forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
412fb857ba
commit
af1e655253
2 files changed
+81
-58
No files matched your search
+28
-28
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
+53
-30
@@ -28,7 +28,38 @@ use crate::scene::truck_tess::{
|
||||
};
|
||||
use crate::scene::wire_model::{SnapHint, TangentGeom, WireModel};
|
||||
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
// ── 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π.
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub(super) fn arc_segments(span_abs: f64) -> u32 {
|
||||
if span_abs < 1e-9 {
|
||||
return 1;
|
||||
}
|
||||
// 2 * acos(1 - 0.001) ≈ 0.0894 rad ≈ 5.12°
|
||||
const MAX_STEP: f64 = 0.0894;
|
||||
((span_abs / MAX_STEP).ceil() as u32).clamp(8, 256)
|
||||
}
|
||||
|
||||
// ── Colour helper ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1189,20 +1220,17 @@ fn legacy_geometry(entity: &EntityType, world_offset: [f64; 3]) -> Geometry {
|
||||
key_verts.push(p1);
|
||||
}
|
||||
acadrust::entities::BoundaryEdge::CircularArc(arc) => {
|
||||
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) =
|
||||
arc_signed_span(arc.start_angle, arc.end_angle, arc.counter_clockwise);
|
||||
let segs = arc_segments(span.abs());
|
||||
if !pts.is_empty() {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
for i in 0..=segs {
|
||||
let t = sa + span * (i as f64 / segs as f64);
|
||||
let p = to_wcs(
|
||||
arc.center.x + arc.radius * t.cos() as f64,
|
||||
arc.center.y + arc.radius * t.sin() as f64,
|
||||
arc.center.x + arc.radius * t.cos(),
|
||||
arc.center.y + arc.radius * t.sin(),
|
||||
);
|
||||
pts.push(p);
|
||||
if i == 0 || i == segs {
|
||||
@@ -1215,34 +1243,29 @@ fn legacy_geometry(entity: &EntityType, world_offset: [f64; 3]) -> Geometry {
|
||||
));
|
||||
}
|
||||
acadrust::entities::BoundaryEdge::EllipticArc(ell) => {
|
||||
let r_maj = ((ell.major_axis_endpoint.x * ell.major_axis_endpoint.x
|
||||
let r_maj = (ell.major_axis_endpoint.x * ell.major_axis_endpoint.x
|
||||
+ ell.major_axis_endpoint.y * ell.major_axis_endpoint.y)
|
||||
.sqrt()) as f32;
|
||||
let r_min = r_maj * ell.minor_axis_ratio as f32;
|
||||
let rot = (ell.major_axis_endpoint.y as f32)
|
||||
.atan2(ell.major_axis_endpoint.x as f32);
|
||||
const TAU: f32 = std::f32::consts::TAU;
|
||||
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;
|
||||
.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) =
|
||||
arc_signed_span(ell.start_angle, ell.end_angle, ell.counter_clockwise);
|
||||
let segs = arc_segments(span.abs());
|
||||
if !pts.is_empty() {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
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();
|
||||
let ocs_x = ell.center.x + (lx * rot.cos() - ly * rot.sin()) as f64;
|
||||
let ocs_y = ell.center.y + (lx * rot.sin() + ly * rot.cos()) as f64;
|
||||
let p = to_wcs(ocs_x, ocs_y);
|
||||
let p = to_wcs(
|
||||
ell.center.x + lx * cr - ly * sr,
|
||||
ell.center.y + lx * sr + ly * cr,
|
||||
);
|
||||
pts.push(p);
|
||||
if i == 0 || i == segs {
|
||||
key_verts.push(p);
|
||||
|
||||
Reference in new issue
Block a user