From a63bba2d5587f07dcce83aaea233796f89927bb3 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Mon, 31 Aug 2026 01:46:09 +0200 Subject: [PATCH] Horizontal and vertical distance dimensions Port of snaporca 9fa304c77a. Parity OK: 17 files identical, 8 diverging at their expected counts. The everyday dimension in SolidWorks and Onshape, and this kernel had no form of it. Distance constrains the straight-line gap; LockX/LockY pin one point's ABSOLUTE coordinate. Neither relates two points along an axis. DistanceX/DistanceY emit SLVS_C_PROJ_PT_DISTANCE against two unit direction lines built in the solver's G_FIXED group, so they add no degrees of freedom. THE DIRECTION IS SIGNED, and getting it backwards is silent. libslvs defines a LINE_SEGMENT's direction as point[0] - point[1] (entity.cpp) and PROJ_PT_DISTANCE constrains (pB - pA).dot(dir) (constrainteq.cpp:234), so the reference lines are built head-first to mean +X and +Y. The same signedness was a real defect in the GUI: the inline editor was pre-filled with |delta|, so when the closest endpoint pair ran right-to-left, opening the dimension and accepting the number shown would flip the point to the other side of its anchor. Opening a dimension and accepting its own value must be a no-op. The refs are now ordered so the shown value is positive. On the CAD-1000-hours corpus, dimensioning and constraining is 31.9% of all observed CAD time -- the largest single class, 7.6x feature operations. This is the item in the constraint epic that lands most directly on it. VERIFICATION LIMIT, as with the previous commit: this fork's kernel suite still cannot run (find_package(assimp) fails at configure time, snaporca-w80c). The shared sources are byte-identical to snaporca's, where kernel is 2603 assertions / 200 cases and ALL LADDERS HELD across all seven rungs. --- resources/images/design_c_dist_x.svg | 1 + resources/images/design_c_dist_y.svg | 1 + src/libslic3r/CAD/SketchEngine.hpp | 4 +- src/libslic3r/CAD/SketchSolver.cpp | 20 ++++++ src/slic3r/GUI/CAD/DesignPanel.cpp | 40 +++++++++++- tests/libslic3r/test_slvs_constraints.cpp | 78 +++++++++++++++++++++++ 6 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 resources/images/design_c_dist_x.svg create mode 100644 resources/images/design_c_dist_y.svg diff --git a/resources/images/design_c_dist_x.svg b/resources/images/design_c_dist_x.svg new file mode 100644 index 0000000000..e811a58df6 --- /dev/null +++ b/resources/images/design_c_dist_x.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_dist_y.svg b/resources/images/design_c_dist_y.svg new file mode 100644 index 0000000000..5aefec85ee --- /dev/null +++ b/resources/images/design_c_dist_y.svg @@ -0,0 +1 @@ + diff --git a/src/libslic3r/CAD/SketchEngine.hpp b/src/libslic3r/CAD/SketchEngine.hpp index 571a5e0428..27ed4f21ea 100644 --- a/src/libslic3r/CAD/SketchEngine.hpp +++ b/src/libslic3r/CAD/SketchEngine.hpp @@ -88,7 +88,9 @@ enum class SketchConstraintType { // Append-only: cereal serializes this enum positionally as its underlying int, so // inserting anywhere but the end reinterprets every constraint in every saved recipe. EqualRadius, - Collinear + Collinear, + DistanceX, // |dx| between two points, projected onto the sketch X axis + DistanceY // |dy| between two points, projected onto the sketch Y axis }; // Constraint on a SketchProfile, referencing profile point indices (a,b,c,d). diff --git a/src/libslic3r/CAD/SketchSolver.cpp b/src/libslic3r/CAD/SketchSolver.cpp index 248ff95beb..0b168ecb85 100644 --- a/src/libslic3r/CAD/SketchSolver.cpp +++ b/src/libslic3r/CAD/SketchSolver.cpp @@ -73,6 +73,15 @@ static SketchSolveResult solve_system(std::vector& entities, b.P(G_FIXED, qw), b.P(G_FIXED, qx), b.P(G_FIXED, qy), b.P(G_FIXED, qz))); b.wp = b.E(Slvs_MakeWorkplane(++b.eh, G_FIXED, origin, b.normal)); + // Unit direction references for the axis-projected distance constraints. Both live in + // G_FIXED, so they are held constant and add no DOF to the system. + // libslvs defines a LINE_SEGMENT's direction as point[0] - point[1] (entity.cpp + // VectorGetExprs), so the unit vector's head is listed first to yield +X / +Y. + const Slvs_hEntity dir_x [[maybe_unused]] = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp, + b.pt2d(G_FIXED, 1.0, 0.0), b.pt2d(G_FIXED, 0.0, 0.0))); + const Slvs_hEntity dir_y [[maybe_unused]] = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp, + b.pt2d(G_FIXED, 0.0, 1.0), b.pt2d(G_FIXED, 0.0, 0.0))); + // ---- Entities ------------------------------------------------------------------- std::vector slot(entities.size()); for (size_t i = 0; i < entities.size(); ++i) { @@ -158,6 +167,9 @@ static SketchSolveResult solve_system(std::vector& entities, switch (c.type) { case CT::Coincident: case CT::Horizontal: case CT::Vertical: case CT::Distance: ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break; + case CT::DistanceX: + case CT::DistanceY: + ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break; case CT::Concentric: ref_ok = ptOf(c.ea, Role::Center) && ptOf(c.eb, Role::Center); break; case CT::Fix: case CT::LockX: case CT::LockY: @@ -194,6 +206,14 @@ static SketchSolveResult solve_system(std::vector& entities, case CT::Distance: b.C(SLVS_C_PT_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0); break; + case CT::DistanceX: + // Distance between the two points measured along X only: project the vector + // between them onto the fixed unit X direction. + b.C(SLVS_C_PROJ_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), dir_x, 0); + break; + case CT::DistanceY: + b.C(SLVS_C_PROJ_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), dir_y, 0); + break; case CT::Fix: { const Vec2d p = coordOf(c.ea, c.ra); b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), p.y()), 0, 0); diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index 003c6bc311..07e46ff24d 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -1530,6 +1530,8 @@ DesignPanel::DesignPanel(wxWindow* parent) cbtn("design_c_radius", _L("Radius"), SketchConstraintType::Radius); cbtn("design_c_diameter", _L("Diameter"), SketchConstraintType::Diameter); cbtn("design_c_fix", _L("Fix point (anchor in place)"), SketchConstraintType::Fix); + cbtn("design_c_dist_x", _L("Horizontal distance"), SketchConstraintType::DistanceX); + cbtn("design_c_dist_y", _L("Vertical distance"), SketchConstraintType::DistanceY); // Trim/Extend are now standalone SKETCH scissors (Mode::Trim/Extend) in the sketch // toolbar, NOT Constrain buttons. The other edit ops (Mirror/Offset/Fillet/Chamfer/ // Move/…) are first-class sketch tools too. Done constraining = the action-bar ✓. @@ -7734,7 +7736,8 @@ void DesignPanel::apply_entity_constraint(SketchConstraintType type) type == T::Concentric || type == T::Tangent || type == T::Angle || type == T::Midpoint || type == T::Symmetric || type == T::EqualRadius || - type == T::Collinear); + type == T::Collinear || + type == T::DistanceX || type == T::DistanceY); if (e0 < 0 || e0 >= int(feat.entities.size()) || (needs_two && (e1 < 0 || e1 >= int(feat.entities.size())))) { fail(needs_two ? _L("Pick two entities first") : _L("Pick an entity first")); @@ -7772,6 +7775,41 @@ void DesignPanel::apply_entity_constraint(SketchConstraintType type) def.ea = e0; def.ra = ra; def.eb = e1; def.rb = rb; break; } + case T::DistanceX: + case T::DistanceY: { + // Axis-projected distance between the closest endpoint pair of the two picked + // entities. Typed in-canvas pre-filled with the current projection, committed on + // the typed value (same deferred pattern as Angle). + const SketchEntity& A = feat.entities[e0]; + const SketchEntity& B = feat.entities[e1]; + const std::pair aps[2] = {{R::P0, A.p0}, {R::P1, A.p1}}; + const std::pair bps[2] = {{R::P0, B.p0}, {R::P1, B.p1}}; + R ra = R::P1, rb = R::P0; + Vec2d pa = A.p1, pb = B.p0; + double best = 1e30; + for (const auto& ap : aps) + for (const auto& bp : bps) { + const double d = (ap.second - bp.second).squaredNorm(); + if (d < best) { best = d; ra = ap.first; rb = bp.first; pa = ap.second; pb = bp.second; } + } + // The constraint is SIGNED: PROJ_PT_DISTANCE fixes (pB - pA).dot(axis), not its + // magnitude. Showing |delta| while the current signed delta is negative would mean + // that opening the dimension and simply accepting the number on screen flips the + // point to the other side of its anchor. Opening a dimension and accepting its own + // value must be a no-op, so order the two refs to make the shown value the positive + // one -- which is also how a dimension ought to read. + int a = e0, b = e1; + double delta = (type == T::DistanceX) ? (pb.x() - pa.x()) : (pb.y() - pa.y()); + if (delta < 0.0) { std::swap(a, b); std::swap(ra, rb); delta = -delta; } + const double cur = delta; + const T tt = type; + m_viewport->open_inline_value(cur, [this, a, b, ra, rb, tt](double v) { + SketchEntityConstraintDef d; + d.type = tt; d.ea = a; d.ra = ra; d.eb = b; d.rb = rb; d.value = v; + commit_entity_constraint(d); + }); + return; // deferred: commit runs on the typed value + } case T::Concentric: { // Two circles/arcs: make their centres coincide. if (!is_round(feat.entities[e0]) || !is_round(feat.entities[e1])) { diff --git a/tests/libslic3r/test_slvs_constraints.cpp b/tests/libslic3r/test_slvs_constraints.cpp index 9bbadd614d..95734a81d9 100644 --- a/tests/libslic3r/test_slvs_constraints.cpp +++ b/tests/libslic3r/test_slvs_constraints.cpp @@ -222,3 +222,81 @@ TEST_CASE("slvs: collinear on already-collinear lines moves nothing", "[slvs][Ca CHECK(ents[i].p1.y() == Approx(before[i].p1.y()).margin(1e-9)); } } + +TEST_CASE("slvs: distance-x drives the horizontal gap and leaves Y alone", "[slvs][CadDocument]") +{ + std::vector ents = { line({0, 0}, {3, 7}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::DistanceX, 0, R::P0, 0, R::P1, 10.0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + // SIGNED, not abs. PROJ_PT_DISTANCE constrains (pB - pA).dot(unit(dir)), and a + // LINE_SEGMENT's direction is point[0] - point[1] (slvs entity.cpp), so the reference + // line is built head-first to mean +X. Assert on abs and a flipped reference passes + // while every dimension lands the point on the wrong side of its anchor. + CHECK(ents[0].p1.x() - ents[0].p0.x() == Approx(10.0).margin(1e-9)); + CHECK(ents[0].p1.y() == Approx(7.0).margin(1e-9)); // Y must not be disturbed +} + +TEST_CASE("slvs: distance-y drives the vertical gap and leaves X alone", "[slvs][CadDocument]") +{ + std::vector ents = { line({0, 0}, {3, 7}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::DistanceY, 0, R::P0, 0, R::P1, 10.0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(ents[0].p1.y() - ents[0].p0.y() == Approx(10.0).margin(1e-9)); // signed: see above + CHECK(ents[0].p1.x() == Approx(3.0).margin(1e-9)); // X must not be disturbed +} + +TEST_CASE("slvs: distance-x is not the straight-line distance", "[slvs][CadDocument]") +{ + // B is at straight-line distance 10 from A; DistanceX = 6 is already satisfied, so a + // correct projection leaves B untouched. This is the case that fails if the constraint + // were wired to SLVS_C_PT_PT_DISTANCE, which would drag B onto the radius-6 circle. + std::vector ents = { line({0, 0}, {6, 8}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::DistanceX, 0, R::P0, 0, R::P1, 6.0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(ents[0].p1.x() == Approx(6.0).margin(1e-9)); + CHECK(ents[0].p1.y() == Approx(8.0).margin(1e-9)); +} + +TEST_CASE("slvs: distance-x plus distance-y fully locates a point", "[slvs][CadDocument]") +{ + std::vector ents = { line({0, 0}, {1, 1}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::DistanceX, 0, R::P0, 0, R::P1, 4.0), + con(CT::DistanceY, 0, R::P0, 0, R::P1, 3.0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(ents[0].p1.x() - ents[0].p0.x() == Approx(4.0).margin(1e-9)); // signed: see above + CHECK(ents[0].p1.y() - ents[0].p0.y() == Approx(3.0).margin(1e-9)); +} + +// The property the GUI's ref-ordering exists to preserve: DistanceX is SIGNED, so applying +// the CURRENT projected delta as the target must not move anything. If the refs are ordered +// so the shown value is positive while the actual signed delta is negative, accepting the +// value a dimension opens with teleports the point to the other side of its anchor. +TEST_CASE("slvs: applying a point's own distance-x is a no-op", "[slvs][CadDocument]") +{ + // p1 sits to the LEFT of p0, so the signed delta p1 - p0 is negative. + std::vector ents = { line({0, 0}, {-4, 7}) }; + std::vector cons = { + con(CT::Fix, 0, R::P0, 0, R::P0), + con(CT::DistanceX, 0, R::P0, 0, R::P1, -4.0), // the CURRENT signed delta + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(ents[0].p1.x() == Approx(-4.0).margin(1e-9)); // stayed left, did not flip to +4 + CHECK(ents[0].p1.y() == Approx(7.0).margin(1e-9)); +}