diff --git a/crates/kernel/src/step_in/mod.rs b/crates/kernel/src/step_in/mod.rs index dd05c78338..d48b240e06 100644 --- a/crates/kernel/src/step_in/mod.rs +++ b/crates/kernel/src/step_in/mod.rs @@ -866,7 +866,7 @@ impl Reader { /// the file does not parse or has no solid the reader can read. pub fn step_orientation_errors(text: &str) -> std::result::Result, String> { let ex = p21::parse(text)?; - let read = solidcraft_step::topology::read_solids(&ex); + let read = solidcraft_step::topology::read_solids_with(&ex, &solidcraft_smlib::pcurves::SmlibTrims); if read.solids.is_empty() { return Err(read.failures.first().map_or_else(|| "the file has no solids".into(), |f| format!("solid #{}: {}", f.item, f.message))); } diff --git a/crates/kernel/src/step_in/parallel.rs b/crates/kernel/src/step_in/parallel.rs index a3332e1199..59f13e5b5d 100644 --- a/crates/kernel/src/step_in/parallel.rs +++ b/crates/kernel/src/step_in/parallel.rs @@ -85,7 +85,7 @@ fn placed(body: &Body, m: &M4) -> crate::Result { /// and brought back as a body: the part SMLib made of the file. A solid the reader or SMLib /// refuses is not imported, with the reason. pub(super) fn smlib_part(ex: &Exchange, item: u64, units: Units) -> std::result::Result<(Part, Vec), Failure> { - let solid = solidcraft_step::topology::read_solid(ex, item, units).map_err(|msg| Failure { read: true, msg })?; + let solid = solidcraft_step::topology::read_solid_with(ex, item, units, &solidcraft_smlib::pcurves::SmlibTrims).map_err(|msg| Failure { read: true, msg })?; let failed = |e: KernelError| Failure { read: false, msg: e.to_string().trim_start_matches("the operation failed: ").to_string() }; let (brep, smlib_order) = crate::native::smlib_record_ordered(&solid.brep).map_err(failed)?; let (body, body_order) = crate::native::body_from_smlib_ordered(&brep).map_err(failed)?; diff --git a/crates/kernel/src/step_in/tests.rs b/crates/kernel/src/step_in/tests.rs index b283bbfacf..ed6e0f1b40 100644 --- a/crates/kernel/src/step_in/tests.rs +++ b/crates/kernel/src/step_in/tests.rs @@ -1682,7 +1682,7 @@ fn step_conformance_parts_read_back_their_exact_shape() { /// measured as the body of that record. pub(super) fn schema_4_volume(text: &str) -> f64 { let ex = solidcraft_step::p21::parse(text).unwrap(); - let r = solidcraft_step::topology::read_solids(&ex); + let r = solidcraft_step::topology::read_solids_with(&ex, &solidcraft_smlib::pcurves::SmlibTrims); assert!(r.failures.is_empty() && r.solids.len() == 1, "{:?}", r.failures); measure(&Body::from_record(r.solids[0].brep.clone()).unwrap()).unwrap().volume } diff --git a/crates/smlib/src/import.rs b/crates/smlib/src/import.rs index e95697d8d1..1e52d2898a 100644 --- a/crates/smlib/src/import.rs +++ b/crates/smlib/src/import.rs @@ -12,7 +12,7 @@ //! - Every end value (edge intervals, face domains) is already in the flat arrays' units, radians //! for angles; the C side converts them to SMLib's own pointwise. -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use solidcraft_geom::brep as b; @@ -85,7 +85,7 @@ fn z_axis(f: &b::Frame) -> [f64; 3] { pub fn arrays(brep: &b::Brep, trusted: bool) -> R { brep.validate().map_err(|e| Unsupported::Invalid(e.0))?; let brep = analytic::analytic(brep)?; - laid_out(brep.as_ref(), trusted, true) + laid_out(brep.as_ref(), trusted, true, &HashSet::new()) } /// [`arrays`] with every surface as the record holds it: a round face truck holds as a rational @@ -94,12 +94,19 @@ pub fn arrays(brep: &b::Brep, trusted: bool) -> R { /// surfaces. A revolution is still refused (SMLib has no such surface). pub fn arrays_keeping_splines(brep: &b::Brep, trusted: bool) -> R { brep.validate().map_err(|e| Unsupported::Invalid(e.0))?; - laid_out(brep, trusted, false) + laid_out(brep, trusted, false, &HashSet::new()) +} + +/// The flat arrays of a record whose uses in `bare` (face, loop, position) go without their +/// UV trims, for SMLib to make (their pcurves in the record are not read): what +/// [`crate::pcurves`] hands SMLib. Not validated, as its bare uses have no pcurves. +pub(crate) fn arrays_with_bare_uses(brep: &b::Brep, bare: &HashSet<(u32, u32, u32)>) -> R { + laid_out(brep, true, true, bare) } /// The flat arrays; `analytic`: a plane on any axes goes as SMLib's plane (else, off unit -/// orthogonal axes, as the bilinear patch it is). -fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R { +/// orthogonal axes, as the bilinear patch it is). The uses in `bare` go without UV trims. +fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool, bare: &HashSet<(u32, u32, u32)>) -> R { let mut a = Arrays::default(); // Regions and shells. Each region gets its own copy of each shell it lists. @@ -174,7 +181,7 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R { let any_spline_face = brep.faces.iter().any(|f| f.nurbs.is_some() || matches!(f.surface, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. })); let any_form = brep.faces.iter().any(|f| f.nurbs.is_some()); - for f in &brep.faces { + for (fi, f) in brep.faces.iter().enumerate() { a.i(flat::FACE_LOOP_COUNT).push(int(f.loops.len())?); a.i(flat::FACE_TRIM_TYPE).push(0); // A face with a NURBS form: what it is (its analytic entry), then the NURBS it is @@ -190,7 +197,7 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R { if any_form { a.i(flat::FACE_NURBS_FORM).push(i64::from(f.nurbs.is_some())); } - for l in &f.loops { + for (li, l) in f.loops.iter().enumerate() { match l { b::Loop::Vertex { vertex } => { a.i(flat::LOOP_EDGEUSE_COUNT).push(0); @@ -199,11 +206,11 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R { b::Loop::Edges { edges } => { a.i(flat::LOOP_EDGEUSE_COUNT).push(int(edges.len())?); a.i(flat::LOOP_VERTEX).push(-1); - for u in edges { + for (p, u) in edges.iter().enumerate() { a.i(flat::EDGEUSE_EDGE).push(i64::from(u.edge)); a.i(flat::EDGEUSE_ORIENTATION).push(i64::from(u.forward)); if any_spline_face { - if spline { + if spline && !bare.contains(&(idx(fi)?, idx(li)?, idx(p)?)) { let shift = intervals.get(u.edge as usize).map_or(0.0, |i| i.1); trim(&mut a, &u.pcurve, u.forward, shift)?; } else { diff --git a/crates/smlib/src/lib.rs b/crates/smlib/src/lib.rs index 059129e5e0..e36f63c940 100644 --- a/crates/smlib/src/lib.rs +++ b/crates/smlib/src/lib.rs @@ -23,6 +23,7 @@ pub mod history; pub mod import; mod measure; mod ops; +pub mod pcurves; mod sheets; mod split; pub mod ssi; diff --git a/crates/smlib/src/pcurves.rs b/crates/smlib/src/pcurves.rs new file mode 100644 index 0000000000..cdebead6ca --- /dev/null +++ b/crates/smlib/src/pcurves.rs @@ -0,0 +1,97 @@ +//! SMLib's own pcurves for a face's uses that have no closed form (an approximated intersection +//! curve on a cone, a file's spline edge on a spline surface): the face goes to SMLib alone, an +//! open sheet, with the trims it has and without the others, and SMLib makes those as it makes +//! every trim it needs (`SmEdgeuse::GetOrCreateUVTrimCurve`, when the body is exported). They are +//! SMLib's, relayed as it holds them on the record's parameters; nothing here fits a curve. + +use std::collections::HashSet; + +use solidcraft_geom::brep::pcurve::{TrimFace, Trims}; +use solidcraft_geom::brep::{self as b, Curve2, EdgeUseRef, Loop, OrientedEdge, OrientedFace, Region, Shell, Vertex}; + +/// [`Trims`] by SMLib. +#[derive(Clone, Copy, Debug, Default)] +pub struct SmlibTrims; + +impl Trims for SmlibTrims { + fn trims(&self, face: &TrimFace<'_>) -> Result>, String> { + trims(face).map_err(|e| format!("SMLib pcurves: {e}")) + } +} + +fn trims(face: &TrimFace<'_>) -> Result>, String> { + let (record, bare) = sheet(face)?; + let arrays = crate::import::arrays_with_bare_uses(&record, &bare).map_err(|e| e.to_string())?; + let body = crate::flat::Flat::from_arrays(&arrays).and_then(|f| f.import()).map_err(|e| e.to_string())?; + let (out, order) = body.to_native_ordered().map_err(|e| e.to_string())?; + let [made] = out.faces.as_slice() else { return Err(format!("SMLib made {} faces of one", out.faces.len())) }; + let mut result = Vec::with_capacity(face.loops.len()); + for l in &face.loops { + let mut pcs = Vec::with_capacity(l.len()); + for u in l { + // SMLib keeps the edges in the order they came; the record renumbers them. + let e = order.edges.get(u.edge as usize).copied().ok_or("an edge SMLib lost")?; + let edge = out.edges.get(e).ok_or("an edge SMLib lost")?; + let ours = face.edges.get(u.edge as usize).ok_or("edge reference")?; + let slack = 1e-9 * ours.interval.iter().fold(1.0f64, |m, t| m.max(t.abs())); + if (0..2).any(|k| (edge.interval[k] - ours.interval[k]).abs() > slack) { + return Err(format!("SMLib moved an edge's interval from {:?} to {:?}", ours.interval, edge.interval)); + } + let found = made.loops.iter().find_map(|ml| match ml { + Loop::Edges { edges } => edges.iter().find(|m| m.edge as usize == e && m.forward == u.forward), + Loop::Vertex { .. } => None, + }); + pcs.push(found.ok_or("a use SMLib did not keep")?.pcurve.clone()); + } + result.push(pcs); + } + Ok(result) +} + +/// The face as a one-face open sheet, and its uses that go without trims. +fn sheet(face: &TrimFace<'_>) -> Result<(b::Brep, HashSet<(u32, u32, u32)>), String> { + let mut edges: Vec = face + .edges + .iter() + .map(|e| b::Edge { + curve: e.curve.clone(), + nurbs: e.nurbs.cloned(), + interval: e.interval, + vertices: e.vertices, + tolerance: e.tolerance, + uses: Vec::new(), + }) + .collect(); + let mut bare = HashSet::new(); + let mut loops = Vec::with_capacity(face.loops.len()); + for (li, l) in face.loops.iter().enumerate() { + let li = u32::try_from(li).map_err(|_| "too many loops")?; + let mut uses = Vec::with_capacity(l.len()); + for (p, u) in l.iter().enumerate() { + let p = u32::try_from(p).map_err(|_| "too many uses")?; + let edge = edges.get_mut(u.edge as usize).ok_or("edge reference")?; + edge.uses.push(EdgeUseRef { face: 0, r#loop: li, position: p, upper: !u.forward }); + let pcurve = match &u.pcurve { + Some(pc) => pc.clone(), + None => { + bare.insert((0, li, p)); + // Not read: the use goes without a trim (`arrays_with_bare_uses`). + Curve2::Spline { knots: vec![edge.interval[0], edge.interval[0], edge.interval[1], edge.interval[1]], points: vec![[0.0; 2]; 2] } + } + }; + uses.push(OrientedEdge { edge: u.edge, forward: u.forward, pcurve }); + } + loops.push(Loop::Edges { edges: uses }); + } + let vertices = face.vertices.iter().map(|&point| Vertex { point, tolerance: face.tolerance }).collect(); + let record = b::Brep { + version: b::VERSION, + tolerance: face.tolerance, + regions: vec![Region { shells: vec![0], void: false }], + shells: vec![Shell::Faces { faces: vec![OrientedFace { face: 0, forward: true }], wires: Vec::new() }], + faces: vec![b::Face { surface: face.surface.clone(), nurbs: face.nurbs.cloned(), domain: face.domain, loops }], + edges, + vertices, + }; + Ok((record, bare)) +} diff --git a/crates/step/src/topology.rs b/crates/step/src/topology.rs index a238544cb1..cd983b4dfe 100644 --- a/crates/step/src/topology.rs +++ b/crates/step/src/topology.rs @@ -27,8 +27,9 @@ //! distance (with margin) as its tolerance; a face whose edges lie off its surface gets its //! pcurves fitted at a looser tolerance, which its edges then carry. //! -//! Pcurves are computed from the 3D edges and our surfaces ([`pcurve::pcurves`]); the file's -//! own PCURVE entities are not used. +//! Pcurves are the closed forms the 3D edges have on our surfaces ([`pcurve::exact_pcurves`]); a +//! use with none gets its pcurve from the kernel ([`read_solids_with`], SMLib's own trims). The +//! file's own PCURVE entities are not used. //! //! Input is hostile: every reference is checked, nesting and counts are capped, and the work of //! a whole file is bounded (shared shells count each time a solid uses them). @@ -38,7 +39,7 @@ use std::f64::consts::TAU; use solidcraft_geom::Vec3; use solidcraft_geom::brep::eval::{CurveEval, SurfaceEval}; -use solidcraft_geom::brep::pcurve::{self, EdgeUse}; +use solidcraft_geom::brep::pcurve::{self, EdgeUse, TrimEdge, TrimFace, TrimUse, Trims}; use solidcraft_geom::brep::{ self, Brep, Curve as BCurve, Curve2, Edge, EdgeUseRef, Face, Frame, Loop, Natural, OrientedEdge, OrientedFace, Region, Shell, Surface as BSurface, Vertex, @@ -227,8 +228,18 @@ impl Index { } /// Read every solid of the file. A solid that cannot be read is listed in -/// [`Solids::failures`] with its reason; the others are still read. +/// [`Solids::failures`] with its reason; the others are still read. A face's use with no +/// closed-form pcurve refuses its solid: [`read_solids_with`] has a kernel make those. pub fn read_solids(ex: &Exchange) -> Solids { + read_solids_in(ex, None) +} + +/// [`read_solids`], with `trims` making the pcurves of uses that have no closed form. +pub fn read_solids_with(ex: &Exchange, trims: &dyn Trims) -> Solids { + read_solids_in(ex, Some(trims)) +} + +fn read_solids_in(ex: &Exchange, trims: Option<&dyn Trims>) -> Solids { let mut items: Vec = ex.entities.iter().filter(|(_, e)| SOLID_ITEMS.contains(&e.name())).map(|(id, _)| *id).collect(); items.sort_unstable(); let index = Index::new(ex); @@ -247,7 +258,7 @@ pub fn read_solids(ex: &Exchange) -> Solids { (Reader::new(ex, c.units), c) }); let context = *context; - match read_with(ex, reader, item, context, &mut budget) { + match read_with(ex, reader, item, context, &mut budget, trims) { Ok(mut s) => { s.product = index.product.get(&item).cloned(); if s.name.is_empty() @@ -267,7 +278,14 @@ pub fn read_solids(ex: &Exchange) -> Solids { pub fn read_solid(ex: &Exchange, item: u64, context: units::Context) -> R { let mut reader = Reader::new(ex, context.units); let mut budget = MAX_FILE_FACES; - read_with(ex, &mut reader, item, context, &mut budget) + read_with(ex, &mut reader, item, context, &mut budget, None) +} + +/// [`read_solid`], with `trims` making the pcurves of uses that have no closed form. +pub fn read_solid_with(ex: &Exchange, item: u64, context: units::Context, trims: &dyn Trims) -> R { + let mut reader = Reader::new(ex, context.units); + let mut budget = MAX_FILE_FACES; + read_with(ex, &mut reader, item, context, &mut budget, Some(trims)) } /// The base tolerance of a context. @@ -275,11 +293,18 @@ pub fn tolerance(context: &units::Context) -> f64 { context.uncertainty.unwrap_or(DEFAULT_TOLERANCE).clamp(MIN_TOLERANCE, MAX_FIT_TOLERANCE) } -fn read_with<'a>(ex: &'a Exchange, reader: &mut Reader<'a>, item: u64, context: units::Context, budget: &mut usize) -> R { +fn read_with<'a>( + ex: &'a Exchange, + reader: &mut Reader<'a>, + item: u64, + context: units::Context, + budget: &mut usize, + trims: Option<&dyn Trims>, +) -> R { let e = entity(ex, item)?; let p = e.params(); let name = p.first().and_then(Param::as_str).unwrap_or("").to_string(); - let mut b = Builder::new(ex, reader, tolerance(&context), budget); + let mut b = Builder::new(ex, reader, tolerance(&context), budget, trims); let (shells, kind) = match e.name() { "MANIFOLD_SOLID_BREP" => (vec![b.shell(rid(p.get(1), "outer shell")?)?], Kind::Solid), "BREP_WITH_VOIDS" => { @@ -347,6 +372,8 @@ struct Builder<'b, 'a> { reader: &'b mut Reader<'a>, tol: f64, budget: &'b mut usize, + /// Makes the pcurves of uses that have no closed form; without it such a face is refused. + trims: Option<&'b dyn Trims>, vertex_ids: HashMap, vertices: Vec, /// STEP edge → schema edge, and whether the schema edge runs with the STEP edge. @@ -358,12 +385,13 @@ struct Builder<'b, 'a> { } impl<'b, 'a> Builder<'b, 'a> { - fn new(ex: &'a Exchange, reader: &'b mut Reader<'a>, tol: f64, budget: &'b mut usize) -> Self { + fn new(ex: &'a Exchange, reader: &'b mut Reader<'a>, tol: f64, budget: &'b mut usize, trims: Option<&'b dyn Trims>) -> Self { Builder { ex, reader, tol, budget, + trims, vertex_ids: HashMap::new(), vertices: Vec::new(), edge_ids: HashMap::new(), @@ -965,23 +993,6 @@ impl<'b, 'a> Builder<'b, 'a> { // Which loop index in `face.loops` each edge loop is. let positions: Vec = face.loops.iter().enumerate().filter(|(_, l)| matches!(l, LoopB::Edges(_))).map(|(i, _)| i).collect(); let tol = self.tol; - // The face's pcurves on `on`, each edge on its NURBS form. - let pcurves_on = |on: &BSurface, domain: Option<[[f64; 2]; 2]>| -> R>> { - let mut uses = Vec::with_capacity(edge_loops.len()); - for l in &edge_loops { - let mut u = Vec::with_capacity(l.len()); - for &(e, forward) in l { - let edge = self.edges.get(e as usize).ok_or("edge reference")?; - let (curve, interval) = match &edge.form { - Some((c, i)) => (c, *i), - _ => (&edge.curve, edge.interval), - }; - u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: 0.0, given: None }); - } - uses.push(u); - } - pcurve::pcurves_within(on, &uses, tol, domain).map_err(|e| e.to_string()) - }; // A cylinder, cone, sphere, torus or revolution on its NURBS form, over the extent of its // boundary on its label; then every pcurve on the forms. let form = match brep::form::frame_of(&label) { @@ -1004,8 +1015,21 @@ impl<'b, 'a> Builder<'b, 'a> { Some((f, d)) => (Some(f), Some(d)), None => (None, None), }; - let surface = form.clone().unwrap_or(label); - let pcs = pcurves_on(&surface, box_on_form)?; + let surface = form.clone().unwrap_or_else(|| label.clone()); + // Closed forms where the geometry has them; the kernel makes the rest (`trims`). + let exact = pcurve::exact_pcurves(&surface, &uses_of(&self.edges, &edge_loops, None)?, tol, box_on_form).map_err(|e| e.to_string())?; + // Without a kernel, a use with neither is refused below with why. + let made = match self.trims { + Some(trims) if exact.iter().flatten().any(Option::is_none) => { + let domain = match box_on_form { + Some(d) => d, + None => natural_box(&surface).ok_or_else(|| unsupported("a face on an unbounded surface without closed-form pcurves"))?, + }; + Some(trims.trims(&self.trim_face(&label, form.as_ref(), domain, &edge_loops, exact)?)?) + } + _ => None, + }; + let pcs = pcurve::pcurves_within(&surface, &uses_of(&self.edges, &edge_loops, made.as_ref())?, tol, box_on_form).map_err(|e| e.to_string())?; // Signed areas of the loops that close in (u, v). let periods = natural_periods(&surface); // Each use starts near where the one before it ends, along a bounded direction that @@ -1071,6 +1095,87 @@ impl<'b, 'a> Builder<'b, 'a> { } } +impl Builder<'_, '_> { + /// The face as a kernel takes it to make its missing pcurves: its edges and vertices + /// numbered within it, each use with its closed-form pcurve where `exact` has one. + fn trim_face<'s>( + &'s self, + surface: &'s BSurface, + nurbs: Option<&'s BSurface>, + domain: [[f64; 2]; 2], + edge_loops: &[Vec<(u32, bool)>], + exact: Vec>>, + ) -> R> { + let (mut edge_at, mut vertex_at) = (HashMap::new(), HashMap::new()); + let (mut edges, mut vertices) = (Vec::new(), Vec::new()); + let mut loops = Vec::with_capacity(edge_loops.len()); + for (l, pcs) in edge_loops.iter().zip(exact) { + let mut uses = Vec::with_capacity(l.len()); + for (&(e, forward), pcurve) in l.iter().zip(pcs) { + let local = match edge_at.get(&e) { + Some(i) => *i, + None => { + let edge = self.edges.get(e as usize).ok_or("edge reference")?; + let mut ends = [0u32; 2]; + for (end, v) in ends.iter_mut().zip(edge.vertices) { + *end = match vertex_at.get(&v) { + Some(i) => *i, + None => { + let i = index(vertices.len())?; + vertices.push(self.point(v)?); + vertex_at.insert(v, i); + i + } + }; + } + let (curve, nurbs, interval) = match &edge.form { + Some((n, i)) => (&edge.curve, Some(n), *i), + None => (&edge.curve, None, edge.interval), + }; + let i = index(edges.len())?; + edges.push(TrimEdge { curve, nurbs, interval, vertices: ends, tolerance: edge.tolerance }); + edge_at.insert(e, i); + i + } + }; + uses.push(TrimUse { edge: local, forward, pcurve }); + } + loops.push(uses); + } + Ok(TrimFace { surface, nurbs, domain, edges, vertices, loops, tolerance: self.tol }) + } +} + +/// A face's uses on their edges' NURBS forms, with `given` pcurves where a kernel made them. +fn uses_of<'e>(edges: &'e [EdgeB], edge_loops: &[Vec<(u32, bool)>], given: Option<&'e Vec>>) -> R>>> { + let mut uses = Vec::with_capacity(edge_loops.len()); + for (li, l) in edge_loops.iter().enumerate() { + let mut u = Vec::with_capacity(l.len()); + for (p, &(e, forward)) in l.iter().enumerate() { + let edge = edges.get(e as usize).ok_or("edge reference")?; + let (curve, interval) = match &edge.form { + Some((c, i)) => (c, *i), + _ => (&edge.curve, edge.interval), + }; + let given = match given { + Some(g) => Some(g.get(li).and_then(|l| l.get(p)).ok_or("a pcurve the kernel did not make")?), + None => None, + }; + u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: 0.0, given }); + } + uses.push(u); + } + Ok(uses) +} + +/// A spline surface's parameter box: its knot ranges. +fn natural_box(s: &BSurface) -> Option<[[f64; 2]; 2]> { + match s.natural() { + [Natural::Range(a, b), Natural::Range(c, d)] if [a, b, c, d].iter().all(|x| x.is_finite()) => Some([[a, b], [c, d]]), + _ => None, + } +} + /// The loop run the other way. fn reverse(uses: &mut [(u32, bool)]) { uses.reverse();