diff --git a/crates/doc/src/project.rs b/crates/doc/src/project.rs index ba0fa6d8e5..c9edbb1d25 100644 --- a/crates/doc/src/project.rs +++ b/crates/doc/src/project.rs @@ -361,7 +361,7 @@ fn edge_curves(body: Option<&crate::ModelBody>, e: usize) -> Vec { .edges .get(e) .filter(|edge| !matches!(edge.curve, solidcraft_geom::brep::Curve::SurfaceIntersection { .. })) - .map(|edge| vec![WireCurve { curve: edge.curve.clone(), interval: edge.interval }]) + .map(|edge| vec![WireCurve { curve: edge.parameters().clone(), interval: edge.interval }]) .unwrap_or_default() } diff --git a/crates/geom/src/brep.rs b/crates/geom/src/brep.rs index c4b13ae0fc..2c62760bc7 100644 --- a/crates/geom/src/brep.rs +++ b/crates/geom/src/brep.rs @@ -488,10 +488,39 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> { impl Edge { /// The curve the edge's parameter is on: its NURBS form when it has one, else its curve. - /// Everything that evaluates the edge at a parameter goes through this. + /// Everything that evaluates the edge at a parameter goes through this, with `interval`. pub fn parameters(&self) -> &Curve { self.nurbs.as_ref().unwrap_or(&self.curve) } + + /// The edge's interval on `curve` itself, for code that reads what the edge is (a circle's + /// angles): `interval` when the edge has no NURBS form; otherwise the angles of its ends on the + /// NURBS, unwrapped forward along the edge (a whole turn for a closed edge). A form's + /// circle or ellipse has its frame turned so its angle grows along the NURBS (the SMLib + /// conversion makes it so). `None` where the NURBS can't be evaluated. + pub fn curve_interval(&self) -> Option<[f64; 2]> { + curve_interval(&self.curve, self.nurbs.as_ref(), self.interval) + } +} + +/// [`Edge::curve_interval`] of an edge given by its parts: its `curve`, its NURBS form and its +/// interval on that form. +pub fn curve_interval(curve: &Curve, nurbs: Option<&Curve>, interval: [f64; 2]) -> Option<[f64; 2]> { + use std::f64::consts::TAU; + let Some(n) = nurbs else { return Some(interval) }; + let (frame, radii) = match curve { + Curve::Circle { frame, radius } => (frame, [*radius, *radius]), + Curve::Ellipse { frame, radii } => (frame, *radii), + _ => return Some(interval), + }; + let eval = eval::CurveEval::new(n).ok()?; + let [t0, t1] = interval; + let o = crate::Vec3::new(frame.origin[0], frame.origin[1], frame.origin[2]); + let (x, y) = (crate::Vec3::from(frame.x_axis), crate::Vec3::from(frame.y_axis)); + let angle = |t: f64| eval.point(t).map(|p| ((p - o).dot(y) / radii[1]).atan2((p - o).dot(x) / radii[0]).rem_euclid(TAU)); + let (a0, a1) = (angle(t0)?, angle(t1)?); + let sweep = (a1 - a0).rem_euclid(TAU); + Some([a0, a0 + if sweep <= 1e-12 { TAU } else { sweep }]) } impl Face { diff --git a/crates/kernel/src/assemble.rs b/crates/kernel/src/assemble.rs index f3c4e32f78..d5ccb3e8c8 100644 --- a/crates/kernel/src/assemble.rs +++ b/crates/kernel/src/assemble.rs @@ -26,6 +26,8 @@ fn invalid(m: impl std::fmt::Display) -> KernelError { #[derive(Clone, Debug)] pub(crate) struct AEdge { pub curve: Curve, + /// The NURBS form the edge's parameter is on, as the record's `Edge::nurbs`. + pub nurbs: Option, pub interval: [f64; 2], pub vertices: [u32; 2], pub tolerance: f64, @@ -118,6 +120,7 @@ impl Assembly { .enumerate() .map(|(i, e)| AEdge { curve: e.curve.clone(), + nurbs: e.nurbs.clone(), interval: e.interval, vertices: e.vertices, tolerance: e.tolerance, @@ -144,7 +147,7 @@ impl Assembly { /// A new edge on `curve` over `interval`, from `a` to `b`. pub(crate) fn edge(&mut self, curve: Curve, interval: [f64; 2], a: u32, b: u32) -> Result { - self.edges.push(AEdge { curve, interval, vertices: [a, b], tolerance: self.tolerance, source: None, of: Vec::new() }); + self.edges.push(AEdge { curve, nurbs: None, interval, vertices: [a, b], tolerance: self.tolerance, source: None, of: Vec::new() }); index(self.edges.len() - 1) } @@ -218,7 +221,7 @@ impl Assembly { } } let edges = self.edges.iter().map(|a| Edge { - nurbs: None, + nurbs: a.nurbs.clone(), curve: a.curve.clone(), interval: a.interval, vertices: a.vertices, @@ -260,7 +263,7 @@ impl Assembly { let e = self.edges.get(u.edge as usize).ok_or_else(|| invalid("edge reference"))?; Ok(EdgeUse { edge: u.edge, - curve: &e.curve, + curve: e.nurbs.as_ref().unwrap_or(&e.curve), interval: e.interval, forward: u.forward, tolerance: e.tolerance, diff --git a/crates/kernel/src/blend.rs b/crates/kernel/src/blend.rs index dbd70594c8..fbc9d6e721 100644 --- a/crates/kernel/src/blend.rs +++ b/crates/kernel/src/blend.rs @@ -119,7 +119,7 @@ impl Faces { /// The edge's unit tangent along its interval (start to end vertex) at its start or end. pub(crate) fn tangent(&self, e: usize, at_end: bool) -> Option { let edge = self.rec.edges.get(e)?; - curve_tangent(&edge.curve, if at_end { edge.interval[1] } else { edge.interval[0] }) + curve_tangent(edge.parameters(), if at_end { edge.interval[1] } else { edge.interval[0] }) } } diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index 40fbd6af8a..3e383323bb 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -870,7 +870,7 @@ impl Body { b.add(v3(v.point)); } for e in &self.record.edges { - curve_hull(&e.curve, e.interval, &mut b); + curve_hull(e.parameters(), e.interval, &mut b); } b } diff --git a/crates/kernel/src/fill.rs b/crates/kernel/src/fill.rs index b7abdc4bb4..58e6d3df17 100644 --- a/crates/kernel/src/fill.rs +++ b/crates/kernel/src/fill.rs @@ -625,14 +625,14 @@ fn patch_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], surface: b } } let given = match pcurves { - Pcurves::Projected(p) => Some(p.pcurve(&edge.curve, edge.interval).ok_or_else(|| { + Pcurves::Projected(p) => Some(p.pcurve(edge.parameters(), edge.interval).ok_or_else(|| { KernelError::Invalid( "not supported yet: a fitted patch on a circular, elliptic or traced edge (it has no exact pcurve on the fit)".into(), ) })?), Pcurves::Computed => None, }; - edges.push(PartEdge { curve: edge.curve.clone(), interval: edge.interval, vertices: ends }); + edges.push(PartEdge { curve: edge.parameters().clone(), interval: edge.interval, vertices: ends }); uses.push(PartUse { edge: k as u32, forward, given }); } if !ccw { diff --git a/crates/kernel/src/iges_out.rs b/crates/kernel/src/iges_out.rs index 3efa7e1a97..90fac84f8c 100644 --- a/crates/kernel/src/iges_out.rs +++ b/crates/kernel/src/iges_out.rs @@ -345,7 +345,7 @@ fn shell(w: &mut W, b: &Brep, si: u32, paint: &HashMap) -> Resu let mut ep = vec![edges.len().to_string()]; for &e in &edges { let edge = b.edges.get(e as usize).ok_or_else(|| failed(format!("edge {e}: no such edge")))?; - let c = topology::curve(&edge.curve).map_err(|m| failed(format!("edge {e}: {m}")))?; + let c = topology::curve(edge.parameters()).map_err(|m| failed(format!("edge {e}: {m}")))?; let [sv, tv] = edge.vertices.map(|v| vidx.get(&v).copied().unwrap_or(0)); let (pa, pb) = (verts.get(sv).copied().unwrap_or([0.0; 3]), verts.get(tv).copied().unwrap_or([0.0; 3])); let cd = curve(w, &c, edge.interval, pa, pb, sv == tv)?; diff --git a/crates/kernel/src/intersection.rs b/crates/kernel/src/intersection.rs index 2bf8c5a07e..bdaaf5e3d0 100644 --- a/crates/kernel/src/intersection.rs +++ b/crates/kernel/src/intersection.rs @@ -56,7 +56,7 @@ pub fn meeting_edges(a: &Body, a_face: Option, b: &Body, b_face: Option Result> { let e = asm.edges.get(u.edge as usize).ok_or_else(|| fail("edge"))?; let (s, t) = asm.ends(u).ok_or_else(|| fail("edge"))?; let (a, b) = (asm.point(s).ok_or_else(|| fail("vertex"))?, asm.point(t).ok_or_else(|| fail("vertex"))?); - let [t0, t1] = e.interval; + // The arc's angles, on the circle itself (its NURBS form, if any, is parameterised otherwise). + let [t0, t1] = solidcraft_geom::brep::curve_interval(&e.curve, e.nurbs.as_ref(), e.interval).ok_or_else(|| fail("edge"))?; match &e.curve { Curve::Line { .. } => Ok(Piece { a, b, arc: None }), Curve::Circle { frame, .. } => { diff --git a/crates/kernel/src/point_path.rs b/crates/kernel/src/point_path.rs index fb72a48740..618637e88b 100644 --- a/crates/kernel/src/point_path.rs +++ b/crates/kernel/src/point_path.rs @@ -152,7 +152,7 @@ const MAX_EDGE_SEGS: usize = 1000; pub fn edge_path(body: &Body, edge: usize) -> Result> { let record = crate::native::brep_record(body)?; let e = record.edges.get(edge).ok_or_else(|| KernelError::Invalid(format!("the body has no edge {edge}")))?; - curve_path(&e.curve, e.interval) + curve_path(e.parameters(), e.interval) } /// The sketch segments `segs` on `plane` projected along `direction` (pointing from them toward diff --git a/crates/kernel/src/prism_curve.rs b/crates/kernel/src/prism_curve.rs index 4fd305d5b9..611ffc3e2e 100644 --- a/crates/kernel/src/prism_curve.rs +++ b/crates/kernel/src/prism_curve.rs @@ -31,7 +31,7 @@ impl<'a> Run<'a> { let e = record.edges.get(edge)?; let at = |v: u32| record.vertices.get(v as usize).map(|v| Vec3::new(v.point[0], v.point[1], v.point[2])); let (start, end) = (at(e.vertices[0])?, at(e.vertices[1])?); - Some(Run { curve: &e.curve, interval: e.interval, forward, ends: if forward { [start, end] } else { [end, start] } }) + Some(Run { curve: e.parameters(), interval: e.interval, forward, ends: if forward { [start, end] } else { [end, start] } }) } /// The parameters it runs from and to. diff --git a/crates/kernel/src/surface_ruled.rs b/crates/kernel/src/surface_ruled.rs index 79fb40fe5f..7715eac842 100644 --- a/crates/kernel/src/surface_ruled.rs +++ b/crates/kernel/src/surface_ruled.rs @@ -351,7 +351,8 @@ fn normal_at(surface: &SurfaceEval, forward: bool, p: Vec3) -> Option { /// one span per quarter turn or less). fn base_curve(r: &Brep, e: usize, forward: bool) -> Result> { let edge = r.edges.get(e).ok_or_else(|| KernelError::Invalid("an edge reference".into()))?; - let [t0, t1] = edge.interval; + // On the curve itself: a conic's angles (its NURBS form, if any, is parameterised otherwise). + let [t0, t1] = edge.curve_interval().ok_or_else(|| KernelError::Invalid("an edge's NURBS form cannot be evaluated".into()))?; let c = match &edge.curve { b::Curve::Line { start, end } => { let at = |t: f64| [start[0] + (end[0] - start[0]) * t, start[1] + (end[1] - start[1]) * t, start[2] + (end[2] - start[2]) * t, 1.0]; diff --git a/crates/smlib/SMLIB_REV b/crates/smlib/SMLIB_REV index 6f4324b727..15c3e32204 100644 --- a/crates/smlib/SMLIB_REV +++ b/crates/smlib/SMLIB_REV @@ -1 +1 @@ -deec9cb312b68a92d1b018cece65e1d86643fee6 +26a4b7b6255a0e4db29fdd81cb4985bb9a9a1a8f diff --git a/crates/smlib/src/export.rs b/crates/smlib/src/export.rs index ba8bcae0ba..7545cb4887 100644 --- a/crates/smlib/src/export.rs +++ b/crates/smlib/src/export.rs @@ -417,6 +417,19 @@ fn surfaces(flat: View, n_faces: usize) -> Res Res { + let eval = b::eval::CurveEval::new(form).map_err(|e| invalid(format!("circle NURBS form: {e}")))?; + let [t0, t1] = range; + let (p, q) = (eval.point(t0), eval.point(t0 + 1e-3 * (t1 - t0))); + let (Some(p), Some(q)) = (p, q) else { return Err(invalid("circle NURBS form")) }; + let (o, x, y) = (Vec3::from(f.origin), Vec3::from(f.x_axis), Vec3::from(f.y_axis)); + let angle = |s: Vec3| (s - o).dot(y).atan2((s - o).dot(x)); + let step = (angle(q) - angle(p)).rem_euclid(TAU); + Ok(if step > std::f64::consts::PI { b::Frame { y_axis: [-f.y_axis[0], -f.y_axis[1], -f.y_axis[2]], ..f } } else { f }) +} + /// A NURBS curve: full knots, cartesian control points and weights. struct Nurbs { knots: Vec, @@ -559,7 +572,8 @@ fn curves(flat: View, n_edges: usize) -> Res<(Vec<(b::Curve, Option, [ // A circle with its NURBS form is parameterised on it: its range and the trims // on it are that NURBS's, so the trims carry over as they are. if forms.get(e) == Some(&1) { - (b::Curve::Circle { frame: f, radius }, Some(next_nurbs()?.record()), r, Some([1.0, 0.0])) + let form = next_nurbs()?.record(); + (b::Curve::Circle { frame: along(f, &form, r)?, radius }, Some(form), r, Some([1.0, 0.0])) } else { (b::Curve::Circle { frame: f, radius }, None, r, None) }