diff --git a/crates/kernel/src/fair.rs b/crates/kernel/src/fair.rs index 95bbfa5337..e009123e5b 100644 --- a/crates/kernel/src/fair.rs +++ b/crates/kernel/src/fair.rs @@ -43,13 +43,13 @@ //! control point that does not change the geometry at all — is reported as an error and the //! geometry is left alone. +use solidcraft_geom::brep as b; use solidcraft_geom::bspline::{HPoint, MAX_SPLINE_DEGREE, NurbsSurface, Side, SplineError, check_knots, local_basis}; use solidcraft_geom::{Grid, Vec3}; -use truck_modeling as mt; use crate::body::Body; use crate::history::Change; -use crate::{KernelError, Result, guard}; +use crate::{KernelError, Result}; /// Most steps one fairing run takes (see [`FairOptions::iterations`]). pub const MAX_FAIR_ITERATIONS: usize = 1000; @@ -720,60 +720,62 @@ pub fn fair_net(degree: [usize; 2], knots: [&[f64]; 2], ctrl: &G Ok(FairNet { degree, knots: [knots[0].to_vec(), knots[1].to_vec()], ctrl: faired, report }) } -/// Fair a truck B-spline curve's control points: [`fair_curve`] on its three-dimensional points, -/// rebuilt as a B-spline of the same knots. Guarded here itself, like every call into truck, so a -/// panic inside comes back as an error and the design is left alone. -pub fn fair_bspline_curve(curve: &mt::BSplineCurve, opts: &FairOptions) -> Result<(mt::BSplineCurve, FairReport)> { - guard("fair a B-spline curve", || { - let ctrl: Vec<[f64; 3]> = curve.control_points().iter().map(|p| [p.x, p.y, p.z]).collect(); - let faired = fair_curve(curve.degree(), curve.knot_vec(), &ctrl, opts)?; - let points: Vec = faired.ctrl.iter().map(|c| mt::Point3::new(c[0], c[1], c[2])).collect(); - let curve = mt::BSplineCurve::try_new(mt::KnotVec::from(faired.knots), points).map_err(|e| KernelError::Failed(format!("fair: {e}")))?; - Ok((curve, faired.report)) - }) +/// The degree of a spline record of `count` control points on `knots`. +fn degree_of(knots: &[f64], count: usize) -> Result { + knots.len().checked_sub(count + 1).ok_or_else(|| KernelError::Invalid(format!("{} knots are too few for {count} control points", knots.len()))) } -/// Fair a truck rational curve's control points: its Cartesian control points are faired like any -/// other curve and its weights are kept, so the result is the same rational curve with moved -/// control points. The convex-hull bound that keeps the geometry within the tolerance still -/// holds, because with the weights unchanged the rational basis functions are non-negative and -/// sum to one. Guarded like [`fair_bspline_curve`]. -pub fn fair_nurbs_curve(curve: &mt::NurbsCurve, opts: &FairOptions) -> Result<(mt::NurbsCurve, FairReport)> { - guard("fair a NURBS curve", || { - let count = curve.control_points().len(); - let mut ctrl = Vec::with_capacity(count); - let mut weights = Vec::with_capacity(count); - for h in curve.control_points() { - let Some(p) = (HPoint { x: h.x, y: h.y, z: h.z, w: h.w }).project() else { - return Err(KernelError::Invalid("a control point has no position: its weight is zero".into())); - }; - ctrl.push([p.x, p.y, p.z]); - weights.push(h.w); - } - let faired = fair_curve(curve.degree(), curve.knot_vec(), &ctrl, opts)?; - let points: Vec = faired.ctrl.iter().zip(&weights).map(|(c, w)| mt::Vector4::new(c[0] * w, c[1] * w, c[2] * w, *w)).collect(); - let bsp = mt::BSplineCurve::try_new(mt::KnotVec::from(faired.knots), points).map_err(|e| KernelError::Failed(format!("fair: {e}")))?; - Ok((mt::NurbsCurve::new(bsp), faired.report)) - }) +/// Fair a B-spline curve record's control points (a `Curve::Spline`): [`fair_curve`] on its +/// points, the same knots. +pub fn fair_bspline_curve(curve: &b::Curve, opts: &FairOptions) -> Result<(b::Curve, FairReport)> { + let b::Curve::Spline { knots, points } = curve else { return Err(KernelError::Invalid("not a B-spline curve".into())) }; + let faired = fair_curve(degree_of(knots, points.len())?, knots, points, opts)?; + Ok((b::Curve::Spline { knots: faired.knots, points: faired.ctrl }, faired.report)) } -/// Fair a truck B-spline surface's control net: [`fair_net`] on its points, rebuilt as a B-spline -/// of the same knots, with the boundary rows held. Guarded like [`fair_bspline_curve`]. -pub fn fair_bspline_surface(surface: &mt::BSplineSurface, opts: &FairOptions) -> Result<(mt::BSplineSurface, FairReport)> { - guard("fair a B-spline surface", || { - let net = truck_net(surface.control_points())?; - let (ku, kv) = surface.knot_vecs(); - let (pu, pv) = surface.degrees(); - let faired = fair_net([pu, pv], [ku, kv], &net.map(|p| [p.x, p.y, p.z]), opts)?; - let net = faired.ctrl.map(|[x, y, z]| mt::Point3::new(x, y, z)).into_u_rows(); - let surface = mt::BSplineSurface::try_new((ku.clone(), kv.clone()), net).map_err(|e| KernelError::Failed(format!("fair: {e}")))?; - Ok((surface, faired.report)) - }) +/// Fair a rational curve record's control points (a `Curve::RationalSpline`): its Cartesian +/// control points are faired like any other curve and its weights are kept, so the result is the +/// same rational curve with moved control points. The convex-hull bound that keeps the geometry +/// within the tolerance still holds, because with the weights unchanged the rational basis +/// functions are non-negative and sum to one. +pub fn fair_nurbs_curve(curve: &b::Curve, opts: &FairOptions) -> Result<(b::Curve, FairReport)> { + let b::Curve::RationalSpline { knots, homogeneous } = curve else { return Err(KernelError::Invalid("not a rational B-spline curve".into())) }; + let hpoints: Vec = homogeneous.iter().map(|h| HPoint { x: h[0], y: h[1], z: h[2], w: h[3] }).collect(); + let ctrl = positions(&Grid::row(hpoints.clone()))?; + let faired = fair_curve(degree_of(knots, homogeneous.len())?, knots, ctrl.cells(), opts)?; + let net = reweighted(Grid::row(hpoints), &Grid::row(faired.ctrl)); + let homogeneous = net.into_cells().into_iter().map(|h| [h.x, h.y, h.z, h.w]).collect(); + Ok((b::Curve::RationalSpline { knots: faired.knots, homogeneous }, faired.report)) } -/// A truck surface's control net (`rows[i][j]`, i along u) as a grid: refused when it is empty +/// Fair a B-spline surface record's control net (a `Surface::Spline`): [`fair_net`] on its +/// points, the same knots, the boundary rows held. +pub fn fair_bspline_surface(surface: &b::Surface, opts: &FairOptions) -> Result<(b::Surface, FairReport)> { + let b::Surface::Spline { u_knots, v_knots, points } = surface else { return Err(KernelError::Invalid("not a B-spline surface".into())) }; + let net = record_net(points)?; + let [cu, cv] = net.count(); + let degree = [degree_of(u_knots, cu)?, degree_of(v_knots, cv)?]; + let faired = fair_net(degree, [u_knots, v_knots], &net, opts)?; + Ok((b::Surface::Spline { u_knots: u_knots.clone(), v_knots: v_knots.clone(), points: faired.ctrl.into_u_rows() }, faired.report)) +} + +/// Fair a rational surface record's control net (a `Surface::RationalSpline`), like +/// [`fair_surface`]: the Cartesian control points are faired, the weights kept. +pub fn fair_nurbs_surface(surface: &b::Surface, opts: &FairOptions) -> Result<(b::Surface, FairReport)> { + let b::Surface::RationalSpline { u_knots, v_knots, homogeneous } = surface else { + return Err(KernelError::Invalid("not a rational B-spline surface".into())); + }; + let net = record_net(homogeneous)?.map(|h| HPoint { x: h[0], y: h[1], z: h[2], w: h[3] }); + let [cu, cv] = net.count(); + let degree = [degree_of(u_knots, cu)?, degree_of(v_knots, cv)?]; + let faired = fair_net(degree, [u_knots, v_knots], &positions(&net)?, opts)?; + let homogeneous = reweighted(net, &faired.ctrl).map(|h| [h.x, h.y, h.z, h.w]).into_u_rows(); + Ok((b::Surface::RationalSpline { u_knots: u_knots.clone(), v_knots: v_knots.clone(), homogeneous }, faired.report)) +} + +/// A surface record's control net (`rows[i][j]`, i along u) as a grid: refused when it is empty /// or not rectangular. -fn truck_net(rows: &[Vec

]) -> Result> { +fn record_net(rows: &[Vec

]) -> Result> { let net = Grid::from_u_rows(rows).ok_or_else(|| KernelError::Invalid("the surface's control net is not rectangular".into()))?; if net.is_empty() { return Err(KernelError::Invalid("the surface has no control points".into())); @@ -813,30 +815,15 @@ pub fn fair_surface(surface: &NurbsSurface, opts: &FairOptions) -> Result<(Nurbs Ok((out, faired.report)) } -/// Fair a truck rational surface's control net, like [`fair_surface`]: the Cartesian control -/// points are faired, the weights kept, and the result is a NURBS surface of the same knots. -/// Guarded like [`fair_bspline_curve`]. -pub fn fair_nurbs_surface(surface: &mt::NurbsSurface, opts: &FairOptions) -> Result<(mt::NurbsSurface, FairReport)> { - guard("fair a NURBS surface", || { - let bsp = surface.non_rationalized(); - let net = truck_net(bsp.control_points())?.map(|h| HPoint { x: h.x, y: h.y, z: h.z, w: h.w }); - let (ku, kv) = bsp.knot_vecs(); - let (pu, pv) = bsp.degrees(); - let faired = fair_net([pu, pv], [ku, kv], &positions(&net)?, opts)?; - let net = reweighted(net, &faired.ctrl).map(|h| mt::Vector4::new(h.x, h.y, h.z, h.w)).into_u_rows(); - let bsp = mt::BSplineSurface::try_new((ku.clone(), kv.clone()), net).map_err(|e| KernelError::Failed(format!("fair: {e}")))?; - Ok((mt::NurbsSurface::new(bsp), faired.report)) - }) -} - /// Fair the B-spline faces at `faces` (indices in `body`'s face order) of `body`: each face's /// interior control vertices move to flatten the thin-plate energy of the face while its boundary /// rows stay put, so the face keeps meeting its neighbours as it did. /// -/// Every picked face keeps the very wires, edges and vertices it had and the way it pointed: only -/// the surface between them changes, so the body is as closed (or as open) as it was, holes and -/// all. A face that is not a B-spline (a plane, cylinder, cone, sphere, torus or swept face) has no -/// control points to fair and is refused by name; a mesh body has no faces to fair at all. +/// Every picked face keeps the very loops, edges and vertices it had and the way it pointed: only +/// its surface changes in the body's record, so the body is as closed (or as open) as it was, +/// holes and all, and SMLib builds it from that record. A face that is not a B-spline (a plane, +/// cylinder, cone, sphere, torus or revolution) has no control points to fair and is refused by +/// name; a mesh body has no faces to fair at all. /// /// The report is over the picked faces together: the energies summed, the largest movement of any /// control point, and the energy trace as the sum at each step (a face that stops early keeps its @@ -844,14 +831,15 @@ pub fn fair_nurbs_surface(surface: &mt::NurbsSurface, opts: &FairOp pub fn fair_faces(body: &Body, faces: &[usize], opts: &FairOptions) -> Result<(Body, FairReport)> { let opts = opts.checked()?; body.require_brep("fair")?; - let all: Vec = body.solid.face_iter().cloned().collect(); - if all.is_empty() { + let mut record = crate::native::brep_record(body)?; + let count = record.faces.len(); + if count == 0 { return Err(KernelError::Invalid("the body has no faces".into())); } let mut picked: Vec = Vec::new(); for &i in faces { - if i >= all.len() { - return Err(KernelError::Invalid(format!("no face {i}: the body has {}", all.len()))); + if i >= count { + return Err(KernelError::Invalid(format!("no face {i}: the body has {count}"))); } if !picked.contains(&i) { picked.push(i); @@ -860,78 +848,52 @@ pub fn fair_faces(body: &Body, faces: &[usize], opts: &FairOptions) -> Result<(B if picked.is_empty() { return Err(KernelError::Invalid("pick the faces to fair".into())); } - guard("fair faces", || { - let mut faired = all.clone(); - let mut reports: Vec = Vec::with_capacity(picked.len()); - for &i in &picked { - let Some(face) = all.get(i) else { continue }; - let (surface, report) = match face.surface() { - mt::Surface::BSplineSurface(s) => { - let (faired, report) = fair_bspline_surface(&s, &opts)?; - (mt::Surface::BSplineSurface(faired), report) - } - mt::Surface::NurbsSurface(s) => { - let (faired, report) = fair_nurbs_surface(&s, &opts)?; - (mt::Surface::NurbsSurface(faired), report) - } - _ => { - return Err(KernelError::Invalid(format!( - "face {i} is not a B-spline face: a plane, cylinder, cone, sphere, torus or swept face has no control points to fair" - ))); - } - }; - // The same face as before, wires and all, with the faired surface between them. - let mut nf = mt::Face::new_unchecked(face.absolute_boundaries().clone(), surface); - if !face.orientation() { - nf.invert(); + let mut reports: Vec = Vec::with_capacity(picked.len()); + for &i in &picked { + let Some(face) = record.faces.get_mut(i) else { continue }; + let (surface, report) = match &face.surface { + b::Surface::Spline { .. } => fair_bspline_surface(&face.surface, &opts)?, + b::Surface::RationalSpline { .. } => fair_nurbs_surface(&face.surface, &opts)?, + _ => { + return Err(KernelError::Invalid(format!( + "face {i} is not a B-spline face: a plane, cylinder, cone, sphere, torus or revolution has no control points to fair" + ))); } - if let Some(slot) = faired.get_mut(i) { - *slot = nf; - } - reports.push(report); - } - let report = FairReport { - energy_before: reports.iter().map(|r| r.energy_before).sum(), - energy_after: reports.iter().map(|r| r.energy_after).sum(), - steps: reports.iter().map(|r| r.steps).max().unwrap_or(0), - movement: reports.iter().fold(0.0f64, |m, r| m.max(r.movement)), - energy_trace: { - let longest = reports.iter().map(|r| r.energy_trace.len()).max().unwrap_or(0); - (0..longest).map(|k| reports.iter().map(|r| r.energy_trace.get(k).copied().unwrap_or(r.energy_after)).sum()).collect() - }, }; - // The body shell for shell, in its own face order: a body with holes keeps them. - let mut shells: Vec = Vec::with_capacity(body.solid.boundaries().len()); - let mut index = 0usize; - for shell in body.solid.boundaries() { - let mut shell_faces = Vec::with_capacity(shell.face_iter().count()); - for f in shell.face_iter() { - let i = index; - index += 1; - shell_faces.push(faired.get(i).cloned().unwrap_or_else(|| f.clone())); - } - shells.push(mt::Shell::from(shell_faces)); + face.surface = surface; + reports.push(report); + } + let report = FairReport { + energy_before: reports.iter().map(|r| r.energy_before).sum(), + energy_after: reports.iter().map(|r| r.energy_after).sum(), + steps: reports.iter().map(|r| r.steps).max().unwrap_or(0), + movement: reports.iter().fold(0.0f64, |m, r| m.max(r.movement)), + energy_trace: { + let longest = reports.iter().map(|r| r.energy_trace.len()).max().unwrap_or(0); + (0..longest).map(|k| reports.iter().map(|r| r.energy_trace.get(k).copied().unwrap_or(r.energy_after)).sum()).collect() + }, + }; + let smlib = solidcraft_smlib::Brep::from_record(&record, true).map_err(|e| KernelError::Failed(format!("fair: SMLib: {e}")))?; + let (result, order) = crate::native::body_from_smlib_ordered(&smlib)?; + // The topology is the body's own, so SMLib's order is the body's: each face kept or + // modified, each edge kept. Where SMLib's order is not the record's the history is not known. + let kept = order.is_some_and(|o| o.faces.iter().enumerate().all(|(i, f)| i == *f) && o.edges.iter().enumerate().all(|(i, e)| i == *e)); + if !kept || result.face_count() != body.face_count() || result.edge_count() != body.edge_count() { + return Ok((result, report)); + } + let mut history = ::identity(body); + for &i in &picked { + if let Some(f) = history.faces.get_mut(i) { + f.change = Change::Modified; } - // The topology is the body's own, face for face, so the solid is built as it (the shells - // are as closed, or as open, as they were) and only the shape is measured anew. - let mut result = body.clone(); - result.solid = std::sync::Arc::new(mt::Solid::new_unchecked(shells)); - result.size = 0.0; - result.volume = Some(std::sync::Arc::new(std::sync::Mutex::new(None))); - result.coarse_missing = None; - result.history = None; - result.phased = false; - let faces: Vec = - (0..all.len()).map(|i| crate::record::from_face(0, i, if picked.contains(&i) { Change::Modified } else { Change::Kept })).collect(); - let result = crate::record::attach(result, &[crate::record::Operand::new(body)], Some(faces), std::collections::HashMap::new())?; - Ok((result, report)) - }) + } + Ok((result.with_history(history, &[body])?, report)) } #[cfg(test)] mod tests { use super::*; - use mt::ParametricCurve; + use solidcraft_geom::brep::eval::CurveEval; /// A clamped uniform knot vector over `[0, spans]` of degree `degree`: `spans + degree` /// control points. @@ -984,8 +946,8 @@ mod tests { } /// The greatest distance between two points of space, in mm. - fn apart(a: mt::Point3, b: mt::Point3) -> f64 { - ((a.x - b.x).powi(2) + (a.y - b.y).powi(2) + (a.z - b.z).powi(2)).sqrt() + fn apart(a: Vec3, b: Vec3) -> f64 { + a.dist(b) } /// A geom point as a coordinate triple. @@ -1218,28 +1180,29 @@ mod tests { } #[test] - fn the_truck_wrappers_fair_a_spline_curve_and_surface_with_their_ends_held() { + fn the_record_wrappers_fair_a_spline_curve_and_surface_with_their_ends_held() { let knots = [0.0, 0.0, 0.0, 0.0, 1.0, 3.0, 5.0, 9.0, 9.0, 9.0, 9.0]; let smooth = affine_curve(&knots, 3, [-20.0, 5.0, 1.5], [4.0, -2.5, 3.0]); let mut rough = smooth.clone(); rough[2][2] += 3.0; rough[4][1] -= 2.0; - let points = |net: &[[f64; 3]]| -> Vec { net.iter().map(|c| mt::Point3::new(c[0], c[1], c[2])).collect() }; - let curve = mt::BSplineCurve::try_new(mt::KnotVec::from(knots.to_vec()), points(&rough)).unwrap(); + let curve = b::Curve::Spline { knots: knots.to_vec(), points: rough.clone() }; let tolerance = 5.0; let opts = FairOptions::new(1.0, 8, false, tolerance).unwrap(); let (faired, report) = fair_bspline_curve(&curve, &opts).unwrap(); + let (fe, ce) = (CurveEval::new(&faired).unwrap(), CurveEval::new(&curve).unwrap()); let mut off = 0.0f64; for k in 0..=64 { let t = 9.0 * f64::from(k) / 64.0; - off = off.max(apart(faired.subs(t), curve.subs(t))); + off = off.max(apart(fe.point(t).unwrap(), ce.point(t).unwrap())); } eprintln!( - "truck curve: energy {:e} → {:e} in {} steps, moved {off:e} mm ({:e} mm of control points)", + "record curve: energy {:e} → {:e} in {} steps, moved {off:e} mm ({:e} mm of control points)", report.energy_before, report.energy_after, report.steps, report.movement ); - assert_eq!(faired.control_points().first().copied(), curve.control_points().first().copied()); - assert_eq!(faired.control_points().last().copied(), curve.control_points().last().copied()); + let b::Curve::Spline { points: moved, .. } = &faired else { panic!("a B-spline curve") }; + assert_eq!(moved.first(), rough.first()); + assert_eq!(moved.last(), rough.last()); assert!(off <= tolerance * (1.0 + 1e-9), "{off:e}"); assert!(report.energy_after < 1e-9 * report.energy_before); @@ -1251,25 +1214,25 @@ mod tests { let smooth = planar_net(&ku, &kv, degree, [0.0, 0.0, 0.0], [20.0, 0.0, 0.0], [0.0, 15.0, 0.0]); let mut rough = smooth.clone(); rough[2 * cu + 2][2] += 5.0; - let rows: Vec> = - (0..cu).map(|i| (0..cv).map(|j| mt::Point3::new(rough[j * cu + i][0], rough[j * cu + i][1], rough[j * cu + i][2])).collect()).collect(); - let surface = mt::BSplineSurface::try_new((mt::KnotVec::from(ku.clone()), mt::KnotVec::from(kv.clone())), rows).unwrap(); + let rows: Vec> = (0..cu).map(|i| (0..cv).map(|j| rough[j * cu + i]).collect()).collect(); + let surface = b::Surface::Spline { u_knots: ku.clone(), v_knots: kv.clone(), points: rows.clone() }; let (faired, report) = fair_bspline_surface(&surface, &FairOptions::new(1.0, 6, true, 8.0).unwrap()).unwrap(); - eprintln!("truck surface: energy {:e} → {:e}, {} steps", report.energy_before, report.energy_after, report.steps); + let b::Surface::Spline { points: moved, .. } = &faired else { panic!("a B-spline surface") }; + eprintln!("record surface: energy {:e} → {:e}, {} steps", report.energy_before, report.energy_after, report.steps); for i in 0..cu { for j in 0..cv { if i < 2 || i + 2 >= cu || j < 2 || j + 2 >= cv { - assert_eq!(faired.control_points()[i][j], surface.control_points()[i][j], "({i}, {j}) is a boundary layer and moved"); + assert_eq!(moved[i][j], rows[i][j], "({i}, {j}) is a boundary layer and moved"); } } } assert!(report.energy_after < report.energy_before); - assert!(faired.control_points()[2][2].z < surface.control_points()[2][2].z, "the bump should come down"); + assert!(moved[2][2][2] < rows[2][2][2], "the bump should come down"); } #[test] fn a_rational_surface_fairs_with_its_boundary_and_tolerance_intact() { - // The truck NURBS wrapper, on weights away from one: fairing moves the Cartesian control + // The rational record wrapper, on weights away from one: fairing moves the Cartesian control // points and keeps the weights, so the surface still cannot move more than the tolerance. let (ku, kv) = (clamped(3, 3), clamped(3, 3)); let degree = [3, 3]; @@ -1278,25 +1241,25 @@ mod tests { let mut rough = smooth.clone(); rough[2 * cu + 3][2] += 6.0; let weights: Vec = (0..cu * cv).map(|k| 0.75 + (k % 3) as f64 * 0.5).collect(); - let rows: Vec> = (0..cu) + let rows: Vec> = (0..cu) .map(|i| { (0..cv) .map(|j| { let (c, w) = (rough[j * cu + i], weights[j * cu + i]); - mt::Vector4::new(c[0] * w, c[1] * w, c[2] * w, w) + [c[0] * w, c[1] * w, c[2] * w, w] }) .collect() }) .collect(); - let bsp = mt::BSplineSurface::try_new((mt::KnotVec::from(ku.clone()), mt::KnotVec::from(kv.clone())), rows).unwrap(); - let surface = mt::NurbsSurface::new(bsp); + let surface = b::Surface::RationalSpline { u_knots: ku.clone(), v_knots: kv.clone(), homogeneous: rows.clone() }; let (faired, report) = fair_nurbs_surface(&surface, &FairOptions::new(1.0, 6, false, 0.5).unwrap()).unwrap(); eprintln!("rational surface: energy {:e} → {:e}, movement {:e} mm", report.energy_before, report.energy_after, report.movement); assert!(report.movement <= 0.5 && report.energy_after < report.energy_before); + let b::Surface::RationalSpline { homogeneous: moved, .. } = &faired else { panic!("a rational surface") }; for i in 0..cu { for j in 0..cv { - let (before, after) = (surface.non_rationalized().control_points()[i][j], faired.non_rationalized().control_points()[i][j]); - assert_eq!(after.w, before.w, "the weight of ({i}, {j}) changed"); + let (before, after) = (rows[i][j], moved[i][j]); + assert_eq!(after[3], before[3], "the weight of ({i}, {j}) changed"); if i == 0 || i + 1 == cu || j == 0 || j + 1 == cv { assert_eq!(after, before, "({i}, {j}) is on the boundary and moved"); } @@ -1405,19 +1368,43 @@ mod tests { /// and `kv`, its sides the straight lines between the surface's corners — true while the /// net's boundary rows describe lines, which is how a plane's control net is built here. fn sheet(ku: &[f64], kv: &[f64], net: &[[f64; 3]]) -> Body { + use crate::sew::{PartEdge, PartFace, PartUse, Parts, assemble}; let (cu, cv) = (ku.len() - 4, kv.len() - 4); assert_eq!(net.len(), cu * cv, "the net does not fit the knots"); - let rows: Vec> = - (0..cu).map(|i| (0..cv).map(|j| mt::Point3::new(net[j * cu + i][0], net[j * cu + i][1], net[j * cu + i][2])).collect()).collect(); - let bsp = mt::BSplineSurface::try_new((mt::KnotVec::from(ku.to_vec()), mt::KnotVec::from(kv.to_vec())), rows).unwrap(); - let corner = |i: usize, j: usize| { - let c = net[j * cu + i]; - mt::Vertex::new(mt::Point3::new(c[0], c[1], c[2])) - }; - let v = [corner(0, 0), corner(cu - 1, 0), corner(cu - 1, cv - 1), corner(0, cv - 1)]; - let line = |a: &mt::Vertex, b: &mt::Vertex| mt::Edge::new(a, b, mt::Curve::Line(mt::Line(a.point(), b.point()))); - let wire: mt::Wire = vec![line(&v[0], &v[1]), line(&v[1], &v[2]), line(&v[2], &v[3]), line(&v[3], &v[0])].into(); - crate::surfaces::surface_body(vec![mt::Face::try_new(vec![wire], mt::Surface::BSplineSurface(bsp)).unwrap()]) + let points: Vec> = (0..cu).map(|i| (0..cv).map(|j| net[j * cu + i]).collect()).collect(); + let surface = b::Surface::Spline { u_knots: ku.to_vec(), v_knots: kv.to_vec(), points }; + let corners = [(0, 0), (cu - 1, 0), (cu - 1, cv - 1), (0, cv - 1)]; + let (u, v) = ([ku[3], ku[cu]], [kv[3], kv[cv]]); + let uv = [[u[0], v[0]], [u[1], v[0]], [u[1], v[1]], [u[0], v[1]]]; + let vertices: Vec<[f64; 3]> = corners.iter().map(|&(i, j)| net[j * cu + i]).collect(); + let edges = (0..4) + .map(|k| PartEdge { + curve: b::Curve::Line { start: vertices[k], end: vertices[(k + 1) % 4] }, + interval: [0.0, 1.0], + vertices: [k as u32, ((k + 1) % 4) as u32], + }) + .collect(); + let loops = vec![ + (0..4) + .map(|k| PartUse { + edge: k as u32, + forward: true, + given: Some(b::Curve2::Spline { knots: vec![0.0, 0.0, 1.0, 1.0], points: vec![uv[k], uv[(k + 1) % 4]] }), + }) + .collect(), + ]; + let made = assemble(Parts { vertices, edges, faces: vec![PartFace { surface, forward: true, loops }], tolerance: 1e-6 }, false).unwrap(); + crate::native::body_from_smlib(&solidcraft_smlib::Brep::from_record(&made.brep, true).unwrap()).unwrap() + } + + /// Each edge of the body's record: its curve, interval and vertices. + fn edge_records(body: &Body) -> Vec<(b::Curve, [f64; 2], [u32; 2])> { + crate::native::brep_record(body).unwrap().edges.into_iter().map(|e| (e.curve, e.interval, e.vertices)).collect() + } + + /// The surface of face `i` of the body's record. + fn face_surface(body: &Body, i: usize) -> b::Surface { + crate::native::brep_record(body).unwrap().faces[i].surface.clone() } #[test] @@ -1432,12 +1419,10 @@ mod tests { rough[j * cu + i][2] += d; } let body = sheet(&ku, &kv, &rough); - let before: Vec<_> = - body.solid.face_iter().flat_map(|f| f.absolute_boundaries().iter()).flat_map(|w| w.edge_iter().map(|e| e.id())).collect(); + let before = edge_records(&body); assert!(!before.is_empty()); let (faired, report) = fair_faces(&body, &[0], &FairOptions::new(1.0, 6, false, 50.0).unwrap()).unwrap(); - let after: Vec<_> = - faired.solid.face_iter().flat_map(|f| f.absolute_boundaries().iter()).flat_map(|w| w.edge_iter().map(|e| e.id())).collect(); + let after = edge_records(&faired); eprintln!( "face: energy {:e} → {:e} in {} steps, movement {:e} mm, {} faces, history {}", report.energy_before, @@ -1452,12 +1437,12 @@ mod tests { assert!(faired.history().is_some(), "the face came from the one it was faired from"); assert!(report.energy_after < 1e-9 * report.energy_before); // The face's own surface is back on the plane, boundary rows bit for bit as they were. - let Some(mt::Surface::BSplineSurface(s)) = faired.solid.face_iter().next().map(|f| f.surface()) else { panic!("a B-spline face") }; + let b::Surface::Spline { points, .. } = face_surface(&faired, 0) else { panic!("a B-spline face") }; for j in 0..cv { for i in 0..cu { - let p = s.control_points()[i][j]; + let p = points[i][j]; let want = if i == 0 || i + 1 == cu || j == 0 || j + 1 == cv { rough[j * cu + i] } else { smooth[j * cu + i] }; - assert!(dist(&[p.x, p.y, p.z], &want) < 1e-6, "({i}, {j})"); + assert!(dist(&p, &want) < 1e-6, "({i}, {j})"); } } // A solid keeps its shells, its faces and its edges: thickening the sheet gives a closed @@ -1465,18 +1450,13 @@ mod tests { // solid of the same shape. let solid = crate::thicken(&body, 2.0, false).unwrap(); assert!(!solid.is_surface()); - let top = - (0..solid.face_count()).find(|&i| matches!(solid.solid.face_iter().nth(i).map(|f| f.surface()), Some(mt::Surface::BSplineSurface(_)))); + let top = (0..solid.face_count()).find(|&i| matches!(face_surface(&solid, i), b::Surface::Spline { .. })); let Some(top) = top else { panic!("the thickened sheet keeps its B-spline face") }; - let edges: Vec<_> = - solid.solid.face_iter().flat_map(|f| f.absolute_boundaries().iter()).flat_map(|w| w.edge_iter().map(|e| e.id())).collect(); + let edges = edge_records(&solid); let (faired, report) = fair_faces(&solid, &[top], &FairOptions::new(1.0, 6, false, 0.5).unwrap()).unwrap(); eprintln!("thickened solid: energy {:e} → {:e}, movement {:e} mm", report.energy_before, report.energy_after, report.movement); assert!(!faired.is_surface() && faired.face_count() == solid.face_count() && faired.shell_count() == solid.shell_count()); - assert_eq!( - faired.solid.face_iter().flat_map(|f| f.absolute_boundaries().iter()).flat_map(|w| w.edge_iter().map(|e| e.id())).collect::>(), - edges - ); + assert_eq!(edge_records(&faired), edges); assert!(report.energy_after < report.energy_before); // A face with no control points to fair is refused, by name. let cube = crate::box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap();