From a1f2a2687a2d26c1479e9689469ec550ab0c2025 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Mon, 31 Aug 2026 01:09:06 +0200 Subject: [PATCH] Equal radius and Collinear, and one Equal button that knows what it picked Port of snaporca 9ec6405e2d. Parity OK: 17 files identical, 8 diverging at their expected counts (DesignPanel.cpp 32, test_slvs_constraints.cpp 3). Measured on the CAD-1000-hours corpus: 51.5% of observed CAD time is 2D sketch work, and dimensioning/constraining alone is 31.9% -- the largest single class. Two constraints every industrial sketcher has were missing here. EqualRadius fixes a dead end rather than adding a feature. Picking two circles and pressing Equal emitted EqualLength, which maps to SLVS_C_EQUAL_LENGTH_LINES and constrains nothing on a curve: a silent no-op with no error. Equal is now one button with two meanings, as in Onshape and SolidWorks. Collinear emits PARALLEL plus PT_LINE_DISTANCE=0 rather than PT_ON_LINE, whose internal valP param this libslvs port leaves at 0, drifting an already-collinear pair. Both types are appended at the END of SketchConstraintType: cereal serializes it positionally, so inserting elsewhere reinterprets every saved recipe. VERIFICATION LIMIT, stated rather than implied: this fork's kernel suite could NOT be run. scripts/CAD/run-kernel-tests.sh fails at CMake configure time on find_package(assimp), before any source compiles -- a pre-existing deps gap (snaporca-w80c), not this change. The shared sources are byte-identical to snaporca's, where the full gate passed: kernel 2588/195 and ALL LADDERS HELD across all seven rungs. Also fixes two defects in this fork's scripts/CAD/run-all-checks.sh: - `cd $(dirname $0)/..` landed in scripts/ instead of the repo root, so every rung looked for itself under scripts/scripts/. Broken since the script moved into scripts/CAD/; the three sibling scripts were fixed then and this was missed, so the gate has not run since. - C defaulted to snaporca-gui, the OTHER fork's rig container, so this fork's gate would drive snaporca's app and report green about the wrong binary. run-kernel-tests.sh:31 documents the identical defect being fixed once already for the build volume; this is the third instance. --- resources/images/design_c_collinear.svg | 1 + resources/images/design_c_equal_radius.svg | 1 + scripts/CAD/run-all-checks.sh | 11 +++- src/libslic3r/CAD/SketchEngine.hpp | 6 ++- src/libslic3r/CAD/SketchSolver.cpp | 17 ++++++ src/slic3r/GUI/CAD/DesignPanel.cpp | 32 +++++++++-- tests/libslic3r/test_slvs_constraints.cpp | 63 ++++++++++++++++++++++ 7 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 resources/images/design_c_collinear.svg create mode 100644 resources/images/design_c_equal_radius.svg diff --git a/resources/images/design_c_collinear.svg b/resources/images/design_c_collinear.svg new file mode 100644 index 0000000000..28b8b6a8cd --- /dev/null +++ b/resources/images/design_c_collinear.svg @@ -0,0 +1 @@ + diff --git a/resources/images/design_c_equal_radius.svg b/resources/images/design_c_equal_radius.svg new file mode 100644 index 0000000000..4d7ccf2c5c --- /dev/null +++ b/resources/images/design_c_equal_radius.svg @@ -0,0 +1 @@ + diff --git a/scripts/CAD/run-all-checks.sh b/scripts/CAD/run-all-checks.sh index 1cb56b8a3c..ee9cdf4281 100755 --- a/scripts/CAD/run-all-checks.sh +++ b/scripts/CAD/run-all-checks.sh @@ -13,9 +13,16 @@ # The rig container is expected to be up with the app running and SNAPORCA_MCP set; bring it up # with scripts/CAD/start-headless-gui.sh inside it. The corpus lives at /corpus in that container. set -uo pipefail -cd "$(dirname "$0")/.." || exit 1 +# ../.. -- this script lives in scripts/CAD/, so one level up is scripts/, not the repo +# root. It was scripts/ladder-all.sh when it was written; the move fixed the three sibling +# scripts and missed this one, which left every rung looking for its own path under +# scripts/scripts/ and reporting instant failures that were all the same typo. +cd "$(dirname "${BASH_SOURCE[0]}")/../.." || exit 1 -C="${C:-snaporca-gui}" +# orcacad-gui, NOT snaporca-gui: that is the other fork's rig, and defaulting to it makes +# this gate verify the wrong fork's binary while reporting green. run-kernel-tests.sh +# carries the same warning about the build volume, where the defect was found first. +C="${C:-orcacad-gui}" CORPUS="${CORPUS:-/corpus}" STEP="${STEP:-20}" [ -n "${FULL:-}" ] && STEP=1 diff --git a/src/libslic3r/CAD/SketchEngine.hpp b/src/libslic3r/CAD/SketchEngine.hpp index 1f5d3851fa..571a5e0428 100644 --- a/src/libslic3r/CAD/SketchEngine.hpp +++ b/src/libslic3r/CAD/SketchEngine.hpp @@ -84,7 +84,11 @@ enum class SketchConstraintType { Tangent, Midpoint, Symmetric, Angle, Radius, Diameter, PointOnLine, // a point lies on a line (or at signed perpendicular distance `value`) - PointOnObject // a point lies on an entity edge (line -> PT_ON_LINE, circle -> PT_ON_CIRCLE) + PointOnObject, // a point lies on an entity edge (line -> PT_ON_LINE, circle -> PT_ON_CIRCLE) + // 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 }; // 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 94302c130b..248ff95beb 100644 --- a/src/libslic3r/CAD/SketchSolver.cpp +++ b/src/libslic3r/CAD/SketchSolver.cpp @@ -173,6 +173,9 @@ static SketchSolveResult solve_system(std::vector& entities, ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb) && primOf(c.ec); break; case CT::PointOnLine: case CT::PointOnObject: ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break; + case CT::EqualRadius: + case CT::Collinear: + ref_ok = primOf(c.ea) && primOf(c.eb); break; } if (!ref_ok) continue; switch (c.type) { @@ -280,6 +283,20 @@ static SketchSolveResult solve_system(std::vector& entities, else b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0); break; + case CT::EqualRadius: + b.C(SLVS_C_EQUAL_RADIUS, 0, 0, 0, primOf(c.ea), primOf(c.eb)); + break; + case CT::Collinear: + // libslvs has no collinear code. Two lines are collinear iff they are parallel + // AND a point of one lies on the other's infinite line — emit both. + b.C(SLVS_C_PARALLEL, 0, 0, 0, primOf(c.ea), primOf(c.eb)); + // Point-on-infinite-line via PT_LINE_DISTANCE=0 rather than PT_ON_LINE: the + // latter creates an internal `valP` param that this port's Slvs_Solve leaves at + // 0 in the working set (ModifyToSatisfy only updates SK.param), so an already + // collinear pair drifts. PT_LINE_DISTANCE=0 is the same condition with no extra + // parameter, so an already-satisfied solve is a clean no-op. + b.C(SLVS_C_PT_LINE_DISTANCE, 0, ptOf(c.eb, Role::P0), 0, primOf(c.ea), 0); + break; } } diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index e0a85d0997..003c6bc311 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -1520,6 +1520,8 @@ DesignPanel::DesignPanel(wxWindow* parent) cbtn("design_c_perpendicular", _L("Perpendicular"), SketchConstraintType::Perpendicular); cbtn("design_c_coincident", _L("Coincident"), SketchConstraintType::Coincident); cbtn("design_c_equal", _L("Equal length"), SketchConstraintType::EqualLength); + cbtn("design_c_equal_radius", _L("Equal radius"), SketchConstraintType::EqualRadius); + cbtn("design_c_collinear", _L("Collinear"), SketchConstraintType::Collinear); cbtn("design_c_concentric", _L("Concentric"), SketchConstraintType::Concentric); cbtn("design_c_tangent", _L("Tangent"), SketchConstraintType::Tangent); cbtn("design_c_midpoint", _L("Midpoint"), SketchConstraintType::Midpoint); @@ -7717,18 +7719,27 @@ void DesignPanel::apply_entity_constraint(SketchConstraintType type) }; CadFeature& feat = m_doc.features[m_constrain_feat]; + + auto is_round = [](const SketchEntity& e) { + return e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Arc; }; + + // One Equal button, two meanings: lines get equal length, curves equal radius. + if (type == T::EqualLength && e0 >= 0 && e1 >= 0 && + e0 < int(feat.entities.size()) && e1 < int(feat.entities.size()) && + is_round(feat.entities[e0]) && is_round(feat.entities[e1])) + type = T::EqualRadius; + const bool needs_two = (type == T::Parallel || type == T::Perpendicular || type == T::EqualLength || type == T::Coincident || type == T::Concentric || type == T::Tangent || type == T::Angle || type == T::Midpoint || - type == T::Symmetric); + type == T::Symmetric || type == T::EqualRadius || + type == T::Collinear); 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")); return; } - auto is_round = [](const SketchEntity& e) { - return e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Arc; }; SketchEntityConstraintDef def; def.type = type; @@ -7832,6 +7843,21 @@ void DesignPanel::apply_entity_constraint(SketchConstraintType type) commit_entity_constraints(defs); return; // multi-def commit done here } + case T::EqualRadius: { + if (!is_round(feat.entities[e0]) || !is_round(feat.entities[e1])) { + fail(_L("Equal radius needs two circles or arcs")); return; + } + def.ea = e0; def.eb = e1; + break; + } + case T::Collinear: { + using ET = SketchEntity::Type; + if (feat.entities[e0].type != ET::Line || feat.entities[e1].type != ET::Line) { + fail(_L("Collinear needs two lines")); return; + } + def.ea = e0; def.eb = e1; + break; + } case T::Fix: { // Anchor the picked entity's reference point to its current coordinate (the // kernel pins it to a fixed reference). A single point — not both endpoints — diff --git a/tests/libslic3r/test_slvs_constraints.cpp b/tests/libslic3r/test_slvs_constraints.cpp index 600ca9fe2a..9bbadd614d 100644 --- a/tests/libslic3r/test_slvs_constraints.cpp +++ b/tests/libslic3r/test_slvs_constraints.cpp @@ -159,3 +159,66 @@ TEST_CASE("slvs: a sketch past the solver's unknown limit still solves", "[slvs] auto res3 = sketch_solve(ents, cons); CHECK_FALSE(res3.ok); } + +TEST_CASE("slvs: equal radius drives two circles to one radius", "[slvs][CadDocument]") +{ + std::vector ents = { circle({0, 0}, 5.0), circle({10, 0}, 12.0) }; + std::vector cons = { + con(CT::EqualRadius, 0, R::P0, 1, R::P0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(ents[0].radius == Approx(ents[1].radius).margin(1e-9)); + CHECK(ents[0].radius > 1e-6); // equal-at-zero would satisfy the line above trivially +} + +TEST_CASE("slvs: equal radius plus a radius dimension pins both", "[slvs][CadDocument]") +{ + std::vector ents = { circle({0, 0}, 5.0), circle({10, 0}, 12.0) }; + std::vector cons = { + con(CT::EqualRadius, 0, R::P0, 1, R::P0), + con(CT::Radius, 0, R::P0, -1, R::P0, 8.0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + CHECK(ents[0].radius == Approx(8.0).margin(1e-9)); + CHECK(ents[1].radius == Approx(8.0).margin(1e-9)); +} + +TEST_CASE("slvs: collinear makes two offset lines share one line", "[slvs][CadDocument]") +{ + std::vector ents = { line({0, 0}, {10, 0}), line({0, 4}, {10, 4}) }; + std::vector cons = { + con(CT::Collinear, 0, R::P0, 1, R::P0), + }; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + const Vec2d& a0 = ents[0].p0; + const Vec2d ad = ents[0].p1 - ents[0].p0; + for (int k = 0; k <= 1; ++k) { + const Vec2d& pk = (k == 0) ? ents[1].p0 : ents[1].p1; + const double cross = ad.x() * (pk.y() - a0.y()) - ad.y() * (pk.x() - a0.x()); + CHECK(cross == Approx(0.0).margin(1e-9)); + } + // A line collapsed to a point is trivially collinear with anything, so the cross + // products above would pass on a degenerate solve. Both lines must survive intact. + CHECK(ad.norm() == Approx(10.0).margin(1e-9)); + CHECK((ents[1].p1 - ents[1].p0).norm() == Approx(10.0).margin(1e-9)); +} + +TEST_CASE("slvs: collinear on already-collinear lines moves nothing", "[slvs][CadDocument]") +{ + std::vector ents = { line({0, 0}, {10, 0}), line({20, 0}, {30, 0}) }; + std::vector cons = { + con(CT::Collinear, 0, R::P0, 1, R::P0), + }; + std::vector before = ents; + auto res = sketch_solve(ents, cons); + REQUIRE(res.ok); + for (size_t i = 0; i < ents.size(); ++i) { // already satisfied: nothing may move + CHECK(ents[i].p0.x() == Approx(before[i].p0.x()).margin(1e-9)); + CHECK(ents[i].p0.y() == Approx(before[i].p0.y()).margin(1e-9)); + CHECK(ents[i].p1.x() == Approx(before[i].p1.x()).margin(1e-9)); + CHECK(ents[i].p1.y() == Approx(before[i].p1.y()).margin(1e-9)); + } +}