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:
packerlschupfer
2026-09-17 12:01:49 +08:00
committed by GitHub
parent 6b0e190e64
commit ca668a3bc9
5 changed files with 298 additions and 0 deletions
+48
View File
@@ -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();