From 983b1ca8f04e2471208e3568ea01573fed555e41 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:03:21 -0400 Subject: [PATCH] Sketch projection: a projected face's curves meet at the face's vertices A face's edges are projected exactly: lines, and arcs read off SMLib's NURBS within the planar-edge tolerance (Body::planar_edges). An SMLib shell's offset corner is a NURBS whose arc came out with its centre 2e-6 mm off and its ends up to 6e-7 mm from the lines they meet, beyond the link's merge tolerance (1e-7): the inner loop of the sensor enclosure's case top projected as sixteen loose curves, the offset of one found no chain to follow, and the Lid lip extrude found no profile l13..a16 (the five acceptance scenarios, action_matrix an_axis_through_a_corner_round_that_a_parameter_moves). Each edge's pieces now end on the edge's vertices where they lie within the edge's and the reading's tolerance of them (the vertex is the point both edges share), and an arc whose ends moved takes the centre nearest its own that is equally far from both, so the sketch keeps both ends on its circle. Body::planar_edges_tolerance names the reading tolerance planar_edges uses. Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/project.rs | 50 ++++++++++++++++++++++++++++++++++++++- crates/kernel/src/body.rs | 9 ++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/crates/doc/src/project.rs b/crates/doc/src/project.rs index c9edbb1d25..37a5dff11d 100644 --- a/crates/doc/src/project.rs +++ b/crates/doc/src/project.rs @@ -426,7 +426,10 @@ fn projected_edges(body: &crate::ModelBody, mesh: &Mesh, plane: &Plane, edges: & let mut out = Vec::new(); for (e, exact) in edges.iter().zip(exact_edges(body, plane, edges)) { match exact { - Some(g) => out.extend(g), + Some(mut g) => { + at_vertices(body, mesh, plane, *e, &mut g); + out.extend(g); + } None => { if let Some(pts) = mesh.edges.get(*e) { out.extend(fit_world(plane, pts)); @@ -437,6 +440,51 @@ fn projected_edges(body: &crate::ModelBody, mesh: &Mesh, plane: &Plane, edges: & out } +/// Edge `e`'s exact pieces `g` with their outer ends on the edge's vertices, where they lie +/// within the edge's and the reading's tolerance of them. An edge's curve need only pass within +/// its tolerance of its vertices, and an arc is read off a NURBS within the planar-edge +/// tolerance: one SMLib made (a shell's offset corner) ended 2e-6 mm from the line it meets, and +/// the two ends, not taken for one point, left the loop open, so an offset of one curve found +/// no chain to follow. The vertex is the point both edges share. +fn at_vertices(body: &crate::ModelBody, mesh: &Mesh, plane: &Plane, e: usize, g: &mut [LinkGeom]) { + let record = body.body.record(); + let Some(edge) = record.edges.get(e) else { return }; + let Some((first, last)) = mesh.edges.get(e).and_then(|pts| pts.first().zip(pts.last())) else { return }; + // The pieces are read within the planar-edge tolerance of the edge's curve, which passes + // within its own tolerance of the vertices. + let tol = edge.vertices.iter().filter_map(|v| record.vertices.get(*v as usize)).map(|v| v.tolerance).fold(edge.tolerance, f64::max) + + body.body.planar_edges_tolerance(); + // Each vertex moves the piece end nearest it (the pieces' order and direction need not be + // the edge's). + for v in [plane.to_local(*first), plane.to_local(*last)] { + let mut best: Option<(f64, &mut Vec2)> = None; + for piece in g.iter_mut() { + if let LinkGeom::Line(a, b) | LinkGeom::Arc { a, b, .. } | LinkGeom::Conic { a, b, .. } = piece { + for p in [a, b] { + let d = p.dist(v); + if d <= tol && best.as_ref().is_none_or(|(bd, _)| d < *bd) { + best = Some((d, p)); + } + } + } + } + if let Some((_, p)) = best { + *p = v; + } + } + // An arc is its centre and two ends on one circle: with its ends moved, its centre moves to + // the nearest point equally far from both (on their bisector), or the sketch would move an + // end back off the vertex onto the circle through the other. + for piece in g.iter_mut() { + if let LinkGeom::Arc { c, a, b } = piece + && let Some(across) = Vec2::new(a.y - b.y, b.x - a.x).normalized() + { + let mid = (*a + *b) * 0.5; + *c = mid + across * (*c - mid).dot(across); + } + } +} + /// A boundary segment as the link geometry that is the same curve. fn link_geom(s: &Seg2) -> Option { Some(match *s { diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index 773e4fdb58..2c233bfe00 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -1198,6 +1198,13 @@ impl Body { .collect()) } + /// How far [`Body::planar_edges`] lets a piece it reads as a line or an arc lie from the + /// edge's curve (a NURBS within this of a circle is that arc), so how far a piece's ends may + /// lie from the edge's vertices. + pub fn planar_edges_tolerance(&self) -> f64 { + (self.size() * 1e-7).max(1e-9) * 10.0 + } + /// Edges `indices` (record edge indices) in `plane`'s coordinates, exactly: lines, circular /// arcs, and free-form curves as the Bézier spans they are, for a sketch to project them. /// `None` for an edge that doesn't lie in a plane parallel to `plane`, isn't read exactly or @@ -1206,7 +1213,7 @@ impl Body { if self.is_mesh() { return vec![None; indices.len()]; } - let tol = (self.size() * 1e-7).max(1e-9) * 10.0; + let tol = self.planar_edges_tolerance(); indices .iter() .map(|i| {