fix(scene): smooth + correct arc edges in hatch boundaries

Hatch boundary CircularArc / EllipticArc tessellation had two bugs that
showed up as polygonal-looking arc edges and, on certain arcs, edges
that drew the long way around the seam:

1. Fixed 32 segments per full circle gave ~5 mm chord-height error at
   r = 1 m, visibly polygonal at typical hatch radii. Replace with an
   adaptive count targeting ~0.1% chord error (≈ 5° max step), floor 8,
   cap 256.
2. The `(TAU - sa, TAU - ea)` flip for CW arcs was direction-correct
   for non-wrap inputs but silently went the long way around whenever
   the short path crossed the 0 / 2π seam (e.g. a CCW arc from 3π/2 to
   π/2 should sweep east through 0; the old code swept west through π).
   Replace with `arc_signed_span` which derives a signed span that wraps
   through 2π in the correct direction.

Shared helpers (`arc_signed_span`, `arc_segments`) live in
`scene/tessellate.rs` and feed both the hatch fill boundary builder
(`scene/mod.rs::hatch_model_from_dxf`) and the legacy wire fallback
(`scene/tessellate.rs::legacy_geometry`). Same fix applied to the
EllipticArc paths in both. Dropped the unused module-level `TAU`
constant the old code carried.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-16 07:20:45 +03:00
commit af1e655253
2 changed files with 81 additions and 58 deletions

View file

@ -2515,43 +2515,43 @@ impl Scene {
boundary.push(to_xy(line.end.x, line.end.y));
}
BoundaryEdge::CircularArc(arc) => {
const TAU: f64 = std::f64::consts::TAU;
let cx = arc.center.x;
let cy = arc.center.y;
let r = arc.radius;
let sa = arc.start_angle;
let ea = arc.end_angle;
let ccw = arc.counter_clockwise;
let (sa, ea) = if ccw { (sa, ea) } else { (TAU - sa, TAU - ea) };
let span = ea - sa;
let segs = ((span.abs() / TAU) * 32.0).ceil().max(4.0) as u32;
let (sa, span) = tessellate::arc_signed_span(
arc.start_angle,
arc.end_angle,
arc.counter_clockwise,
);
let segs = tessellate::arc_segments(span.abs());
for i in 0..=segs {
let t = sa + span * (i as f64 / segs as f64);
boundary.push(to_xy(cx + r * t.cos(), cy + r * t.sin()));
boundary.push(to_xy(
arc.center.x + arc.radius * t.cos(),
arc.center.y + arc.radius * t.sin(),
));
}
}
BoundaryEdge::EllipticArc(ell) => {
const TAU: f32 = std::f32::consts::TAU;
let cx = ell.center.x;
let cy = ell.center.y;
let maj_x = ell.major_axis_endpoint.x as f32;
let maj_y = ell.major_axis_endpoint.y as f32;
let r_maj = (maj_x * maj_x + maj_y * maj_y).sqrt();
let r_min = r_maj * ell.minor_axis_ratio as f32;
let rot = maj_y.atan2(maj_x);
let ccw = ell.counter_clockwise;
let sa = ell.start_angle as f32;
let ea = ell.end_angle as f32;
let (sa, ea) = if ccw { (sa, ea) } else { (TAU - sa, TAU - ea) };
let span = ea - sa;
let segs = ((span.abs() / TAU) * 32.0).ceil().max(4.0) as u32;
let r_maj = (ell.major_axis_endpoint.x * ell.major_axis_endpoint.x
+ ell.major_axis_endpoint.y * ell.major_axis_endpoint.y)
.sqrt();
let r_min = r_maj * ell.minor_axis_ratio;
let rot = ell
.major_axis_endpoint
.y
.atan2(ell.major_axis_endpoint.x);
let (sa, span) = tessellate::arc_signed_span(
ell.start_angle,
ell.end_angle,
ell.counter_clockwise,
);
let segs = tessellate::arc_segments(span.abs());
let (cr, sr) = (rot.cos(), rot.sin());
for i in 0..=segs {
let t = sa + span * (i as f32 / segs as f32);
let t = sa + span * (i as f64 / segs as f64);
let lx = r_maj * t.cos();
let ly = r_min * t.sin();
boundary.push(to_xy(
cx + (lx * rot.cos() - ly * rot.sin()) as f64,
cy + (lx * rot.sin() + ly * rot.cos()) as f64,
ell.center.x + lx * cr - ly * sr,
ell.center.y + lx * sr + ly * cr,
));
}
}