fix(scene): revert wrap-aware arc span — restore legacy CW direction

af1e655's wrap-aware `arc_signed_span` was correct per a strict reading
of the DXF "is_counterclockwise" spec but caused CW boundary arcs on
real files to sweep the long way around the circle, flipping arc
direction and filling huge unintended regions.

Restore the legacy `(TAU - sa, TAU - ea)` flip semantics. The adaptive
`arc_segments` count fix from the same commit is kept — the visible
faceting issue on large-radius arcs was orthogonal to direction and the
revert affects only the span computation.

The wrap-through-2π edge case the previous commit tried to address is
back to being a known limitation. A proper fix needs to know how each
upstream writer (AutoCAD, BricsCAD, ODA, etc.) actually encodes wrap
arcs and isn't possible without sample files exercising the case.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-16 07:26:09 +03:00
1 parent af1e655253
commit cb7e6e3cc3
1 file changed
+11 -18
+11 -18
View File
@@ -30,28 +30,21 @@ use crate::scene::wire_model::{SnapHint, TangentGeom, WireModel};
// ── Arc tessellation helpers ─────────────────────────────────────────────
/// Convert arc `(start, end, ccw)` into a `(start, signed_span)` that
/// (a) sweeps in the correct direction and (b) wraps through 2π when the
/// short path crosses the seam. The legacy `(TAU - sa, TAU - ea)` flip
/// was direction-correct for non-wrap arcs but silently rendered the
/// long-way-around when the short path crossed 0 / 2π.
/// Convert hatch-boundary arc `(start, end, ccw)` into a
/// `(start, signed_span)` ready for the sampling loop. Matches the
/// legacy `(TAU - sa, TAU - ea)` flip used here for years — direction
/// semantics are preserved on real files. (Wrap-through-2π is a known
/// edge case in that convention; do not "fix" it without a wider audit
/// of how upstream writers emit CW boundary arcs.)
pub(super) fn arc_signed_span(start: f64, end: f64, ccw: bool) -> (f64, f64) {
const TAU: f64 = std::f64::consts::TAU;
let mut span = end - start;
if ccw {
if span <= 0.0 {
span += TAU;
}
} else if span >= 0.0 {
span -= TAU;
}
(start, span)
let (sa, ea) = if ccw { (start, end) } else { (TAU - start, TAU - end) };
(sa, ea - sa)
}
/// Segment count for an arc targeting ~0.1% chord-height error vs.
/// radius. Floors at 8 so very small arcs don't degenerate into
/// triangles; rough cap of 256 prevents pathological huge-radius +
/// near-full-circle inputs from exploding the boundary vertex count.
/// Segment count for an arc targeting ~0.1% chord-height error.
/// Floors at 8 so very small arcs don't degenerate into triangles; cap
/// of 256 keeps pathological full-circle inputs bounded.
pub(super) fn arc_segments(span_abs: f64) -> u32 {
if span_abs < 1e-9 {
return 1;