diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 29c81caa41..0c9bd0b1ee 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -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 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(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 65f43b3ee5..74d332ca01 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -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."); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index ca26b80da2..656d41f338 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -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 diff --git a/src/slic3r/Utils/PaintCLI.cpp b/src/slic3r/Utils/PaintCLI.cpp new file mode 100644 index 0000000000..d99dc0b450 --- /dev/null +++ b/src/slic3r/Utils/PaintCLI.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 + +#include +#include +#include +#include + +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(); + 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> &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> &supports_states() +{ + static const std::vector> s = { + { EnforcerBlockerType::ENFORCER, "ENFORCER" }, + { EnforcerBlockerType::BLOCKER, "BLOCKER" }, + }; + return s; +} + +const std::vector> &fuzzy_states() +{ + // FUZZY_SKIN is an enum alias for ENFORCER; the layer is single-state. + static const std::vector> s = { + { EnforcerBlockerType::FUZZY_SKIN, "FUZZY_SKIN" }, + }; + return s; +} + +const std::vector> &mmu_states() +{ + static std::vector> s = []{ + std::vector> 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 &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 diff --git a/src/slic3r/Utils/PaintCLI.hpp b/src/slic3r/Utils/PaintCLI.hpp new file mode 100644 index 0000000000..8e31750b37 --- /dev/null +++ b/src/slic3r/Utils/PaintCLI.hpp @@ -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 +#include +#include + +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 &source_paths, + std::ostream &out); + +} // namespace PaintCLI +} // namespace Slic3r + +#endif