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
+61
View File
@@ -0,0 +1,61 @@
#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
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <iosfwd>
#include <string>
#include <vector>
namespace Slic3r {
class Model;
namespace MeshInspect {
// Writes the --inspect-mesh JSON for `model` to `out`: per object its bounding boxes and the faces
// it can be laid on, taken from lay_on_face_planes() so they are the faces the --ground-* options
// choose from, in the frame those options take. At most `max_planes` faces are listed per object,
// largest first. `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, size_t max_planes = 8);
} // namespace MeshInspect
} // namespace Slic3r