mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 05:27:50 +00:00
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.
This commit is contained in:
@@ -80,6 +80,7 @@ using namespace nlohmann;
|
||||
#ifdef WIN32
|
||||
#include "dev-utils/BaseException.h"
|
||||
#endif
|
||||
#include "slic3r/Utils/MeshOrient.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/BitmapCache.hpp"
|
||||
#include "slic3r/GUI/OpenGLManager.hpp"
|
||||
@@ -4447,6 +4448,63 @@ int CLI::run(int argc, char **argv)
|
||||
for (auto &o : model.objects)
|
||||
// this affects volumes:
|
||||
o->rotate(Geometry::deg2rad(m_config.opt_float(opt_key)), Y);
|
||||
} else if (opt_key == "ground_largest_face" || opt_key == "lay_flat") {
|
||||
// Find the largest planar face cluster across all volumes and rotate so it
|
||||
// sits on Z=0. Single flag; the operator passes `1` to enable.
|
||||
if (m_config.option<ConfigOptionInt>(opt_key)->value != 0) {
|
||||
for (auto &model : m_models)
|
||||
Slic3r::MeshOrient::ground_largest_face(model);
|
||||
}
|
||||
} else if (opt_key == "ground_face_normal") {
|
||||
// Rotate so the face whose mesh-local normal best matches NX,NY,NZ sits on Z=0.
|
||||
const std::string &s = m_config.option<ConfigOptionString>(opt_key)->value;
|
||||
if (!s.empty()) {
|
||||
Vec3d n;
|
||||
if (sscanf(s.c_str(), "%lf,%lf,%lf", &n.x(), &n.y(), &n.z()) != 3) {
|
||||
BOOST_LOG_TRIVIAL(error) << "--ground-face-normal expects NX,NY,NZ (e.g. 0,0,-1), got: " << s;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
for (auto &model : m_models)
|
||||
Slic3r::MeshOrient::ground_face_normal(model, n);
|
||||
}
|
||||
} else if (opt_key == "ground_face_point") {
|
||||
// Find triangle containing (X,Y,Z) in mesh-local coords, then ground its face.
|
||||
const std::string &s = m_config.option<ConfigOptionString>(opt_key)->value;
|
||||
if (!s.empty()) {
|
||||
Vec3d p;
|
||||
if (sscanf(s.c_str(), "%lf,%lf,%lf", &p.x(), &p.y(), &p.z()) != 3) {
|
||||
BOOST_LOG_TRIVIAL(error) << "--ground-face-point expects X,Y,Z, got: " << s;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
bool any_failed = false;
|
||||
for (auto &model : m_models)
|
||||
if (!Slic3r::MeshOrient::ground_face_point(model, p)) any_failed = true;
|
||||
if (any_failed) {
|
||||
BOOST_LOG_TRIVIAL(error) << "--ground-face-point: point (" << p.x() << "," << p.y() << "," << p.z()
|
||||
<< ") is not on the mesh surface. Pick a point ON a face.";
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
} else if (opt_key == "center_on_bed") {
|
||||
// Translate so bbox XY centroid hits bed center (from printable_area; falls back
|
||||
// to a Core-One-ish (125,110) footprint if the config didn't carry a bed shape).
|
||||
if (m_config.option<ConfigOptionInt>(opt_key)->value != 0) {
|
||||
Vec2d bed_center(125.0, 110.0);
|
||||
if (auto *area_opt = m_print_config.option<ConfigOptionPoints>("printable_area");
|
||||
area_opt && area_opt->values.size() >= 4) {
|
||||
Vec2d lo = area_opt->values[0], hi = area_opt->values[0];
|
||||
for (const auto &p : area_opt->values) {
|
||||
lo.x() = std::min(lo.x(), p.x()); lo.y() = std::min(lo.y(), p.y());
|
||||
hi.x() = std::max(hi.x(), p.x()); hi.y() = std::max(hi.y(), p.y());
|
||||
}
|
||||
bed_center = 0.5 * (lo + hi);
|
||||
}
|
||||
for (auto &model : m_models)
|
||||
Slic3r::MeshOrient::center_on_bed(model, bed_center);
|
||||
}
|
||||
} else if (opt_key == "scale") {
|
||||
float ratio = m_config.opt_float(opt_key);
|
||||
if (ratio <= 0.f) {
|
||||
|
||||
@@ -11994,6 +11994,50 @@ CLITransformConfigDef::CLITransformConfigDef()
|
||||
def->sidetext = u8"°"; // degrees, don't need translation
|
||||
def->set_default_value(new ConfigOptionFloat(0));
|
||||
|
||||
// "Ground a face to the bed" CLI primitives — GUI equivalents (lay-flat / face-pick
|
||||
// gizmos) previously had no CLI counterpart, forcing scripted pipelines to
|
||||
// round-trip through the GUI to set orientation. All work in the mesh-local
|
||||
// frame so they compose with prior --rotate-* flags.
|
||||
def = this->add("ground_largest_face", coInt);
|
||||
def->label = L("Ground largest face");
|
||||
def->tooltip = L("Find the largest planar face on the mesh and rotate so it sits "
|
||||
"on the bed (Z=0). Covers the common 'this part has one obvious "
|
||||
"orientation' case. 1=on, 0=off. Default 0.");
|
||||
def->cli_params = "0|1";
|
||||
def->set_default_value(new ConfigOptionInt(0));
|
||||
|
||||
def = this->add("lay_flat", coInt);
|
||||
def->label = L("Lay flat");
|
||||
def->tooltip = L("Alias for --ground-largest-face. Matches the GUI's lay-flat "
|
||||
"terminology. 1=on, 0=off. Default 0.");
|
||||
def->cli_params = "0|1";
|
||||
def->set_default_value(new ConfigOptionInt(0));
|
||||
|
||||
def = this->add("ground_face_normal", coString);
|
||||
def->label = L("Ground face normal");
|
||||
def->tooltip = L("Rotate so the face whose mesh-local normal best matches NX,NY,NZ "
|
||||
"sits on the bed. Example: --ground-face-normal 0,0,-1 grounds the "
|
||||
"face already pointing -Z (typically already flat). Use 1,0,0 to "
|
||||
"stand a part on its +X side. Vector is normalized internally.");
|
||||
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("Find the triangle containing the given mesh-local point X,Y,Z "
|
||||
"and rotate so its face sits on the bed. Useful when multiple "
|
||||
"faces have similar normals — picking by point disambiguates. "
|
||||
"Coords are mesh-local (post-OrcaSlicer centering).");
|
||||
def->cli_params = "X,Y,Z";
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("center_on_bed", coInt);
|
||||
def->label = L("Center on bed");
|
||||
def->tooltip = L("Translate the model so its XY bounding-box center lands at the bed "
|
||||
"center. Useful after --ground-* operations. 1=on, 0=off. Default 0.");
|
||||
def->cli_params = "0|1";
|
||||
def->set_default_value(new ConfigOptionInt(0));
|
||||
|
||||
def = this->add("scale", coFloat);
|
||||
def->label = L("Scale");
|
||||
def->tooltip = L("Scale the model by a float factor.");
|
||||
|
||||
@@ -655,6 +655,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/bambu_networking.hpp
|
||||
Utils/Bonjour.cpp
|
||||
Utils/Bonjour.hpp
|
||||
Utils/MeshOrient.cpp
|
||||
Utils/MeshOrient.hpp
|
||||
Utils/CalibUtils.cpp
|
||||
Utils/CalibUtils.hpp
|
||||
Utils/ColorSpaceConvert.cpp
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
// MeshOrient.cpp — CLI orientation primitives. See MeshOrient.hpp.
|
||||
#include "MeshOrient.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/format.hpp>
|
||||
|
||||
#include <Eigen/Geometry>
|
||||
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace MeshOrient {
|
||||
|
||||
namespace {
|
||||
|
||||
// Per-triangle data: outward normal (unit) + area (mm²).
|
||||
struct TriInfo {
|
||||
Vec3d normal;
|
||||
double area;
|
||||
};
|
||||
|
||||
// Iterate every triangle of every volume of every object, return their unit
|
||||
// normals + areas (all in mesh-local coords). Skips degenerate (zero-area)
|
||||
// triangles. The caller is interested in the surface composition, not which
|
||||
// triangle came from where — area is the only weight that matters.
|
||||
std::vector<TriInfo> collect_triangles(const Model &model)
|
||||
{
|
||||
std::vector<TriInfo> tris;
|
||||
for (const ModelObject *mo : model.objects) {
|
||||
if (!mo) continue;
|
||||
for (const ModelVolume *mv : mo->volumes) {
|
||||
if (!mv || !mv->is_model_part()) continue;
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
tris.reserve(tris.size() + its.indices.size());
|
||||
for (const Vec3i32 &tri : its.indices) {
|
||||
const Vec3f &a = its.vertices[tri[0]];
|
||||
const Vec3f &b = its.vertices[tri[1]];
|
||||
const Vec3f &c = its.vertices[tri[2]];
|
||||
const Vec3d ab = (b - a).cast<double>();
|
||||
const Vec3d ac = (c - a).cast<double>();
|
||||
const Vec3d cross = ab.cross(ac);
|
||||
const double mag = cross.norm();
|
||||
if (mag < 1e-12) continue; // degenerate
|
||||
tris.push_back({ cross / mag, 0.5 * mag });
|
||||
}
|
||||
}
|
||||
}
|
||||
return tris;
|
||||
}
|
||||
|
||||
// Apply a rotation that maps `target_normal_mesh` (mesh-local, will be
|
||||
// normalized) to -Z, to every instance of every object. Same math as
|
||||
// Selection::flattening_rotate (GUI Selection.cpp:1432) — quaternion from
|
||||
// the world-space transformed normal to -Z, applied after the existing
|
||||
// instance matrix (preserving offset).
|
||||
void apply_ground_rotation(Model &model, const Vec3d &target_normal_mesh)
|
||||
{
|
||||
const Vec3d n_mesh = target_normal_mesh.normalized();
|
||||
for (ModelObject *mo : model.objects) {
|
||||
if (!mo) continue;
|
||||
for (ModelInstance *inst : mo->instances) {
|
||||
if (!inst) continue;
|
||||
const Geometry::Transformation &t = inst->get_transformation();
|
||||
// Transform the mesh-local normal into world coords via the
|
||||
// inverse-transpose of the rotation/scale (3x3 block).
|
||||
const Vec3d tnormal = t.get_matrix().matrix().block(0, 0, 3, 3)
|
||||
.inverse().transpose() * n_mesh;
|
||||
const Eigen::Quaterniond q = Eigen::Quaterniond()
|
||||
.setFromTwoVectors(tnormal.normalized(), -Vec3d::UnitZ());
|
||||
const Transform3d rotation(q);
|
||||
// Compose new matrix: offset * new_rotation * old_no_offset.
|
||||
const Transform3d new_matrix = t.get_offset_matrix()
|
||||
* rotation
|
||||
* t.get_matrix_no_offset();
|
||||
inst->set_transformation(Geometry::Transformation(new_matrix));
|
||||
}
|
||||
//ORCA: lift each instance so its grounded face sits exactly at Z=0.
|
||||
// ModelObject::ensure_on_bed() is a no-op for CLI-loaded instances
|
||||
// (it skips any instance whose auto_drop flag is false, and CLI
|
||||
// loaders default that to false). Slicer-chat 2026-06-22 hit this:
|
||||
// after Quaterniond rotation the grounded face landed at z≈-1e-9
|
||||
// due to FP, the slicer then rejected the model with the cryptic
|
||||
// "No layers were detected" error. Apply the lift directly: per
|
||||
// instance, compute its world-coord bbox min.z and shift the
|
||||
// offset by -min.z (zero if already on or above bed).
|
||||
for (size_t i = 0; i < mo->instances.size(); ++i) {
|
||||
ModelInstance *inst = mo->instances[i];
|
||||
if (!inst) continue;
|
||||
const BoundingBoxf3 ib = mo->instance_bounding_box(i, false);
|
||||
const double min_z = ib.min.z();
|
||||
if (min_z != 0.0) { // covers below AND above
|
||||
Vec3d o = inst->get_offset();
|
||||
o.z() -= min_z;
|
||||
inst->set_offset(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ground_largest_face(Model &model, std::string *out_chosen_normal)
|
||||
{
|
||||
const std::vector<TriInfo> tris = collect_triangles(model);
|
||||
if (tris.empty()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MeshOrient: model has no triangles to ground";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cluster triangles by quantized normal direction. Quantize each normal
|
||||
// component to 0.001 (≈ 0.06° angular precision) — coplanar triangles
|
||||
// from triangulation share a normal to many decimals, so this groups
|
||||
// them while keeping distinct face orientations apart.
|
||||
struct QKey { int x, y, z; };
|
||||
struct QKeyHash { size_t operator()(const QKey &k) const noexcept {
|
||||
return (size_t(uint32_t(k.x)) * 73856093u)
|
||||
^ (size_t(uint32_t(k.y)) * 19349663u)
|
||||
^ (size_t(uint32_t(k.z)) * 83492791u);
|
||||
}};
|
||||
struct QKeyEq { bool operator()(const QKey &a, const QKey &b) const noexcept {
|
||||
return a.x == b.x && a.y == b.y && a.z == b.z;
|
||||
}};
|
||||
|
||||
auto quantize = [](const Vec3d &n) -> QKey {
|
||||
return { int(std::lround(n.x() * 1000.0)),
|
||||
int(std::lround(n.y() * 1000.0)),
|
||||
int(std::lround(n.z() * 1000.0)) };
|
||||
};
|
||||
|
||||
// Bucket sum-of-area per quantized normal, plus the area-weighted normal
|
||||
// sum so we can recover a precise representative direction at the end.
|
||||
struct Bucket { double area = 0.0; Vec3d weighted_normal = Vec3d::Zero(); };
|
||||
std::unordered_map<QKey, Bucket, QKeyHash, QKeyEq> buckets;
|
||||
buckets.reserve(tris.size() / 4 + 1);
|
||||
|
||||
for (const TriInfo &t : tris) {
|
||||
Bucket &b = buckets[quantize(t.normal)];
|
||||
b.area += t.area;
|
||||
b.weighted_normal += t.area * t.normal;
|
||||
}
|
||||
|
||||
// Pick the bucket with maximum accumulated area.
|
||||
double max_area = -1.0;
|
||||
Vec3d best_normal = Vec3d::UnitZ();
|
||||
for (const auto &kv : buckets) {
|
||||
if (kv.second.area > max_area) {
|
||||
max_area = kv.second.area;
|
||||
best_normal = kv.second.weighted_normal.normalized();
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format(
|
||||
"MeshOrient::ground_largest_face: %1% triangles, %2% distinct normals, "
|
||||
"largest cluster area=%3$.3f mm² normal=(%4$+.4f, %5$+.4f, %6$+.4f)")
|
||||
% tris.size() % buckets.size() % max_area
|
||||
% best_normal.x() % best_normal.y() % best_normal.z();
|
||||
|
||||
if (out_chosen_normal)
|
||||
*out_chosen_normal = (boost::format("(%1$+.4f,%2$+.4f,%3$+.4f)")
|
||||
% best_normal.x() % best_normal.y() % best_normal.z()).str();
|
||||
|
||||
apply_ground_rotation(model, best_normal);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ground_face_normal(Model &model, const Vec3d &target_normal)
|
||||
{
|
||||
if (target_normal.norm() < 1e-9) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MeshOrient::ground_face_normal: zero-length target normal";
|
||||
return false;
|
||||
}
|
||||
const Vec3d target = target_normal.normalized();
|
||||
|
||||
const std::vector<TriInfo> tris = collect_triangles(model);
|
||||
if (tris.empty()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MeshOrient::ground_face_normal: model has no triangles";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Of all the triangles whose normal best matches `target`, pick the
|
||||
// representative as the area-weighted average of the cluster of
|
||||
// triangles within an angular tolerance of the best hit (0.5° ≈ 1e-4
|
||||
// in dot-product terms). This handles re-triangulated meshes where the
|
||||
// matching face was split into many triangles.
|
||||
double best_dot = -2.0;
|
||||
for (const TriInfo &t : tris)
|
||||
best_dot = std::max(best_dot, t.normal.dot(target));
|
||||
|
||||
const double tol = 1e-4;
|
||||
Vec3d weighted_normal = Vec3d::Zero();
|
||||
double cluster_area = 0.0;
|
||||
for (const TriInfo &t : tris) {
|
||||
const double d = t.normal.dot(target);
|
||||
if (d >= best_dot - tol) {
|
||||
weighted_normal += t.area * t.normal;
|
||||
cluster_area += t.area;
|
||||
}
|
||||
}
|
||||
const Vec3d chosen = weighted_normal.normalized();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format(
|
||||
"MeshOrient::ground_face_normal: target=(%1$+.4f,%2$+.4f,%3$+.4f) "
|
||||
"→ chosen=(%4$+.4f,%5$+.4f,%6$+.4f) cluster_area=%7$.3f mm² "
|
||||
"best_dot=%8$.6f")
|
||||
% target.x() % target.y() % target.z()
|
||||
% chosen.x() % chosen.y() % chosen.z()
|
||||
% cluster_area % best_dot;
|
||||
|
||||
apply_ground_rotation(model, chosen);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ground_face_point(Model &model, const Vec3d &point_mesh)
|
||||
{
|
||||
// First pass: compute the mesh bbox centroid in mesh-local coords. When
|
||||
// multiple faces overlap the click point (internal + external surfaces
|
||||
// of a wall, both sides of an edge), we use this centroid to disambiguate:
|
||||
// prefer the face whose normal points AWAY from the centroid (the
|
||||
// "outward-facing" surface — what an operator pointing at the part
|
||||
// intuitively means by "this face").
|
||||
BoundingBoxf3 bbox;
|
||||
bool have_box = false;
|
||||
for (const ModelObject *mo : model.objects) {
|
||||
if (!mo) continue;
|
||||
for (const ModelVolume *mv : mo->volumes) {
|
||||
if (!mv || !mv->is_model_part()) continue;
|
||||
for (const Vec3f &v : mv->mesh().its.vertices) {
|
||||
if (!have_box) { bbox.min = bbox.max = v.cast<double>(); have_box = true; }
|
||||
else bbox.merge(v.cast<double>());
|
||||
}
|
||||
}
|
||||
}
|
||||
Vec3d mesh_centroid = Vec3d::Zero();
|
||||
if (have_box) mesh_centroid = 0.5 * (bbox.min + bbox.max);
|
||||
|
||||
// Find every triangle that contains `point_mesh` (barycentric check),
|
||||
// then pick the most outward-facing one (highest dot product of normal
|
||||
// with the centroid→point direction). Ties broken by larger area.
|
||||
Vec3d best_normal = Vec3d::UnitZ();
|
||||
double best_outward = -2.0;
|
||||
double best_area = -1.0;
|
||||
int hits = 0;
|
||||
Vec3d outward_ref(0.0, 0.0, -1.0);
|
||||
if ((point_mesh - mesh_centroid).norm() > 1e-9)
|
||||
outward_ref = (point_mesh - mesh_centroid).normalized();
|
||||
|
||||
for (const ModelObject *mo : model.objects) {
|
||||
if (!mo) continue;
|
||||
for (const ModelVolume *mv : mo->volumes) {
|
||||
if (!mv || !mv->is_model_part()) continue;
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
for (const Vec3i32 &tri : its.indices) {
|
||||
const Vec3d a = its.vertices[tri[0]].cast<double>();
|
||||
const Vec3d b = its.vertices[tri[1]].cast<double>();
|
||||
const Vec3d c = its.vertices[tri[2]].cast<double>();
|
||||
const Vec3d ab = b - a;
|
||||
const Vec3d ac = c - a;
|
||||
const Vec3d cross = ab.cross(ac);
|
||||
const double area2 = cross.norm(); // 2× area
|
||||
if (area2 < 1e-12) continue;
|
||||
const Vec3d n = cross / area2;
|
||||
|
||||
// Plane distance (point must lie in the triangle's plane).
|
||||
const Vec3d ap = point_mesh - a;
|
||||
const double plane_d = std::abs(ap.dot(n));
|
||||
if (plane_d > 1e-3) continue;
|
||||
|
||||
// Barycentrics via projection onto ab/ac basis.
|
||||
const double d00 = ab.dot(ab);
|
||||
const double d01 = ab.dot(ac);
|
||||
const double d11 = ac.dot(ac);
|
||||
const double d20 = ap.dot(ab);
|
||||
const double d21 = ap.dot(ac);
|
||||
const double denom = d00 * d11 - d01 * d01;
|
||||
if (std::abs(denom) < 1e-12) continue;
|
||||
const double v = (d11 * d20 - d01 * d21) / denom;
|
||||
const double w = (d00 * d21 - d01 * d20) / denom;
|
||||
const double u = 1.0 - v - w;
|
||||
const double tol = 1e-3;
|
||||
if (u < -tol || v < -tol || w < -tol) continue;
|
||||
|
||||
++hits;
|
||||
const double outward = n.dot(outward_ref);
|
||||
const double tri_area = 0.5 * area2;
|
||||
// Outward direction wins; area is the tiebreaker.
|
||||
if (outward > best_outward + 1e-6 ||
|
||||
(std::abs(outward - best_outward) <= 1e-6 && tri_area > best_area)) {
|
||||
best_outward = outward;
|
||||
best_area = tri_area;
|
||||
best_normal = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hits == 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format(
|
||||
"MeshOrient::ground_face_point: no triangle contains point "
|
||||
"(%1$.3f, %2$.3f, %3$.3f) — give a point ON the mesh surface")
|
||||
% point_mesh.x() % point_mesh.y() % point_mesh.z();
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format(
|
||||
"MeshOrient::ground_face_point: point=(%1$.3f,%2$.3f,%3$.3f) hits=%4% "
|
||||
"chosen_normal=(%5$+.4f,%6$+.4f,%7$+.4f) area=%8$.3f mm²")
|
||||
% point_mesh.x() % point_mesh.y() % point_mesh.z()
|
||||
% hits
|
||||
% best_normal.x() % best_normal.y() % best_normal.z()
|
||||
% best_area;
|
||||
|
||||
apply_ground_rotation(model, best_normal);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool center_on_bed(Model &model, const Vec2d &bed_center)
|
||||
{
|
||||
// Compute the combined bounding box of every instance in world coords,
|
||||
// then translate every instance by (bed_center - bbox_xy_centroid).
|
||||
// Z is left alone — ensure_on_bed sets that.
|
||||
if (model.objects.empty()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MeshOrient::center_on_bed: model is empty";
|
||||
return false;
|
||||
}
|
||||
BoundingBoxf3 combined;
|
||||
bool have_box = false;
|
||||
for (const ModelObject *mo : model.objects) {
|
||||
if (!mo) continue;
|
||||
for (size_t i = 0; i < mo->instances.size(); ++i) {
|
||||
const BoundingBoxf3 ib = mo->instance_bounding_box(i);
|
||||
if (!have_box) { combined = ib; have_box = true; }
|
||||
else combined.merge(ib);
|
||||
}
|
||||
}
|
||||
if (!have_box) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MeshOrient::center_on_bed: no instances to center";
|
||||
return false;
|
||||
}
|
||||
const Vec2d centroid_xy(0.5 * (combined.min.x() + combined.max.x()),
|
||||
0.5 * (combined.min.y() + combined.max.y()));
|
||||
const Vec3d shift(bed_center.x() - centroid_xy.x(),
|
||||
bed_center.y() - centroid_xy.y(),
|
||||
0.0);
|
||||
for (ModelObject *mo : model.objects) {
|
||||
if (!mo) continue;
|
||||
for (ModelInstance *inst : mo->instances) {
|
||||
if (!inst) continue;
|
||||
inst->set_offset(inst->get_offset() + shift);
|
||||
}
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format(
|
||||
"MeshOrient::center_on_bed: bed=(%1$.1f,%2$.1f) centroid=(%3$.1f,%4$.1f) shift=(%5$+.1f,%6$+.1f)")
|
||||
% bed_center.x() % bed_center.y()
|
||||
% centroid_xy.x() % centroid_xy.y()
|
||||
% shift.x() % shift.y();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace MeshOrient
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,52 @@
|
||||
// MeshOrient.hpp — CLI orientation primitives.
|
||||
//
|
||||
// Implements "ground a face to the bed" operations that the GUI exposes via
|
||||
// the lay-flat / face-pick gizmos but the CLI was missing. Slicer-chat
|
||||
// 2026-06-22: auto-orient picks bad orientations for parts with one obvious
|
||||
// flat face; without these primitives, the operator has to break the
|
||||
// pipeline and round-trip through the GUI just to set orientation.
|
||||
//
|
||||
// All functions operate on Model in-place — they rotate the ModelInstance
|
||||
// transform of every instance of every object so that the chosen face lands
|
||||
// on Z=0 (normal pointing -Z). They don't translate; ensure_on_bed in the
|
||||
// existing CLI pipeline handles the Z-lift afterwards.
|
||||
//
|
||||
// Mesh-local normals: the operator's --ground-face-normal vector is
|
||||
// interpreted in the mesh's *own* coordinate system, NOT in world coords.
|
||||
// This is robust against prior --rotate-* CLI flags being applied first.
|
||||
#ifndef slic3r_MeshOrient_hpp_
|
||||
#define slic3r_MeshOrient_hpp_
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
class Model;
|
||||
|
||||
namespace MeshOrient {
|
||||
|
||||
// Find the largest planar face (cluster of coplanar triangles by mesh-local
|
||||
// normal) across all volumes of all objects in `model`, then rotate every
|
||||
// instance so that face points to -Z. Returns true on success; false +
|
||||
// emits a BOOST_LOG error if the mesh is empty.
|
||||
bool ground_largest_face(Model &model, std::string *out_chosen_normal = nullptr);
|
||||
|
||||
// Rotate every instance so the face whose mesh-local normal best matches
|
||||
// `target_normal` (highest dot product) points to -Z. The target is
|
||||
// normalized internally; (0,0,0) is rejected.
|
||||
bool ground_face_normal(Model &model, const Vec3d &target_normal);
|
||||
|
||||
// Find the triangle that contains `point_mesh` (in mesh-local coords) and
|
||||
// ground its face. If multiple triangles contain the point (edge/vertex),
|
||||
// the triangle with the largest area wins.
|
||||
bool ground_face_point(Model &model, const Vec3d &point_mesh);
|
||||
|
||||
// Translate every instance so the combined model bounding-box centroid sits
|
||||
// at `bed_center` (XY only; Z is left to ensure_on_bed). Called after
|
||||
// orientation when --center-on-bed is passed.
|
||||
bool center_on_bed(Model &model, const Vec2d &bed_center);
|
||||
|
||||
} // namespace MeshOrient
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user