mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 01:40:57 +00:00
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:
co-authored by
Claude Opus 5
parent
510e63dff2
commit
cbbd24dcb4
@@ -1072,6 +1072,16 @@ bool off_join(SketchEntity& a, SketchEntity& b)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normalise an entity that was offset while traversed REVERSED to head-to-tail traversal order:
|
||||
// swap its stored ends so p0 is the traversal start and p1 the traversal end. An arc must swap
|
||||
// its stored sweep too, because "traversed the other way" reverses the stored sweep direction.
|
||||
void off_reverse(SketchEntity& e)
|
||||
{
|
||||
std::swap(e.p0, e.p1);
|
||||
if (e.type == SketchEntity::Type::Arc)
|
||||
std::swap(e.start_angle, e.end_angle);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<SketchEntity> SketchEngine::offset_entities(
|
||||
@@ -1087,60 +1097,97 @@ std::vector<SketchEntity> SketchEngine::offset_entities(
|
||||
if (off_one(src[i], d, o)) out.push_back(o);
|
||||
}
|
||||
|
||||
// Chain the open curves by shared endpoints. Greedy walk: start from an entity nobody
|
||||
// precedes (an open chain's head), else from whatever is left (a closed loop).
|
||||
std::vector<bool> used(open_idx.size(), false);
|
||||
|
||||
// Stored endpoints of the k-th open entity.
|
||||
auto ends = [&](int k, Vec2d& p0, Vec2d& p1) { p0 = src[open_idx[k]].p0; p1 = src[open_idx[k]].p1; };
|
||||
|
||||
auto has_predecessor = [&](int k) {
|
||||
Vec2d p0, p1; ends(k, p0, p1);
|
||||
// An endpoint shared by no OTHER unused open curve is a FREE end: the loose end of an open
|
||||
// chain rather than a seam. (`used` matters — entities already pulled into a chain must not
|
||||
// count, otherwise the far end of the chain we just walked would look shared.)
|
||||
auto is_free = [&](int k, const Vec2d& pt) {
|
||||
for (size_t j = 0; j < open_idx.size(); ++j) {
|
||||
if (int(j) == k || used[j]) continue;
|
||||
Vec2d q0, q1; ends(int(j), q0, q1);
|
||||
if (off_same(q1, p0)) return true;
|
||||
if (off_same(pt, q0) || off_same(pt, q1)) return false;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
for (size_t pass = 0; pass < 2; ++pass) {
|
||||
for (size_t s = 0; s < open_idx.size(); ++s) {
|
||||
if (used[s]) continue;
|
||||
// Pass 0 seeds only open-chain heads, so an open chain is never entered mid-way
|
||||
// (which would split it in two and lose a seam).
|
||||
if (pass == 0 && has_predecessor(int(s))) continue;
|
||||
struct Chain {
|
||||
std::vector<std::pair<int, bool>> items; // (entity index, reversed)
|
||||
Vec2d start, end; // traversal start/end points
|
||||
};
|
||||
|
||||
std::vector<int> chain{ int(s) };
|
||||
used[s] = true;
|
||||
for (;;) {
|
||||
Vec2d p0, p1; ends(chain.back(), p0, p1);
|
||||
int nxt = -1;
|
||||
for (size_t j = 0; j < open_idx.size(); ++j) {
|
||||
if (used[j]) continue;
|
||||
Vec2d q0, q1; ends(int(j), q0, q1);
|
||||
if (off_same(p1, q0)) { nxt = int(j); break; }
|
||||
}
|
||||
if (nxt < 0) break;
|
||||
used[nxt] = true;
|
||||
chain.push_back(nxt);
|
||||
// Walk one chain from the unused seed `s`, traversing AWAY from its free end. `reversed`
|
||||
// means the traversal enters at the entity's p1 and leaves at its p0, i.e. the entity is
|
||||
// travelled opposite to its STORED direction. An entity whose p1 is free (but p0 is not)
|
||||
// is the head of an open chain and must start reversed; an isolated entity or a closed
|
||||
// loop starts forward.
|
||||
auto walk = [&](int s) -> Chain {
|
||||
Chain c;
|
||||
Vec2d s0, s1; ends(s, s0, s1);
|
||||
const bool rev = !is_free(s, s0) && is_free(s, s1);
|
||||
c.items.emplace_back(s, rev);
|
||||
used[s] = true;
|
||||
c.start = rev ? s1 : s0;
|
||||
c.end = rev ? s0 : s1;
|
||||
for (;;) {
|
||||
int nxt = -1;
|
||||
bool nrev = false;
|
||||
for (size_t j = 0; j < open_idx.size(); ++j) {
|
||||
if (used[j]) continue;
|
||||
Vec2d q0, q1; ends(int(j), q0, q1);
|
||||
if (off_same(c.end, q0)) { nxt = int(j); nrev = false; break; }
|
||||
if (off_same(c.end, q1)) { nxt = int(j); nrev = true; break; }
|
||||
}
|
||||
|
||||
Vec2d h0, h1, t0, t1;
|
||||
ends(chain.front(), h0, h1);
|
||||
ends(chain.back(), t0, t1);
|
||||
const bool closed = chain.size() > 2 && off_same(t1, h0);
|
||||
|
||||
std::vector<SketchEntity> off;
|
||||
for (int k : chain) {
|
||||
SketchEntity o;
|
||||
if (off_one(src[open_idx[k]], d, o)) off.push_back(o);
|
||||
}
|
||||
if (off.empty()) continue;
|
||||
|
||||
for (size_t i = 0; i + 1 < off.size(); ++i) off_join(off[i], off[i + 1]);
|
||||
if (closed && off.size() > 1) off_join(off.back(), off.front());
|
||||
|
||||
for (auto& o : off) out.push_back(o);
|
||||
if (nxt < 0) break;
|
||||
c.items.emplace_back(nxt, nrev);
|
||||
used[nxt] = true;
|
||||
Vec2d q0, q1; ends(nxt, q0, q1);
|
||||
c.end = nrev ? q0 : q1;
|
||||
}
|
||||
return c;
|
||||
};
|
||||
|
||||
// Offset one chain as traversed: every entity is offset with an EFFECTIVE distance that
|
||||
// already accounts for how it was walked, then reversed entities have their stored ends
|
||||
// swapped so the emitted chain is head-to-tail in traversal order (which is what keeps the
|
||||
// seam repair below — and any later offset/mirror — well-oriented). A chain whose final
|
||||
// traversal end coincides with its first traversal start is CLOSED.
|
||||
auto emit = [&](const Chain& c) {
|
||||
const bool closed = c.items.size() > 1 && off_same(c.end, c.start);
|
||||
std::vector<SketchEntity> off;
|
||||
off.reserve(c.items.size());
|
||||
for (const auto& it : c.items) {
|
||||
// A reversed entity offsets with -d rather than +d:
|
||||
// * a line walked backwards has its left-hand side on the other side, so -d;
|
||||
// * an arc walked backwards has its sweep sign effectively flipped, which is exactly
|
||||
// the `sgn` term off_one reads, so -d again.
|
||||
const double ed = it.second ? -d : d;
|
||||
SketchEntity o;
|
||||
if (!off_one(src[open_idx[it.first]], ed, o)) continue;
|
||||
if (it.second) off_reverse(o);
|
||||
off.push_back(o);
|
||||
}
|
||||
if (off.empty()) return;
|
||||
for (size_t i = 0; i + 1 < off.size(); ++i) off_join(off[i], off[i + 1]);
|
||||
if (closed && off.size() > 1) off_join(off.back(), off.front());
|
||||
for (auto& o : off) out.push_back(o);
|
||||
};
|
||||
|
||||
// Pass 1: open chains, seeded at a free end so they are never entered mid-way (which would
|
||||
// split one open chain in two and lose a seam).
|
||||
for (size_t s = 0; s < open_idx.size(); ++s) {
|
||||
if (used[s]) continue;
|
||||
Vec2d s0, s1; ends(int(s), s0, s1);
|
||||
if (!is_free(int(s), s0) && !is_free(int(s), s1)) continue;
|
||||
emit(walk(int(s)));
|
||||
}
|
||||
// Pass 2: what is left has no free end and is a CLOSED loop; start anywhere, forward.
|
||||
for (size_t s = 0; s < open_idx.size(); ++s) {
|
||||
if (used[s]) continue;
|
||||
emit(walk(int(s)));
|
||||
}
|
||||
|
||||
return out;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user