perf(scene): zoom-adaptive arc tessellation for hatch boundary outlines

The shared `arc_segments` helper now takes (radius, span, chord_tol) so
each call site can pick its own tolerance:

* Hatch fill polygon (built at load / on edit, never re-tessellated)
  uses `fill_chord_tol(r) = max(0.001*r, 1µm)` — fixed high quality so
  zoom-in stays sharp without rebuilding the boundary.
* Hatch wire outline (rebuilt every frame inside the render scope) uses
  `wire_chord_tol(r) = min(scene_override, fill_chord_tol(r))` —
  reads the per-frame `truck_tess::set_curve_tol_override` (the Scene
  already sets it to `world_per_pixel × 0.5` for Phase 3.2 wires) so
  far-out boundary arcs collapse to a handful of segments at low zoom
  while staying at least as sharp as the underlying fill at high zoom.

Same arc_segments switch applied to the legacy 16-segments-per-circle
formula in `hatch_model_from_dxf`'s polyline-bulge tessellation, so
fill boundaries with bulged polylines also gain the proper chord-error
target.

Also expose `truck_tess::current_curve_tol` (pub(crate)) and add
`active_curve_tol()` returning Option so the new wire path can
distinguish "scene override active" from "load-time, no zoom info".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-16 07:55:46 +03:00
commit dc80968023
3 changed files with 69 additions and 17 deletions

View file

@ -2492,9 +2492,12 @@ impl Scene {
-std::f32::consts::TAU
};
}
let segs = ((sweep.abs() / std::f32::consts::TAU) * 16.0)
.ceil()
.max(4.0) as u32;
let r64 = r as f64;
let segs = tessellate::arc_segments(
r64,
sweep.abs() as f64,
tessellate::fill_chord_tol(r64),
);
for j in 0..segs {
let a = a0 + sweep * (j as f32 / segs as f32);
boundary.push(to_xy(
@ -2520,7 +2523,11 @@ impl Scene {
arc.end_angle,
arc.counter_clockwise,
);
let segs = tessellate::arc_segments(span.abs());
let segs = tessellate::arc_segments(
arc.radius,
span.abs(),
tessellate::fill_chord_tol(arc.radius),
);
for i in 0..=segs {
let t = sa + span * (i as f64 / segs as f64);
boundary.push(to_xy(
@ -2543,7 +2550,11 @@ impl Scene {
ell.end_angle,
ell.counter_clockwise,
);
let segs = tessellate::arc_segments(span.abs());
let segs = tessellate::arc_segments(
r_maj,
span.abs(),
tessellate::fill_chord_tol(r_maj),
);
let (cr, sr) = (rot.cos(), rot.sin());
for i in 0..=segs {
let t = sa + span * (i as f64 / segs as f64);