//! Surface matching: rebuild a surface body's face so that one boundary of it meets a picked //! edge chain of another body up to a requested continuity (G1, G2 or G3), while the boundary //! opposite it does not move. //! //! A match is the bridge core ([`crate::bridge`]) pointed at a different pair of boundaries. One //! end reads its position, tangent, curvature and change of curvature from the *target's* face //! along the picked chain — that is the end that moves. The other end reads them from the body's //! own surface along the boundary opposite it, built through the original boundary's own points: //! a match may shape the middle, never that seam. In between the cross-sections are the bridge's //! cubic Hermites, whose crown is free, so the crown whose surface stays closest to the original //! one — sampled on a grid and measured against it — is the one kept: a match moves the seam and //! as little else as the conditions allow. //! //! Which boundary of the face moves is read off the geometry: the one nearest the picked chain. //! The order a match may ask for is read off the target the same way: a chain that several faces //! of the target meet, with shapes that part company, is a seam the target itself does not hold //! beyond that order, and no surface can be told to carry a condition the target has not got. use mt::{BoundedCurve, ParametricCurve, ParametricSurface, ParametricSurface3D, SearchNearestParameter}; use solidcraft_geom::Vec3; use truck_modeling as mt; use crate::body::{Body, from_p3, p3}; use crate::bridge::{self, Continuity, Frame}; use crate::record::Rebuild; use crate::surfaces::surface_body; use crate::{KernelError, Result, guard}; /// Stations a match reads along the seam and along the boundary it keeps. const STATIONS: usize = 33; /// Samples each way of the grid the rebuilt surface is compared with the original on. const GRID: usize = 7; /// The crowns the match tries at each end: [`crate::bridge::bridge`]'s free knob, which shapes /// the middle without touching either end's conditions. const CROWNS: [f64; 5] = [-0.5, 0.0, 0.5, 1.0, 2.0]; /// Points a caller may pick one face with. const MAX_PICKS: usize = 64; /// Samples per edge of a chain, for lengths and distances. const PER_EDGE: usize = 32; /// A seam is credited with G1 when its normals agree this closely (degrees), with G2 when the /// two curvature vectors agree within this fraction of the larger of them, and with G3 when the /// change of curvature agrees within this fraction of the curvature over the body's size. const G1_DEG: f64 = 0.05; const G2_REL: f64 = 5e-3; const G3_REL: f64 = 5e-2; /// Two faces of the target count as meeting each other at the picked chain when their normals /// agree this closely, their curvature within this fraction of the larger, and their change of /// curvature within this fraction of the curvature over the body's size. const SUPPLY_DEG: f64 = 0.05; const SUPPLY_REL: f64 = 2e-2; const SUPPLY_BEND_REL: f64 = 1e-1; /// Rebuild the face of `body` picked by `faces` (points on it) so that its boundary nearest the /// chain of `target`'s edges picked by `target_pick` takes that chain's place and repeats the /// target's own shape along it up to `order` — position, tangent, curvature vector, and the /// change of that curvature vector along the direction the match leaves in. The boundary /// opposite keeps its place and the original surface's own frame, and the middle is shaped to /// stay as close to the original surface as the conditions allow. pub fn match_surface(body: &Body, faces: &[Vec3], target: &Body, target_pick: &[Vec3], order: Continuity) -> Result { body.require_brep("match")?; target.require_brep("match")?; if !body.is_surface() { return Err(KernelError::Invalid("match rebuilds a surface body: this one is closed".into())); } let face = face_near(body, faces)?; let chain = bridge::chain(target, target_pick)?; let near = nearest_side(&face, &chain)?; let far = near.opposite(); let far_edges = side_edges(&face, far, body.size() * 1e-6 + 1e-9)?; let (mid_near, mid_far) = (chain_mid(&chain)?, Side::at(&face.surface(), &ranges(&face)?, far, 0.5)); let leave = (mid_far - mid_near) .normalized() .filter(|l| l.is_finite()) .ok_or_else(|| KernelError::Invalid("the far boundary sits on the picked chain: there is nothing to match".into()))?; // Which face of the target the match continues from, and how much of the requested order the // target itself holds along that chain. let candidates = support_faces(target, &chain, leave)?; let supply = supply_order(&candidates, &chain, leave, target.size())?; if order.conditions() > supply.conditions() { return Err(KernelError::Invalid(format!( "the target's own surfaces part company at the picked edges: they hold only {} there, so a seam cannot hold {}", supply.as_str(), order.as_str() ))); } let Some((target_face, _)) = candidates.first() else { return Err(KernelError::Invalid("no face of the target holds the picked edges".into())); }; // The stations of the seam: what the target does there along the direction the match leaves // in, and what the far boundary does coming back the other way. let mut near_frames = bridge::frames(target_face, &chain, leave, STATIONS)?; let far_frames = bridge::frames(&face, &far_edges, leave * -1.0, STATIONS)?; // The two chains are sampled from their own starts: turn one round when that pairs the // stations better (a match is built station to station). let direct: f64 = near_frames.iter().zip(&far_frames).map(|(a, b)| a.p.dist(b.p)).sum(); let reversed: f64 = near_frames.iter().zip(far_frames.iter().rev()).map(|(a, b)| a.p.dist(b.p)).sum(); if reversed < direct { near_frames.reverse(); } guard("surface match", || { // The crown is the free knob: keep the one whose surface stays closest to the original. let mut best: Option<(f64, mt::Face)> = None; for crown_near in CROWNS { for crown_far in CROWNS { let made = bridge::bridge(&near_frames, &far_frames, [order, Continuity::G1], [crown_near, crown_far])?; let Some(made) = made.solid.face_iter().next().cloned() else { return Err(KernelError::Failed("match: the bridge made no face".into())); }; let stray = deviation(&made, &face)?; if best.as_ref().is_none_or(|(b, _)| stray < *b) { best = Some((stray, made)); } } } let Some((_, made)) = best else { return Err(KernelError::Failed("match: no crown built a surface".into())); }; let mut kept = made; // The rebuilt face takes the target's own sense at the seam: the two surfaces then meet // the same way round, which is what a stitch, a thicken or a display of the result reads. let seam = chain_mid(&chain)?; if let (Some((made_n, _)), Some((target_n, _))) = (normal_near(&kept, seam), normal_near(target_face, seam)) && made_n.dot(target_n) < 0.0 { kept.invert(); } let mut rec = Rebuild::default(); rec.face(&kept, &face); Ok(crate::record::rebuilt_body(surface_body(vec![kept]), &[&body.solid, &target.solid], &rec)) }) } /// How well the rebuilt surface `made` meets `target` along the chain picked by `target_pick`: /// the worst distance from the seam to that chain, the worst angle between the two surfaces' /// normals across it (degrees), and the highest continuity the seam holds /// ([`Continuity::G0`] when even the normals disagree). pub fn seam_continuity(made: &Body, target: &Body, target_pick: &[Vec3]) -> Result<(f64, f64, Continuity)> { made.require_brep("match")?; target.require_brep("match")?; let chain = bridge::chain(target, target_pick)?; let poly = chain_polyline(&chain)?; // The face whose boundary runs along the chain: the one nearest it, so that a body that has // not been matched yet — where the seam is still some way off — can be measured too. let mid = chain_mid(&chain)?; let face = made .solid .face_iter() .filter_map(|f| face_distance(f, mid).map(|d| (d, f))) .min_by(|a, b| a.0.total_cmp(&b.0)) .map(|(_, f)| f.clone()) .ok_or_else(|| KernelError::Failed("match: the body has no faces".into()))?; let near = nearest_side(&face, &chain)?; let far = near.opposite(); let ranges = ranges(&face)?; let mid_far = Side::at(&face.surface(), &ranges, far, 0.5); let leave = (mid_far - chain_mid(&chain)?) .normalized() .ok_or_else(|| KernelError::Invalid("the seam and the far boundary coincide: there is nothing to measure".into()))?; let candidates = support_faces(target, &chain, leave)?; let Some((target_face, _)) = candidates.first() else { return Err(KernelError::Invalid("no face of the target holds the picked edges".into())); }; let frames = bridge::frames(target_face, &chain, leave, STATIONS)?; let size = target.size(); guard("surface match", || { let surf = face.surface(); let (mut gap, mut deg, mut curv, mut bend) = (0.0f64, 0.0f64, 0.0f64, 0.0f64); for i in 0..STATIONS { let t = i as f64 / (STATIONS - 1) as f64; let p = Side::at(&surf, &ranges, near, t); gap = gap.max(chain_distance(&poly, p)); // What the target says there, and what the rebuilt surface actually does there. let Some(frame) = frames.iter().min_by(|a, b| a.p.dist(p).total_cmp(&b.p.dist(p))) else { return Err(KernelError::Failed("match: the target has no frames".into())); }; let (_, t_made, bend_made, change_made) = seam_shape(&surf, &ranges, near, t)?; if let Some((n_made, _)) = normal_near(&face, p) && let Some((n_target, _)) = normal_near(target_face, p) { deg = deg.max(angle_deg(n_made, n_target)); } let scale = frame.bend.len().max(bend_made.len()).max(1.0 / size); curv = curv.max((bend_made - frame.bend).len() / scale); // The change of curvature is what a surface can hold: only its normal part counts, // and it turns round with the direction of travel. let change = if t_made.dot(frame.leave) < 0.0 { change_made * -1.0 } else { change_made }; let scale3 = frame.bend_change.len().max(change.len()).max(scale / size); bend = bend.max((change - frame.bend_change).len() / scale3); } let mut order = Continuity::G0; if deg <= G1_DEG { order = Continuity::G1; if curv <= G2_REL { order = Continuity::G2; if bend <= G3_REL { order = Continuity::G3; } } } Ok((gap, deg, order)) }) } /// Which side of a face's parameter rectangle a boundary runs along: the match reads its face as /// a tensor patch, so its four sides are its own parameter lines. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Side { /// The `u = u0` side (its points run along `v`). U0, /// The `u = u1` side. U1, /// The `v = v0` side (its points run along `u`). V0, /// The `v = v1` side. V1, } impl Side { /// The four sides, in a fixed order. const ALL: [Side; 4] = [Side::U0, Side::U1, Side::V0, Side::V1]; /// The side opposite this one: what a match keeps when this one moves. fn opposite(self) -> Side { match self { Side::U0 => Side::U1, Side::U1 => Side::U0, Side::V0 => Side::V1, Side::V1 => Side::V0, } } /// Whether this side pins `u` (its points run along `v`). fn is_u(self) -> bool { matches!(self, Side::U0 | Side::U1) } /// The value this side pins, and the range the side runs over. fn fixed_and_along(self, u: (f64, f64), v: (f64, f64)) -> (f64, (f64, f64)) { match self { Side::U0 => (u.0, v), Side::U1 => (u.1, v), Side::V0 => (v.0, u), Side::V1 => (v.1, u), } } /// The point at the normalized station `t` along the side. fn at(surf: &mt::Surface, ranges: &[(f64, f64); 2], side: Side, t: f64) -> Vec3 { let (u, v) = (ranges[0], ranges[1]); let (fixed, along) = side.fixed_and_along(u, v); let t = along.0 + (along.1 - along.0) * t.clamp(0.0, 1.0); if side.is_u() { from_p3(surf.subs(fixed, t)) } else { from_p3(surf.subs(t, fixed)) } } } /// The bounded parameter ranges of a face's surface. fn ranges(face: &mt::Face) -> Result<[(f64, f64); 2]> { let surf = face.surface(); let (Some(u), Some(v)) = surf.try_range_tuple() else { return Err(KernelError::Failed("match: the face's surface is unbounded".into())); }; Ok([u, v]) } /// How far `p` is from the face's own surface. fn face_distance(face: &mt::Face, p: Vec3) -> Option { let surf = face.surface(); let (u, v) = surf.search_nearest_parameter(p3(p), None, 100)?; Some(from_p3(surf.subs(u, v)).dist(p)) } /// The surface's unit normal at the point nearest `p`, and how far `p` is from that point. fn normal_near(face: &mt::Face, p: Vec3) -> Option<(Vec3, f64)> { let surf = face.surface(); let (u, v) = surf.search_nearest_parameter(p3(p), None, 100)?; let n = surf.normal(u, v); let n = Vec3::new(n.x, n.y, n.z).normalized()?; Some((n, from_p3(surf.subs(u, v)).dist(p))) } /// The face of `body` the picks lie on: they must all pick the same one. A pick is a point the /// caller clicked, so the face nearest it within the body's own pick tolerance is the face it /// means (`crate::surfaces::edge_near` reads points the same way). fn face_near(body: &Body, picks: &[Vec3]) -> Result { if picks.is_empty() || picks.len() > MAX_PICKS || picks.iter().any(|p| !p.is_finite()) { return Err(KernelError::Invalid(format!("pick 1 to {MAX_PICKS} points on the face to match"))); } let tol = body.size() * 0.05 + 1e-6; let mut found: Option = None; for p in picks { let hit = body .solid .face_iter() .filter_map(|f| face_distance(f, *p).map(|d| (d, f))) .min_by(|a, b| a.0.total_cmp(&b.0)) .filter(|(d, _)| *d <= tol) .map(|(_, f)| f.clone()) .ok_or_else(|| KernelError::Invalid(format!("no face of the body passes through {p:?}")))?; if found.as_ref().is_some_and(|f| f.id() != hit.id()) { return Err(KernelError::Invalid("the picks are on different faces: match one face".into())); } found = Some(hit); } found.ok_or_else(|| KernelError::Invalid("pick a point on the face to match".into())) } /// A chain of edges as a polyline (each edge sampled), for lengths and distances. fn chain_polyline(chain: &[mt::Edge]) -> Result> { if chain.is_empty() { return Err(KernelError::Invalid("the chain has no edges".into())); } let mut out = Vec::with_capacity(chain.len() * (PER_EDGE + 1)); for e in chain { let c = e.curve(); let (t0, t1) = c.range_tuple(); if !(t1 > t0) || ![t0, t1].into_iter().all(f64::is_finite) { return Err(KernelError::Failed("match: an edge has no parameter range".into())); } for k in 0..=PER_EDGE { out.push(from_p3(c.subs(t0 + (t1 - t0) * k as f64 / PER_EDGE as f64))); } } Ok(out) } /// The point at the middle of a chain, by length. fn chain_mid(chain: &[mt::Edge]) -> Result { let poly = chain_polyline(chain)?; let mut cum = Vec::with_capacity(poly.len()); cum.push(0.0); for w in poly.windows(2) { let (Some(a), Some(b)) = (w.first(), w.get(1)) else { continue }; let Some(last) = cum.last().copied() else { break }; cum.push(last + a.dist(*b)); } let total = cum.last().copied().unwrap_or(0.0); if !(total > 1e-9) || !total.is_finite() { return Err(KernelError::Invalid("the picked chain has no length".into())); } let half = total * 0.5; let hi = cum.partition_point(|x| *x < half).clamp(1, poly.len().saturating_sub(1)); let (Some(a), Some(b)) = (poly.get(hi - 1), poly.get(hi)) else { return Err(KernelError::Failed("match: chain".into())); }; let (Some(&lo), Some(&up)) = (cum.get(hi - 1), cum.get(hi)) else { return Err(KernelError::Failed("match: chain".into())); }; let span = up - lo; let t = if span > 1e-12 { (half - lo) / span } else { 0.0 }; Ok(*a + (*b - *a) * t) } /// How far `p` is from a polyline. fn chain_distance(poly: &[Vec3], p: Vec3) -> f64 { let mut best = f64::INFINITY; for w in poly.windows(2) { let (Some(a), Some(b)) = (w.first(), w.get(1)) else { continue }; let d = b.dist(*a); let t = if d > 1e-12 { ((p - *a).dot(*b - *a) / (d * d)).clamp(0.0, 1.0) } else { 0.0 }; best = best.min(p.dist(*a + (*b - *a) * t)); } best } /// The side of the face's own parameter rectangle the picked chain sits nearest. fn nearest_side(face: &mt::Face, chain: &[mt::Edge]) -> Result { let surf = face.surface(); let ranges = ranges(face)?; let poly = chain_polyline(chain)?; /// Samples of each side used to tell which one the chain runs along. const SAMPLES: usize = 17; let mut best: Option<(f64, Side)> = None; for side in Side::ALL { let mut total = 0.0; for k in 0..SAMPLES { let t = k as f64 / (SAMPLES - 1) as f64; total += chain_distance(&poly, Side::at(&surf, &ranges, side, t)); } let mean = total / SAMPLES as f64; if best.is_none_or(|(b, _)| mean < b) { best = Some((mean, side)); } } best.map(|(_, s)| s).ok_or_else(|| KernelError::Failed("match: the face has no sides".into())) } /// The face's own edges that run along a side of its parameter rectangle, chained head to tail. /// A match keeps the far side's own edges, so they must be a chain that runs on. fn side_edges(face: &mt::Face, side: Side, tol: f64) -> Result> { let surf = face.surface(); let ranges = ranges(face)?; let (fixed, along) = side.fixed_and_along(ranges[0], ranges[1]); let band = (along.1 - along.0).abs().max(1e-12) * 1e-3; let mut pool: Vec = Vec::new(); for e in face.edge_iter() { let c = e.curve(); let (t0, t1) = c.range_tuple(); let p = from_p3(c.subs(0.5 * (t0 + t1))); let Some((u, v)) = surf.search_nearest_parameter(p3(p), None, 100) else { continue }; if from_p3(surf.subs(u, v)).dist(p) > tol { continue; } let got = if side.is_u() { u } else { v }; if (got - fixed).abs() <= band { pool.push(e.clone()); } } if pool.is_empty() { return Err(KernelError::Invalid("the face has no edges along the boundary a match would keep".into())); } let first = pool.remove(0); let mut chain = vec![first]; let mut end = match chain.first() { Some(e) => e.back().clone(), None => return Err(KernelError::Failed("match: chain".into())), }; while let Some(k) = pool.iter().position(|e| e.front().id() == end.id() || e.back().id() == end.id()) { let e = pool.remove(k); let e = if e.front().id() == end.id() { e } else { e.inverse() }; end = e.back().clone(); chain.push(e); } let mut start = match chain.first() { Some(e) => e.front().clone(), None => return Err(KernelError::Failed("match: chain".into())), }; while let Some(k) = pool.iter().position(|e| e.front().id() == start.id() || e.back().id() == start.id()) { let e = pool.remove(k); let e = if e.back().id() == start.id() { e } else { e.inverse() }; start = e.front().clone(); chain.insert(0, e); } if !pool.is_empty() { return Err(KernelError::Invalid("the face's edges along that boundary do not run on from one another".into())); } Ok(chain) } /// The faces of `target` that meet the chain and whose tangent plane holds the leave direction, /// best first: the face a match continues from, and the ones it might have been read from. fn support_faces(target: &Body, chain: &[mt::Edge], leave: Vec3) -> Result> { let mid = chain_mid(chain)?; let tol = target.size() * 1e-6 + 1e-9; let mut out: Vec<(mt::Face, f64)> = Vec::new(); for face in target.solid.face_iter() { if !chain.iter().all(|e| face.edge_iter().any(|f| f.id() == e.id())) { continue; } let Some((n, d)) = normal_near(face, mid) else { continue }; if d > tol { continue; } let score = (leave - n * leave.dot(n)).len(); if score.is_finite() && score >= 0.2 { out.push((face.clone(), score)); } } if out.is_empty() { return Err(KernelError::Invalid( "no face of the target holds the picked edges in the direction the match leaves in: pick the chain of a face the surface can continue" .into(), )); } out.sort_by(|a, b| b.1.total_cmp(&a.1)); Ok(out) } /// The highest order the target itself holds along the chain: every face that meets those edges /// and whose tangent plane holds the leave direction reads the chain's shape from its own /// surface, and where two of them part company the target's own geometry jumps at the seam. fn supply_order(candidates: &[(mt::Face, f64)], chain: &[mt::Edge], leave: Vec3, size: f64) -> Result { let Some((first, best)) = candidates.first() else { return Err(KernelError::Invalid("no face of the target holds the picked edges".into())); }; let keep: Vec<&mt::Face> = candidates.iter().filter(|(_, s)| *s >= 0.5 * best).map(|(f, _)| f).collect(); let mut order = Continuity::G3; if keep.len() < 2 { return Ok(order); } let primary: Vec = bridge::frames(first, chain, leave, STATIONS)?; for other in keep.iter().skip(1) { // A face that cannot be read along the whole chain says nothing here: the face the match // is built on is the one that must be able to supply it. let Ok(other_frames) = bridge::frames(other, chain, leave, STATIONS) else { continue }; let mut worst_deg = 0.0f64; let mut worst = 0.0f64; let mut worst_change = 0.0f64; for (a, b) in primary.iter().zip(&other_frames) { if let (Some((na, _)), Some((nb, _))) = (normal_near(first, a.p), normal_near(other, b.p)) { worst_deg = worst_deg.max(angle_deg(na, nb)); } let scale = a.bend.len().max(b.bend.len()).max(1.0 / size); worst = worst.max((a.bend - b.bend).len() / scale); let scale3 = a.bend_change.len().max(b.bend_change.len()).max(scale / size); worst_change = worst_change.max((a.bend_change - b.bend_change).len() / scale3); } if worst_deg > SUPPLY_DEG { order = Continuity::G0; } if worst > SUPPLY_REL && order.conditions() > 1 { order = Continuity::G1; } if worst_change > SUPPLY_BEND_REL && order.conditions() > 2 { order = Continuity::G2; } } Ok(order) } /// The acute angle between two directions, in degrees. For two surfaces' normals this is the /// angle between their tangent planes: a sheet has no inside, so which way a normal points says /// nothing about whether the surfaces meet smoothly. fn angle_deg(a: Vec3, b: Vec3) -> f64 { let d = a.dot(b) / (a.len() * b.len()); if !d.is_finite() { return f64::NAN; } d.abs().clamp(0.0, 1.0).acos().to_degrees() } /// The geometric shape of a surface curve at a point from its first three derivatives along the /// curve: the same statement [`crate::bridge`] makes of a support surface — the unit tangent, the /// curvature vector, and the arclength derivative of that curvature vector. fn shape(d1: Vec3, d2: Vec3, d3: Vec3) -> Result<(Vec3, Vec3, Vec3)> { let sigma = d1.len(); if !(sigma > 1e-12) || !sigma.is_finite() { return Err(KernelError::Failed("match: the seam's cross-section does not move".into())); } let t = d1 * (1.0 / sigma); let tp = d2 * (1.0 / sigma) - d1 * (d1.dot(d2) / (sigma * sigma * sigma)); let nn = d2 - t * d2.dot(t); let np = d3 - t * d3.dot(t) - tp * d2.dot(t) - t * d2.dot(tp); let kappa = np * (sigma * sigma) - nn * (2.0 * d1.dot(d2)); let change = kappa * (1.0 / (sigma.powi(5))); if ![t, nn, change].into_iter().all(Vec3::is_finite) { return Err(KernelError::Failed("match: the seam's shape is not finite".into())); } Ok((t, nn * (1.0 / (sigma * sigma)), change)) } /// The shape of a face's surface along its cross direction at the station `t`, read inwards from /// the boundary `near` over five one-sided samples: the point, the unit tangent, the curvature /// vector and the change of that curvature. The same finite difference [`crate::bridge::frames`] /// reads a support surface with, so that a rebuilt seam can be compared with what it matched. fn seam_shape(surf: &mt::Surface, ranges: &[(f64, f64); 2], near: Side, t: f64) -> Result<(Vec3, Vec3, Vec3, Vec3)> { let (station, cross, at_start) = if near.is_u() { (ranges[1].0 + (ranges[1].1 - ranges[1].0) * t, ranges[0], near == Side::U0) } else { (ranges[0].0 + (ranges[0].1 - ranges[0].0) * t, ranges[1], near == Side::V0) }; let span = (cross.1 - cross.0).abs().max(1e-12); let h = span * 1e-3; let c0 = if at_start { cross.0 } else { cross.1 }; let sgn = if at_start { 1.0 } else { -1.0 }; let at = |x: f64| if near.is_u() { from_p3(surf.subs(x, station)) } else { from_p3(surf.subs(station, x)) }; let q: Vec = (0..5).map(|k| at(c0 + sgn * k as f64 * h)).collect(); let (Some(&q0), Some(&q1), Some(&q2), Some(&q3), Some(&q4)) = (q.first(), q.get(1), q.get(2), q.get(3), q.get(4)) else { return Err(KernelError::Failed("match: seam samples".into())); }; let d1 = (q0 * -25.0 + q1 * 48.0 + q2 * -36.0 + q3 * 16.0 + q4 * -3.0) * (sgn / (12.0 * h)); let d2 = (q0 * 35.0 + q1 * -104.0 + q2 * 114.0 + q3 * -56.0 + q4 * 11.0) * (1.0 / (12.0 * h * h)); let d3 = (q0 * -5.0 + q1 * 18.0 + q2 * -24.0 + q3 * 14.0 + q4 * -3.0) * (sgn / (2.0 * h * h * h)); let (t, bend, change) = shape(d1, d2, d3)?; Ok((q0, t, bend, change)) } /// How far the rebuilt surface strays from the original one: the root mean square of the /// distances from a grid of its samples to the original surface. fn deviation(made: &mt::Face, original: &mt::Face) -> Result { let (surf, os) = (made.surface(), original.surface()); let ranges = ranges(made)?; let mut sum = 0.0f64; let mut n = 0usize; for i in 0..GRID { for j in 0..GRID { let u = ranges[0].0 + (ranges[0].1 - ranges[0].0) * i as f64 / (GRID - 1) as f64; let v = ranges[1].0 + (ranges[1].1 - ranges[1].0) * j as f64 / (GRID - 1) as f64; let q = from_p3(surf.subs(u, v)); let Some((a, b)) = os.search_nearest_parameter(p3(q), None, 100) else { continue }; let d = from_p3(os.subs(a, b)).dist(q); if d.is_finite() { sum += d * d; n += 1; } } } if n == 0 { return Err(KernelError::Failed("match: the original surface cannot be sampled".into())); } Ok((sum / n as f64).sqrt()) } #[cfg(test)] #[path = "matchsurf_tests.rs"] mod tests;