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.
This commit is contained in:
packerlschupfer
2026-09-16 12:56:46 +08:00
committed by GitHub
parent 9321f24959
commit 93c8b3f2b0
11 changed files with 717 additions and 133 deletions
+35
View File
@@ -11956,6 +11956,13 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def->tooltip = L("This outputs the model\u2019s information.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("inspect_mesh", coBool);
def->label = L("Inspect mesh (JSON to stdout)");
def->tooltip = L("Print a JSON summary of each loaded object to stdout, then exit: its bounding boxes and the "
"convex hull faces it can be laid on, with their normals, areas and centers. These are the faces "
"the --ground-* options choose from. Machine-readable alternative to --info.");
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.");
@@ -12075,6 +12082,34 @@ CLITransformConfigDef::CLITransformConfigDef()
def->sidetext = u8"°"; // degrees, don't need translation
def->set_default_value(new ConfigOptionFloat(0));
// The --ground-* options choose from the faces the "Lay on Face" gizmo offers. Like the other
// transforms they run in command-line order, so they see the rotations given before them.
def = this->add("ground_largest_face", coBool);
def->label = L("Ground largest face");
def->tooltip = L("Lay each object on the largest face of its convex hull and drop it onto the bed. Of equally large "
"faces, the one already facing down is kept. Objects without a face large enough to rest on are left "
"as they are. Transforms run in command-line order, so rotations given before this option are respected. "
"--orient 1 runs after all transforms and replaces the orientation.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("ground_face_normal", coString);
def->label = L("Ground face by normal");
def->tooltip = L("Lay each object on the convex hull face whose outward normal is closest to the direction NX,NY,NZ "
"and drop it onto the bed. The direction is in object coordinates, which include the rotations given "
"before this option and match the plate axes unless the input file rotates the object. For example, "
"1,0,0 stands the object on its +X side. --orient 1 runs after all transforms and replaces the orientation.");
def->cli_params = "NX,NY,NZ";
def->set_default_value(new ConfigOptionString(""));
def = this->add("ground_face_point", coString);
def->label = L("Ground face at point");
def->tooltip = L("Lay each object on the convex hull face that contains the point X,Y,Z and drop it onto the bed. "
"The point is in object coordinates, which include the rotations given before this option; "
"--inspect-mesh reports face centers in them. Objects without such a face are left as they are, and "
"the run fails if no object has one. --orient 1 runs after all transforms and replaces the orientation.");
def->cli_params = "X,Y,Z";
def->set_default_value(new ConfigOptionString(""));
def = this->add("scale", coFloat);
def->label = L("Scale");
def->tooltip = L("Scale the model by a float factor.");