Files
OrcaSlicer/src/slic3r/Utils/MeshInspect.cpp
T
packerlschupfer 93c8b3f2b0 CLI: --ground-* orientation from the Lay on Face planes, and --inspect-mesh (#15073)
* CLI: --ground-face-* / --lay-flat / --center-on-bed orientation primitives

Adds the CLI counterparts to the GUI's lay-flat / face-pick gizmos.
Scripted / CI / AI pipelines can now set orientation without rendering
a wxWidgets frame; today the only way is a GUI round-trip.

New CLI actions (all operate in the mesh-local frame so they compose
with prior --rotate-* / --orient flags):

  --ground-largest-face 1     Auto-detect the largest planar-face
   or  --lay-flat 1           cluster (area-weighted), rotate so its
                              normal points -Z. Covers "this part has
                              one obvious flat side" cases.

  --ground-face-normal NX,NY,NZ    Pick the face whose mesh-local
                                   normal best matches the given
                                   vector; ground it. e.g.
                                   `--ground-face-normal 1,0,0`
                                   stands a part on its +X side.

  --ground-face-point X,Y,Z        Find the triangle containing the
                                   given mesh-local point; ground its
                                   face. Disambiguates when several
                                   faces share a normal (largest
                                   containing triangle wins).

  --center-on-bed 1                Translate so the XY bounding-box
                                   centroid lands at the bed center
                                   (derived from printable_area).

New file `src/slic3r/Utils/MeshOrient.{hpp,cpp}`:
- collect_triangles_object / compute_face_clusters — quantize
  per-triangle normals (0.001, ~0.06°) and area-weighted-average
  within clusters. Same clustering logic used by lay-flat.
- apply_ground_rotation — same math as Selection::flattening_rotate
  in the GUI (Selection.cpp:1432): world-space quaternion from the
  transformed normal to -Z, applied as offset * new_rot * old_no_offset
  on every instance of every object, then a per-instance Z-lift so the
  grounded face lands at exactly 0 (avoids "No layers were detected"
  from FP-error z≈-1e-9).
- ground_face_point uses a top-N cluster search + point-in-triangle
  test in local space; largest-area triangle wins on ambiguity.

Rationale: without these, any CLI pipeline that needs a specific
face on the bed must either encode custom rotation math per part or
break out of the pipeline into the GUI. Both are bad for
reproducibility. The --ground-face-* triple + the largest-face
auto-mode cover essentially every orientation intent expressible
in a slicing wizard.

Scope:
- `src/slic3r/Utils/MeshOrient.{hpp,cpp}` — new, ~420 lines
- `src/slic3r/CMakeLists.txt` — 2-line registration
- `src/libslic3r/PrintConfig.cpp` — 5 new CLIMiscConfigDef entries
- `src/OrcaSlicer.cpp` — 58-line handler block + 1 include

No behaviour change when the flags are absent.

(cherry picked from commit c45a9795e1)

* CLI grounding: choose among the Lay on Face planes, per object

Addresses review:
- Move the geometry of GLGizmoFlatten::update_planes() into
  libslic3r/LayOnFace and use it from the gizmo and the CLI, so the
  --ground-* options pick convex-hull faces per object and instance,
  with part transformations (--rotate-x/y) applied.
- Drop --center-on-bed, the --lay-flat alias and MeshOrient; make
  --ground-largest-face a coBool.
- Parse --ground-face-normal and --ground-face-point strictly. A point
  that only some objects contain grounds those and leaves the others.
- Fold in --inspect-mesh from #14603, reporting the same planes.
- Tests in tests/libslic3r/test_lay_on_face.cpp: bounding boxes before
  and after, rotate then ground, two objects, and a ribbed part whose
  parallel inner faces outsum its base.

* CLI --inspect-mesh, --ground-face-*: reject missing input and empty values

- Without an input file or --load-assemble-list, --inspect-mesh printed
  nothing and exited 0. Reject it up front with CLI_INVALID_PARAMS.
- An explicit empty --ground-face-normal or --ground-face-point was
  silently ignored. Only options given on the command line reach the
  transforms loop, so an empty value now fails the strict parse like any
  other malformed value.
2026-09-16 12:56:46 +08:00

62 lines
2.6 KiB
C++

#include "MeshInspect.hpp"
#include "libslic3r/LayOnFace.hpp"
#include "libslic3r/Model.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cmath>
#include <ostream>
namespace Slic3r {
namespace MeshInspect {
using json = nlohmann::json;
static json to_json(const Vec3d &v) { return json::array({ v.x(), v.y(), v.z() }); }
static json to_json(const BoundingBoxf3 &bb)
{
return { { "min", to_json(bb.min) }, { "max", to_json(bb.max) }, { "size", to_json(bb.size()) } };
}
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths, std::ostream &out, size_t max_planes)
{
json objects = json::array();
for (const ModelObject *mo : model.objects) {
json obj = { { "name", mo->name },
{ "triangle_count", mo->facets_count() },
{ "instance_count", mo->instances.size() },
{ "bbox_object", to_json(mo->raw_mesh_bounding_box()) } };
if (!mo->instances.empty()) {
const std::vector<LayOnFacePlane> planes = lay_on_face_planes(*mo, mo->instances.front()->get_matrix_no_offset());
json planes_json = json::array();
for (size_t i = 0; i < std::min(planes.size(), max_planes); ++i)
planes_json.push_back({ { "normal", to_json(planes[i].normal) },
{ "area_mm2", std::round(double(planes[i].area) * 1000.) / 1000. },
{ "center", to_json(planes[i].center) } });
obj["bbox_world"] = to_json(mo->instance_bounding_box(0));
obj["instance_offset"] = to_json(mo->instances.front()->get_offset());
obj["plane_count"] = planes.size();
obj["planes"] = std::move(planes_json);
}
objects.push_back(std::move(obj));
}
const json root = {
{ "sources", source_paths },
{ "note", "Lengths in mm. bbox_object and the plane normals and centers are in object coordinates: the parts as "
"currently transformed, without the instance transformation. --ground-face-normal and "
"--ground-face-point take values in these coordinates. area_mm2 uses instance 0's scale, "
"bbox_world is instance 0 on the plate." },
{ "objects", std::move(objects) },
};
// 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.
out << root.dump(2, ' ', false, json::error_handler_t::replace) << std::endl;
}
} // namespace MeshInspect
} // namespace Slic3r