From 000f74f951cfab6fb0a2a1b926e4f1bb3a2bb9a0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 2 May 2026 17:15:02 +0300 Subject: [PATCH] feat: render CircularArc and EllipticArc hatch boundary edges Hatch boundaries with arc or ellipse edges were silently dropped from the wire outline, leaving gaps in the hatch contour. CircularArc and EllipticArc boundary edges are now tessellated as polyline segments and their centers are emitted as Center snap hints. Co-Authored-By: Claude Sonnet 4.6 --- src/scene/tessellate.rs | 61 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index 95ca05607..31b0c6c93 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -625,6 +625,7 @@ fn legacy_geometry(entity: &EntityType, world_offset: [f64; 3]) -> Geometry { let elev = (h.elevation - oz) as f32; let mut pts: Vec<[f32; 3]> = Vec::new(); let mut key_verts: Vec<[f32; 3]> = Vec::new(); + let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new(); for path in &h.paths { for edge in &path.edges { match edge { @@ -642,11 +643,69 @@ fn legacy_geometry(entity: &EntityType, world_offset: [f64; 3]) -> Geometry { acadrust::entities::BoundaryEdge::Line(ln) => { let p0 = [(ln.start.x - ox) as f32, (ln.start.y - oy) as f32, elev]; let p1 = [(ln.end.x - ox) as f32, (ln.end.y - oy) as f32, elev]; + if !pts.is_empty() { pts.push([f32::NAN; 3]); } pts.push(p0); pts.push(p1); key_verts.push(p0); key_verts.push(p1); } + acadrust::entities::BoundaryEdge::CircularArc(arc) => { + let cx = (arc.center.x - ox) as f32; + let cy = (arc.center.y - oy) as f32; + let r = arc.radius as f32; + let sa = (arc.start_angle as f32).to_radians(); + let ea = (arc.end_angle as f32).to_radians(); + let span = if ea > sa { + ea - sa + } else { + ea - sa + std::f32::consts::TAU + }; + let segs = ((span / std::f32::consts::TAU) * 32.0) + .ceil() + .max(4.0) as u32; + if !pts.is_empty() { pts.push([f32::NAN; 3]); } + for i in 0..=segs { + let t = sa + span * (i as f32 / segs as f32); + let p = [cx + r * t.cos(), cy + r * t.sin(), elev]; + pts.push(p); + if i == 0 || i == segs { key_verts.push(p); } + } + snap_pts.push((Vec3::new(cx, cy, elev), SnapHint::Center)); + } + acadrust::entities::BoundaryEdge::EllipticArc(ell) => { + let cx = (ell.center.x - ox) as f32; + let cy = (ell.center.y - oy) as f32; + 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); + let sa = ell.start_angle as f32; + let ea = ell.end_angle as f32; + let span = if ea > sa { + ea - sa + } else { + ea - sa + std::f32::consts::TAU + }; + let segs = ((span / std::f32::consts::TAU) * 32.0) + .ceil() + .max(4.0) as u32; + if !pts.is_empty() { pts.push([f32::NAN; 3]); } + for i in 0..=segs { + let t = sa + span * (i as f32 / segs as f32); + let lx = r_maj * t.cos(); + let ly = r_min * t.sin(); + let p = [ + cx + lx * rot.cos() - ly * rot.sin(), + cy + lx * rot.sin() + ly * rot.cos(), + elev, + ]; + pts.push(p); + if i == 0 || i == segs { key_verts.push(p); } + } + snap_pts.push((Vec3::new(cx, cy, elev), SnapHint::Center)); + } _ => {} } } @@ -654,7 +713,7 @@ fn legacy_geometry(entity: &EntityType, world_offset: [f64; 3]) -> Geometry { if pts.is_empty() { pts = vec![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]; } - (pts, vec![], vec![], key_verts) + (pts, snap_pts, vec![], key_verts) } EntityType::Ole2Frame(ole) => { // OLE objects carry a bounding rectangle in model space.