Port the exact loop area, the offset traversal fix, and the 2D sketch ladder

Carries snaporca 572f794c84, d0f9a0052a, 9f7e4e3627 and 3974f8a170. Parity holds: 17 files
identical, 8 diverging by their expected counts.

EXACT AREA. A loop's area is now integrated entity by entity in traversal order — Green's
theorem — instead of being shoelaced over the render polyline, which faceted every arc into 24
chords and lost 2.02 mm2 on a 3706.86 mm2 stadium. 0.054%, invisible on screen, and wrong in a
number reported as "the area".

OFFSET FOLLOWS THE TRAVERSAL. Offsetting a mirrored profile put one half on the wrong side and
split the loop in two, because the chainer only followed p1->p0 links and each entity's offset
side was taken from its stored direction. Chains are now orientation-aware, seeded at a free end,
offset by `reversed ? -d : d`, and normalised head-to-tail on the way out — so offset is correct
for any input ordering and its own output cannot reintroduce the problem.

Both are the same underlying lesson, which has now cost three separate defects: an entity's
STORED direction is not its direction of TRAVEL around the loop.

THE LADDER. scripts/sketch-ladder.py is a graded suite of 2D sketches judged the way a person
judges them — VERTEX, LENGTH, ARC, TANGENT, SYMMETRY, CLOSED — with area only as a cross-check,
because area is derived and nobody can confirm it by eye. Eight rungs from a rectangle up to
MPD5 from the StudyCadCam corpus, a dia 27 x 95 pin reproduced as its revolve half-profile with
the R5 fillet tangency solved exactly. Entirely 2D: no extrude or any solid feature.

Kernel here: all tests passed, 2681 assertions in 231 test cases, including the new
"profile: a mirrored half offsets as one loop, not two". GUI target builds and links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-08-22 14:26:46 +02:00
co-authored by Claude Opus 5
parent 510e63dff2
commit cbbd24dcb4
4 changed files with 534 additions and 49 deletions
+70 -7
View File
@@ -27,6 +27,11 @@
namespace Slic3r {
namespace GUI {
// How many chords an arc is drawn with. loop_report corrects each arc's area back to the true
// curve using this exact number, so the two MUST agree — changing it here without updating the
// correction silently biases every reported area.
static constexpr int kArcFacets = 24;
// Positioning helpers (defined lower down, used by the dimension methods above them).
static bool entity_ref_point(const SketchEntity& e, Vec2d& out);
static void translate_entity(SketchEntity& e, const Vec2d& d);
@@ -2814,7 +2819,7 @@ std::vector<Vec2d> DesignSketchTool::entity_polyline(const SketchEntity& e, bool
closed = true;
return circle_polygon(e.center, e.radius);
case SketchEntity::Type::Arc: {
const int n = 24;
const int n = kArcFacets;
std::vector<Vec2d> pts; pts.reserve(n + 1);
for (int i = 0; i <= n; ++i) {
const double a = e.start_angle + (e.end_angle - e.start_angle) * double(i) / double(n);
@@ -8883,12 +8888,70 @@ DesignSketchTool::LoopReport DesignSketchTool::loop_report() const
? M_PI * c.radius * c.radius
: M_PI * c.radius * c.rminor;
} else {
double a = 0.0;
for (size_t i = 0; i + 1 < r.poly.size(); ++i)
a += r.poly[i].x() * r.poly[i + 1].y() - r.poly[i + 1].x() * r.poly[i].y();
if (r.poly.size() > 2)
a += r.poly.back().x() * r.poly.front().y() - r.poly.front().x() * r.poly.back().y();
li.area = 0.5 * a;
// EXACT area by Green's theorem over the chain, entity by entity, with no faceting
// anywhere. The obvious alternative — shoelace over the render polyline — is short by
// the slivers between each arc and its chords: 2.02 mm2 on a 3706.86 mm2 stadium,
// 0.054%, invisible on screen and simply wrong in a number reported as "the area".
// Correcting the shoelace afterwards does NOT work: a mirrored arc stores a negated
// sweep, so two corrections that should add cancel instead. Integrating each entity
// in TRAVERSAL order sidesteps the sign question entirely.
// line A->B : x0*y1 - x1*y0
// arc a0->a1: Cx*r*(sin a1 - sin a0) - Cy*r*(cos a1 - cos a0) + r^2*(a1 - a0)
// Both are the integrand of the contour integral, so area2 accumulates 2*area and is
// halved once at the end. (Doubling the arc term instead reads 5913.72 on the stadium
// — exactly one arc's contribution too much, which is how the slip was caught.)
auto ent_ends = [&](int ei, Vec2d& a, Vec2d& b) {
a = m_entities[ei].p0; b = m_entities[ei].p1;
};
const double eps = 1e-6;
double area2 = 0.0;
bool exact = true;
Vec2d cur(0, 0);
for (size_t k = 0; k < r.ents.size(); ++k) {
const int ei = r.ents[k];
if (ei < 0 || ei >= int(m_entities.size())) { exact = false; break; }
const SketchEntity& e = m_entities[ei];
if (e.type != SketchEntity::Type::Line && e.type != SketchEntity::Type::Arc) {
exact = false; break; // spline / ellipse arc: fall back below
}
Vec2d A, B; ent_ends(ei, A, B);
bool rev = false;
if (k == 0) {
// Orient the first entity by whichever of its ends the SECOND one touches:
// that shared point is where this entity must finish.
if (r.ents.size() > 1) {
Vec2d C, D; ent_ends(r.ents[1], C, D);
if ((A - C).norm() < eps || (A - D).norm() < eps) rev = true;
}
} else {
if ((B - cur).norm() < eps) rev = true;
else if ((A - cur).norm() >= eps) { exact = false; break; }
}
const Vec2d P = rev ? B : A;
const Vec2d Q = rev ? A : B;
if (e.type == SketchEntity::Type::Line) {
area2 += P.x() * Q.y() - Q.x() * P.y();
} else {
const double a0 = rev ? e.end_angle : e.start_angle;
const double a1 = rev ? e.start_angle : e.end_angle;
area2 += e.center.x() * e.radius * (std::sin(a1) - std::sin(a0))
- e.center.y() * e.radius * (std::cos(a1) - std::cos(a0))
+ e.radius * e.radius * (a1 - a0);
}
cur = Q;
}
if (exact) {
li.area = 0.5 * area2;
} else {
// Splines and elliptical arcs have no closed form here; the render polyline is
// the honest best estimate, and it is flagged as such by being the fallback.
double a = 0.0;
for (size_t i = 0; i + 1 < r.poly.size(); ++i)
a += r.poly[i].x() * r.poly[i + 1].y() - r.poly[i + 1].x() * r.poly[i].y();
if (r.poly.size() > 2)
a += r.poly.back().x() * r.poly.front().y() - r.poly.front().x() * r.poly.back().y();
li.area = 0.5 * a;
}
}
out.loops.push_back(std::move(li));
}