mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
Infer parallel, perpendicular, equal radius and tangent while drawing
Port of snaporca 2d36d28770. Parity OK: 17 files identical, 8 diverging at their expected counts. infer_axis_constraint returned only Horizontal or Vertical. On the CAD-1000-hours corpus the top two transitions are sketch_dim -> sketch_draw (5896) and back (5756): the signature of geometry that does not self-constrain as it is drawn. Every rule requires the relation to be ALREADY TRUE within tolerance, so nothing the user drew is moved; parallel/perpendicular and tangent additionally require a shared endpoint. TWO LIMITS THE CORPUS RUNG FORCED, neither visible to the unit tests: 1. At most ONE constraint per rule per new entity, not one per PAIR, and no one-at-a-time fallback for the relations batch. EqualRadius has no locality restriction, so 200 equal holes produced ~20000 candidates; the rejected batch then cost a solve per constraint and pinned the app at 95% of a core with the MCP socket unresponsive. 2. Relations only for gesture-sized batches. "A scripted add is not a drawn gesture" is already this file's rule at its bulk call site (snaporca-8xg1), and EqualRadius also couples geometrically distant entities, merging independent connected components and defeating the partitioning that makes large sketches solvable (snaporca-yww4). With the cap alone geometry stayed correct (32/32 sheets clean) but seven of the largest timed out, including MPD681 -- the sheet that call site's own comment names. Also fixes the tolerance leak behind 2: the bulk path asks for exact inference with ang_tol_rad = 0 but len_tol_frac kept its 0.01 default. ALSO independent of this feature: run-kernel-tests.sh defaulted to TAGS=[CadDocument] while four CAD test files carry their own tags and nothing selected them (2624 assertions / 206 cases reported, 7648 / 264 actual). All 58 dark cases were passing; the coverage was never exercised. VERIFICATION LIMIT, as with the previous three commits: this fork's kernel suite still cannot run (find_package(assimp) at configure time, snaporca-w80c). Shared sources are byte-identical to snaporca's, where kernel is 7651 assertions / 265 cases and ALL LADDERS HELD 7/7.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "libslic3r/CAD/SketchInference.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -123,4 +124,111 @@ infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Unsigned angle between two (unnormalized) direction vectors, in [0, pi]. 0 = same
|
||||
// direction, pi = opposite, pi/2 = perpendicular. Inputs must be non-degenerate.
|
||||
// static: this is a file-local helper, not part of the module's interface -- at namespace
|
||||
// scope with external linkage it would be a link-time collision waiting to happen.
|
||||
static double unsigned_angle(const Vec2d& a, const Vec2d& b)
|
||||
{
|
||||
const double cross = a.x() * b.y() - a.y() * b.x();
|
||||
const double dot = a.x() * b.x() + a.y() * b.y();
|
||||
return std::atan2(std::abs(cross), dot);
|
||||
}
|
||||
|
||||
std::vector<SketchEntityConstraintDef>
|
||||
infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
|
||||
double ang_tol_rad, double len_tol_frac)
|
||||
{
|
||||
std::vector<SketchEntityConstraintDef> out;
|
||||
if (new_ei <= 0 || new_ei >= int(entities.size())) return out;
|
||||
|
||||
// AT MOST ONE constraint per rule per new entity, not one per PAIR. Without this the
|
||||
// function is quadratic in the sketch: a drawing with 200 equal holes yields ~20000
|
||||
// EqualRadius candidates, the batch is rejected as over-constrained, and the caller's
|
||||
// one-at-a-time fallback then runs a solve per constraint. Measured 2026-08-31: that
|
||||
// pinned the app at 95% of a core with the MCP socket unresponsive -- the same failure
|
||||
// the axes batch above already carries a warning about. Keep the best candidate only.
|
||||
int best_ang_j = -1, best_rad_j = -1, best_tan_j = -1;
|
||||
double best_ang_err = 1e30, best_rad_err = 1e30, best_tan_err = 1e30;
|
||||
SketchConstraintType best_ang_type = SketchConstraintType::Parallel;
|
||||
|
||||
const SketchEntity& n = entities[new_ei];
|
||||
const bool n_line = n.type == SketchEntity::Type::Line;
|
||||
const bool n_curve = n.type == SketchEntity::Type::Arc || n.type == SketchEntity::Type::Circle;
|
||||
if (!n_line && !n_curve) return out; // not a Line / Arc / Circle
|
||||
if (n_line && (n.p1 - n.p0).squaredNorm() < 1e-18) return out; // degenerate
|
||||
if (n_curve && n.radius < 1e-9) return out;
|
||||
|
||||
for (int j = 0; j < new_ei; ++j) {
|
||||
const SketchEntity& o = entities[j];
|
||||
const bool o_line = o.type == SketchEntity::Type::Line;
|
||||
const bool o_curve = o.type == SketchEntity::Type::Arc || o.type == SketchEntity::Type::Circle;
|
||||
if (!o_line && !o_curve) continue;
|
||||
if (o_line && (o.p1 - o.p0).squaredNorm() < 1e-18) continue;
|
||||
if (o_curve && o.radius < 1e-9) continue;
|
||||
|
||||
if (n_line && o_line) {
|
||||
// R1 — parallel / perpendicular, restricted to CONNECTED lines. Connection is
|
||||
// what keeps this from firing on every distant line that is roughly parallel.
|
||||
const bool connected = (n.p0 - o.p0).squaredNorm() <= 1e-14 ||
|
||||
(n.p0 - o.p1).squaredNorm() <= 1e-14 ||
|
||||
(n.p1 - o.p0).squaredNorm() <= 1e-14 ||
|
||||
(n.p1 - o.p1).squaredNorm() <= 1e-14;
|
||||
if (!connected) continue;
|
||||
const double ang = unsigned_angle(n.p1 - n.p0, o.p1 - o.p0);
|
||||
const double par_err = std::min(ang, M_PI - ang);
|
||||
const double per_err = std::abs(ang - M_PI / 2.0);
|
||||
if (par_err <= ang_tol_rad && par_err < best_ang_err) {
|
||||
best_ang_err = par_err; best_ang_j = j;
|
||||
best_ang_type = SketchConstraintType::Parallel;
|
||||
} else if (per_err <= ang_tol_rad && per_err < best_ang_err) {
|
||||
best_ang_err = per_err; best_ang_j = j;
|
||||
best_ang_type = SketchConstraintType::Perpendicular;
|
||||
}
|
||||
} else if (n_curve && o_curve) {
|
||||
// R2 — equal radius between circles / arcs, relative to the larger.
|
||||
const double larger = n.radius > o.radius ? n.radius : o.radius;
|
||||
const double err = std::abs(n.radius - o.radius) / larger;
|
||||
if (err <= len_tol_frac && err < best_rad_err) { best_rad_err = err; best_rad_j = j; }
|
||||
} else {
|
||||
// R3 — tangent where a line meets a circle / arc at a shared endpoint, and only
|
||||
// when the line is ALREADY perpendicular to the radius at that point.
|
||||
const SketchEntity& ln = n_line ? n : o;
|
||||
const SketchEntity& cv = n_line ? o : n;
|
||||
const Vec2d ldir = ln.p1 - ln.p0;
|
||||
bool tangent = false;
|
||||
const Vec2d le[2] = { ln.p0, ln.p1 };
|
||||
for (int k = 0; k < 2 && !tangent; ++k) {
|
||||
if (cv.type == SketchEntity::Type::Arc) {
|
||||
const Vec2d ce[2] = { cv.p0, cv.p1 };
|
||||
for (int m = 0; m < 2; ++m) {
|
||||
if ((le[k] - ce[m]).squaredNorm() > 1e-14) continue;
|
||||
const Vec2d r = ce[m] - cv.center;
|
||||
if (r.squaredNorm() < 1e-18) continue;
|
||||
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
|
||||
if (tangent) break;
|
||||
}
|
||||
} else { // Circle: shared point is a line endpoint on the rim.
|
||||
const Vec2d r = le[k] - cv.center;
|
||||
if (std::abs(r.norm() - cv.radius) > 1e-7) continue;
|
||||
if (r.squaredNorm() < 1e-18) continue;
|
||||
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
|
||||
}
|
||||
}
|
||||
if (tangent && best_tan_err > 0.0) { best_tan_err = 0.0; best_tan_j = j; }
|
||||
}
|
||||
}
|
||||
|
||||
auto emit = [&](SketchConstraintType t, int j) {
|
||||
if (j < 0) return;
|
||||
SketchEntityConstraintDef c;
|
||||
c.type = t; c.ea = j; c.eb = new_ei;
|
||||
out.push_back(c);
|
||||
};
|
||||
emit(best_ang_type, best_ang_j); // R1
|
||||
emit(SketchConstraintType::EqualRadius, best_rad_j); // R2
|
||||
emit(SketchConstraintType::Tangent, best_tan_j); // R3
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -39,6 +39,17 @@ InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
|
||||
std::optional<SketchConstraintType>
|
||||
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = 3.0 * M_PI / 180.0);
|
||||
|
||||
// Relational constraints to auto-emit for a newly drawn entity `new_ei` against the
|
||||
// entities already in the sketch. Pure, no GUI/GL dependencies, unit-testable.
|
||||
//
|
||||
// Deliberately conservative: every rule requires the relation to be ALREADY TRUE within
|
||||
// tolerance, so an inferred constraint never moves geometry the user drew — it only pins a
|
||||
// relation that is visibly there. Returns an empty vector when nothing qualifies.
|
||||
std::vector<SketchEntityConstraintDef>
|
||||
infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
|
||||
double ang_tol_rad = 2.0 * M_PI / 180.0,
|
||||
double len_tol_frac = 0.01);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_SketchInference_hpp_
|
||||
|
||||
@@ -2411,6 +2411,39 @@ void DesignSketchTool::infer_auto_constraints(int base, double ang_tol_rad, doub
|
||||
if (!try_add_constraints(axes))
|
||||
for (const auto& c : axes) try_add_constraints({ c });
|
||||
|
||||
// 3) Relational constraints on the new entities (parallel/perpendicular on connected
|
||||
// lines, equal radius, tangent). Same batch-then-one-at-a-time fallback as above:
|
||||
// try_add_constraints rolls a conflicting batch back, so a single rejected relation
|
||||
// never costs the others. Every rule only pins a relation that is ALREADY true, so
|
||||
// nothing the user drew is moved by this.
|
||||
// A SCRIPTED ADD IS NOT A DRAWN GESTURE — the rule this function already states at its
|
||||
// bulk call site, which passes zero tolerances for exactly that reason (snaporca-8xg1).
|
||||
// Relational inference must obey it too, and for a second reason beyond tolerance:
|
||||
// EqualRadius couples entities that are geometrically far apart, so on a real drawing it
|
||||
// merges independent connected components into one huge system and defeats the
|
||||
// component partitioning that makes large sketches solvable at all (snaporca-yww4).
|
||||
// Measured 2026-08-31 on the corpus rung: geometry stayed correct (32/32 sheets clean)
|
||||
// but seven of the largest sheets hit main-thread timeout — MPD681 among them, the very
|
||||
// sheet named in the comment at the bulk call site. Exact-equality would not save it
|
||||
// either: patterned holes in real drawings ARE exactly equal.
|
||||
// So: relations only for batches the size of a human gesture. A polyline segment is 1, a
|
||||
// rectangle 4, a polygon a dozen; a scripted or imported add is hundreds.
|
||||
constexpr int kRelInferMaxBatch = 16;
|
||||
std::vector<SketchEntityConstraintDef> rels;
|
||||
if (n - base <= kRelInferMaxBatch) {
|
||||
const double rel_len_tol = ang_tol_rad > 0.0 ? 0.01 : 0.0; // exact-only when the caller asked for exact
|
||||
for (int i = base; i < n; ++i) {
|
||||
auto r = infer_relations(m_entities, i, ang_tol_rad, rel_len_tol);
|
||||
rels.insert(rels.end(), r.begin(), r.end());
|
||||
}
|
||||
}
|
||||
// NO one-at-a-time fallback here, unlike the two batches above. Relations are a
|
||||
// convenience: nothing is incorrect without them. The fallback costs one SOLVE PER
|
||||
// CONSTRAINT, which is what the warning on the axes batch is about, and paying it for
|
||||
// optional constraints is how a bulk add pins the app at 100% CPU with the MCP socket
|
||||
// unresponsive (measured 2026-08-31). If the batch conflicts, drop the batch.
|
||||
if (!rels.empty()) try_add_constraints(rels);
|
||||
|
||||
resolve_live();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user