mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-17 05:52:39 +00:00
CLI: --inspect-paint — dump per-facet paint state as JSON (#14608)
* CLI: --inspect-paint — dump per-facet paint state as JSON
Reads the per-facet enforcer/blocker/extruder/fuzzy-skin state stored
on every ModelVolume (supported_facets / seam_facets /
mmu_segmentation_facets / fuzzy_skin_facets) and emits a structured
JSON summary to stdout. Machine-readable alternative to opening the
paint gizmos.
Per (object, volume, layer, state): facet count, surface area in
mm², and mesh-local bounding box. Empty layers collapse to
{"empty": true}. Summary at the top level rolls up totals.
One correctness detail worth calling out: FacetsAnnotation::
get_facets_strict returns an indexed_triangle_set whose `vertices`
array is the whole source mesh — only `indices` are filtered to the
painted triangles. A naive bounding_box(its) would report the whole
mesh's bbox even when only a few facets are painted. The helper
its_referenced_bbox() walks only the vertices actually indexed by
the painted triangles, so `bbox` correctly localizes the painted
region.
Rationale: every paint-driven workflow — GUI-painted .3mf verified
in CI, AI agents planning support enforcers, MMU color layout checks
— needs to know what's already painted on a model. Today that's a
GUI-only read. --inspect-paint closes that loop for scripted callers.
New file src/slic3r/Utils/PaintCLI.{hpp,cpp} (~215 lines). Depends
only on Model, TriangleMesh, TriangleSelector, FacetsAnnotation, and
nlohmann::json — all already in tree. No new dependencies, no
signature changes, no behavior change when the flag is absent.
Registered as an action (parallel to --info) so it satisfies the
"needs an action" check and bypasses the GUI fallback; control falls
through the normal post-action path to a clean exit 0.
Verification:
unpainted STL: every layer {"empty": true}, summary zero
GUI-painted .3mf: enforcer count / area / bbox match painter
clean JSON: parseable via jq
* CLI --inspect-paint: exit after printing, reject conflicting actions
- Finish like the end of CLI::run once the JSON is written, as the
tooltip says. The callback manager is Linux-only, so its use is
guarded.
- Reject actions that would otherwise be skipped without notice
(--slice, --export-3mf, ...) before loading. Load-time options such as
--uptodate are still accepted.
- Replace invalid UTF-8 in object names and paths instead of throwing.
- Report every input file as sources; inputs are merged into one model
before actions run.
* CLI --inspect-paint: reject a run without input
Without an input file or --load-assemble-list there is nothing to
inspect, and the run printed nothing and exited 0. Reject it up front
with CLI_INVALID_PARAMS, next to the other invalid-parameter checks.
This commit is contained in:
@@ -87,6 +87,7 @@ using namespace nlohmann;
|
||||
#include "dev-utils/BaseException.h"
|
||||
#endif
|
||||
#include "slic3r/Utils/MeshInspect.hpp"
|
||||
#include "slic3r/Utils/PaintCLI.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/BitmapCache.hpp"
|
||||
#include "slic3r/GUI/OpenGLManager.hpp"
|
||||
@@ -1443,6 +1444,29 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
// --inspect-paint prints its JSON and exits, so any action that does work of its
|
||||
// own (slicing, exporting) would be skipped without notice. Reject those up front;
|
||||
// only options that merely tune how the input is loaded may come along.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "inspect_paint") != m_actions.end()) {
|
||||
static const std::set<std::string> inspect_compatible = { "inspect_paint", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::string &action : m_actions) {
|
||||
if (inspect_compatible.count(action) == 0) {
|
||||
std::string flag = action;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--inspect-paint cannot be combined with --" << flag << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
// Without input there is nothing to inspect; fail rather than print nothing and exit 0.
|
||||
if (m_input_files.empty() && m_config.opt_string("load_assemble_list").empty()) {
|
||||
boost::nowide::cerr << "--inspect-paint needs an input file or --load-assemble-list" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
|
||||
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
|
||||
// sliced or exported.
|
||||
@@ -6100,6 +6124,30 @@ int CLI::run(int argc, char **argv)
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
record_exit_reson(outfile_dir, CLI_SUCCESS, plate_to_slice, cli_errors[CLI_SUCCESS], sliced_info);
|
||||
boost::nowide::cerr.flush();
|
||||
return CLI_SUCCESS;
|
||||
} else if (opt_key == "inspect_paint") {
|
||||
// --inspect-paint — read the per-facet enforcer/blocker/extruder/
|
||||
// fuzzy state from the loaded model and emit a JSON summary.
|
||||
// Machine-readable alternative to opening the paint gizmos.
|
||||
for (Model &model : m_models) {
|
||||
model.add_default_instances();
|
||||
Slic3r::PaintCLI::inspect_to_json(model, m_input_files, boost::nowide::cout);
|
||||
}
|
||||
boost::nowide::cout.flush();
|
||||
// The tooltip promises "then exit"; conflicting actions were rejected before
|
||||
// loading. Finish like the end of run(). flush_and_exit() is not usable here:
|
||||
// it prints "found error ..." to stdout, which would corrupt the JSON.
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
if (g_cli_callback_mgr.is_started()) {
|
||||
PrintBase::SlicingStatus slicing_status{100, "All done, Success"};
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
|
||||
@@ -11991,6 +11991,19 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
"the --ground-* options choose from. Machine-readable alternative to --info.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// --inspect-paint \u2014 dump the per-facet enforcer/blocker/extruder/fuzzy
|
||||
// paint state stored on the loaded model (supports, seam, MMU color,
|
||||
// fuzzy-skin) as JSON. Read-only; lets CI / scripted / AI tooling
|
||||
// reason about existing paint on a .3mf without loading the GUI.
|
||||
def = this->add("inspect_paint", coBool);
|
||||
def->label = L("Inspect paint (JSON to stdout)");
|
||||
def->tooltip = L("Print a structured JSON summary of every painted layer "
|
||||
"(supports, seam, MMU color, fuzzy-skin) already stored on "
|
||||
"the loaded model \u2014 per-state facet count, surface area, "
|
||||
"and mesh-local bounding box \u2014 then exit. Machine-readable "
|
||||
"alternative to opening the paint gizmos in the GUI.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("export_settings", coString);
|
||||
def->label = L("Export Settings");
|
||||
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
|
||||
|
||||
@@ -682,6 +682,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/Bonjour.hpp
|
||||
Utils/MeshInspect.cpp
|
||||
Utils/MeshInspect.hpp
|
||||
Utils/PaintCLI.cpp
|
||||
Utils/PaintCLI.hpp
|
||||
Utils/CalibUtils.cpp
|
||||
Utils/CalibUtils.hpp
|
||||
Utils/ColorSpaceConvert.cpp
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// PaintCLI.cpp — CLI paint-inspection primitives. See PaintCLI.hpp.
|
||||
#include "PaintCLI.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace PaintCLI {
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
double its_surface_area(const indexed_triangle_set &its)
|
||||
{
|
||||
double total = 0.0;
|
||||
for (const stl_triangle_vertex_indices &t : its.indices) {
|
||||
const Vec3f &a = its.vertices[t(0)];
|
||||
const Vec3f &b = its.vertices[t(1)];
|
||||
const Vec3f &c = its.vertices[t(2)];
|
||||
total += 0.5 * (b - a).cross(c - a).norm();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Bbox over triangle-referenced vertices only. get_facets_strict() returns
|
||||
// an itset with the full source vertex list — using bounding_box() on it
|
||||
// would report the whole mesh's bbox even when only a few facets are painted.
|
||||
BoundingBoxf3 its_referenced_bbox(const indexed_triangle_set &its)
|
||||
{
|
||||
BoundingBoxf3 bb;
|
||||
bool first = true;
|
||||
for (const stl_triangle_vertex_indices &t : its.indices) {
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const Vec3d v = its.vertices[t(k)].cast<double>();
|
||||
if (first) { bb.min = bb.max = v; first = false; }
|
||||
else bb.merge(v);
|
||||
}
|
||||
}
|
||||
return bb;
|
||||
}
|
||||
|
||||
json vec3_to_json(const Vec3d &v)
|
||||
{
|
||||
return json::array({ v.x(), v.y(), v.z() });
|
||||
}
|
||||
|
||||
json bbox_to_json(const BoundingBoxf3 &bb)
|
||||
{
|
||||
return {
|
||||
{ "min", vec3_to_json(bb.min) },
|
||||
{ "max", vec3_to_json(bb.max) },
|
||||
{ "size", vec3_to_json(Vec3d(bb.max - bb.min)) },
|
||||
};
|
||||
}
|
||||
|
||||
// One (layer, state) row — empty ones are omitted at the caller level.
|
||||
json state_entry(const std::string &label, const indexed_triangle_set &its)
|
||||
{
|
||||
return {
|
||||
{ "state", label },
|
||||
{ "facets", its.indices.size() },
|
||||
{ "area_mm2", its_surface_area(its) },
|
||||
{ "bbox", bbox_to_json(its_referenced_bbox(its)) },
|
||||
};
|
||||
}
|
||||
|
||||
// Iterate the states relevant to one FacetsAnnotation kind, collecting
|
||||
// non-empty entries. Empty layer → {"empty": true}. `n_facets_out` is the
|
||||
// running total of painted facets — bumped for the summary.
|
||||
json inspect_layer(const ModelVolume &mv, const FacetsAnnotation &fa,
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &states,
|
||||
size_t &n_facets_out)
|
||||
{
|
||||
if (fa.empty())
|
||||
return { { "empty", true } };
|
||||
|
||||
json entries = json::array();
|
||||
for (const auto &st : states) {
|
||||
if (!fa.has_facets(mv, st.first))
|
||||
continue;
|
||||
indexed_triangle_set its = fa.get_facets_strict(mv, st.first);
|
||||
if (its.indices.empty())
|
||||
continue;
|
||||
n_facets_out += its.indices.size();
|
||||
entries.push_back(state_entry(st.second, its));
|
||||
}
|
||||
return {
|
||||
{ "empty", entries.empty() },
|
||||
{ "states", std::move(entries) },
|
||||
};
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &supports_states()
|
||||
{
|
||||
static const std::vector<std::pair<EnforcerBlockerType, std::string>> s = {
|
||||
{ EnforcerBlockerType::ENFORCER, "ENFORCER" },
|
||||
{ EnforcerBlockerType::BLOCKER, "BLOCKER" },
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &fuzzy_states()
|
||||
{
|
||||
// FUZZY_SKIN is an enum alias for ENFORCER; the layer is single-state.
|
||||
static const std::vector<std::pair<EnforcerBlockerType, std::string>> s = {
|
||||
{ EnforcerBlockerType::FUZZY_SKIN, "FUZZY_SKIN" },
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &mmu_states()
|
||||
{
|
||||
static std::vector<std::pair<EnforcerBlockerType, std::string>> s = []{
|
||||
std::vector<std::pair<EnforcerBlockerType, std::string>> v;
|
||||
for (int i = 1; i <= int(EnforcerBlockerType::ExtruderMax); ++i)
|
||||
v.emplace_back(EnforcerBlockerType(i), "extruder_" + std::to_string(i));
|
||||
return v;
|
||||
}();
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths,
|
||||
std::ostream &out)
|
||||
{
|
||||
json root;
|
||||
root["sources"] = source_paths;
|
||||
root["frame"] = "mesh_local";
|
||||
root["note"] = "Coordinates are mesh-local (each volume's own frame). "
|
||||
"Paint gizmos operate in this frame.";
|
||||
|
||||
json objects = json::array();
|
||||
size_t total_objects = 0, total_volumes = 0, total_painted = 0, total_facets = 0;
|
||||
|
||||
for (size_t oi = 0; oi < model.objects.size(); ++oi) {
|
||||
const ModelObject *mo = model.objects[oi];
|
||||
if (!mo) continue;
|
||||
++total_objects;
|
||||
|
||||
json obj;
|
||||
obj["index"] = oi;
|
||||
obj["name"] = mo->name;
|
||||
|
||||
json volumes = json::array();
|
||||
for (size_t vi = 0; vi < mo->volumes.size(); ++vi) {
|
||||
const ModelVolume *mv = mo->volumes[vi];
|
||||
if (!mv) continue;
|
||||
++total_volumes;
|
||||
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
json vol;
|
||||
vol["index"] = vi;
|
||||
vol["name"] = mv->name;
|
||||
vol["n_facets"] = its.indices.size();
|
||||
vol["is_model_part"] = mv->is_model_part();
|
||||
vol["bbox_mesh_local"] = bbox_to_json(bounding_box(its));
|
||||
|
||||
size_t vol_painted = 0;
|
||||
json paints;
|
||||
paints["supports"] = inspect_layer(*mv, mv->supported_facets,
|
||||
supports_states(), vol_painted);
|
||||
paints["seam"] = inspect_layer(*mv, mv->seam_facets,
|
||||
supports_states(), vol_painted);
|
||||
paints["mmu_segmentation"] = inspect_layer(*mv, mv->mmu_segmentation_facets,
|
||||
mmu_states(), vol_painted);
|
||||
paints["fuzzy_skin"] = inspect_layer(*mv, mv->fuzzy_skin_facets,
|
||||
fuzzy_states(), vol_painted);
|
||||
vol["paints"] = std::move(paints);
|
||||
vol["painted_facets_total"] = vol_painted;
|
||||
|
||||
if (vol_painted > 0) ++total_painted;
|
||||
total_facets += vol_painted;
|
||||
|
||||
volumes.push_back(std::move(vol));
|
||||
}
|
||||
obj["volumes"] = std::move(volumes);
|
||||
objects.push_back(std::move(obj));
|
||||
}
|
||||
root["objects"] = std::move(objects);
|
||||
root["summary"] = {
|
||||
{ "objects", total_objects },
|
||||
{ "volumes", total_volumes },
|
||||
{ "volumes_with_paint", total_painted },
|
||||
{ "painted_facets_total", total_facets },
|
||||
};
|
||||
|
||||
// Object names and file paths are arbitrary bytes, and dump() throws on invalid
|
||||
// UTF-8 by default. Replace such sequences with U+FFFD so the output is always
|
||||
// valid JSON rather than an exception out of the CLI.
|
||||
out << root.dump(2, ' ', false, json::error_handler_t::replace) << std::endl;
|
||||
}
|
||||
|
||||
} // namespace PaintCLI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,31 @@
|
||||
// PaintCLI.hpp — CLI paint-inspection primitives.
|
||||
//
|
||||
// Backs the --inspect-paint CLI action. Reads the per-facet enforcer /
|
||||
// blocker / extruder / fuzzy-skin state that OrcaSlicer stores on every
|
||||
// ModelVolume (supports, seam, MMU color, fuzzy-skin) and emits a
|
||||
// structured JSON summary — facet count, surface area, and mesh-local
|
||||
// bounding box per state — so CI / scripted / AI tooling can reason
|
||||
// about existing paint on a .3mf without opening the GUI.
|
||||
//
|
||||
// Coordinates are mesh-local (each volume's own frame), matching the
|
||||
// frame that the paint gizmos operate in.
|
||||
#ifndef slic3r_PaintCLI_hpp_
|
||||
#define slic3r_PaintCLI_hpp_
|
||||
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
class Model;
|
||||
|
||||
namespace PaintCLI {
|
||||
|
||||
// `source_paths` lists every input file; the CLI merges them into one Model.
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths,
|
||||
std::ostream &out);
|
||||
|
||||
} // namespace PaintCLI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user