kernel: surface match on the record, through bridge's record API

match_surface and seam_continuity read faces as record surfaces (SurfaceEval over the
face's domain), pick the face and its kept side's edges from the record, chain and frame
the target through bridge::chain/frames, keep the crown whose bridge strays least from the
original, and turn the bridge face to the target's sense by building it again turned
(surfaces::Parts). The history is over [body, target]: the face modified from the body's,
its edges new. Behaviour, constants and messages unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 07:26:11 -04:00
parent 7c0d9b5e82
commit 87907628e5
+229 -233
View File
@@ -16,15 +16,14 @@
//! 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 solidcraft_geom::brep::{self as b, eval::CurveEval, eval::SurfaceEval};
use crate::body::{Body, from_p3, p3};
use crate::body::Body;
use crate::bridge::{self, Continuity, Frame};
use crate::record::Rebuild;
use crate::surfaces::surface_body;
use crate::{KernelError, Result, guard};
use crate::history::{Change, EdgeProv, History};
use crate::surfaces::Chain;
use crate::{KernelError, Result};
/// Stations a match reads along the seam and along the boundary it keeps.
const STATIONS: usize = 33;
@@ -61,27 +60,31 @@ const SUPPLY_BEND_REL: f64 = 1e-1;
/// 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.
/// stay as close to the original surface as the conditions allow. Its history is over
/// `[body, target]`: the face is the body's face, modified; its edges are new.
pub fn match_surface(body: &Body, faces: &[Vec3], target: &Body, target_pick: &[Vec3], order: Continuity) -> Result<Body> {
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 (record, target_record) = (body.record(), target.record());
let face = face_near(&record, faces, body.size() * 0.05 + 1e-6)?;
let original = Patch::of(&record, face)?;
let chain = bridge::chain(target, target_pick)?;
let near = nearest_side(&face, &chain)?;
let line = chain_polyline(&target_record, &chain)?;
let near = nearest_side(&original, &line)?;
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 far_edges = side_edges(&record, face, &original, far, body.size() * 1e-6 + 1e-9)?;
let (mid_near, mid_far) = (chain_mid(&line)?, original.at(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())?;
let candidates = support_faces(target, &chain, &line, leave)?;
let supply = supply_order(target, &candidates, &chain, leave)?;
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 {}",
@@ -89,13 +92,13 @@ pub fn match_surface(body: &Body, faces: &[Vec3], target: &Body, target_pick: &[
order.as_str()
)));
}
let Some((target_face, _)) = candidates.first() else {
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)?;
let mut near_frames = bridge::frames(target, target_face, &chain, leave, STATIONS)?;
let far_frames = bridge::frames(body, 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();
@@ -103,37 +106,36 @@ pub fn match_surface(body: &Body, faces: &[Vec3], target: &Body, target_pick: &[
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));
}
// The crown is the free knob: keep the one whose surface stays closest to the original.
let mut best: Option<(f64, Body)> = 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 stray = deviation(&Patch::of(&made.record(), 0)?, &original)?;
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))
})
}
let Some((_, made)) = best else {
return Err(KernelError::Failed("match: no crown built a surface".into()));
};
// 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(&line)?;
let made_record = made.record();
let turn = match (Patch::of(&made_record, 0)?.normal_near(seam), Patch::of(&target_record, target_face)?.normal_near(seam)) {
(Some((m, _)), Some((t, _))) => m.dot(t) < 0.0,
_ => false,
};
let kept = crate::surfaces::Parts::of(&made_record, &[(0, turn)])?.canonical()?;
let sheet = crate::surfaces::surface_body(&kept.brep)?;
let history = History {
operands: 2,
faces: vec![crate::record::from_face(0, face, Change::Modified)],
edges: (0..sheet.edge_count()).map(|_| EdgeProv { faces: vec![0], origins: Vec::new() }).collect(),
};
sheet.with_history(history, &[body, target])
}
/// How well the rebuilt surface `made` meets `target` along the chain picked by `target_pick`:
@@ -143,68 +145,61 @@ pub fn match_surface(body: &Body, faces: &[Vec3], target: &Body, target_pick: &[
pub fn seam_continuity(made: &Body, target: &Body, target_pick: &[Vec3]) -> Result<(f64, f64, Continuity)> {
made.require_brep("match")?;
target.require_brep("match")?;
let (record, target_record) = (made.record(), target.record());
let chain = bridge::chain(target, target_pick)?;
let poly = chain_polyline(&chain)?;
let poly = chain_polyline(&target_record, &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)))
let mid = chain_mid(&poly)?;
let face = (0..record.faces.len())
.filter_map(|f| Patch::of(&record, f).ok().and_then(|p| p.normal_near(mid).map(|(_, d)| (d, p))))
.min_by(|a, b| a.0.total_cmp(&b.0))
.map(|(_, f)| f.clone())
.map(|(_, p)| p)
.ok_or_else(|| KernelError::Failed("match: the body has no faces".into()))?;
let near = nearest_side(&face, &chain)?;
let near = nearest_side(&face, &poly)?;
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)?)
let leave = (face.at(far, 0.5)? - mid)
.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 {
let candidates = support_faces(target, &chain, &poly, 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 target_patch = Patch::of(&target_record, target_face)?;
let frames = bridge::frames(target, 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 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 = face.at(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) = face.seam_shape(near, t)?;
if let (Some((n_made, _)), Some((n_target, _))) = (face.normal_near(p), target_patch.normal_near(p)) {
deg = deg.max(angle_deg(n_made, n_target));
}
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;
}
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))
})
}
Ok((gap, deg, order))
}
/// Which side of a face's parameter rectangle a boundary runs along: the match reads its face as
@@ -251,58 +246,89 @@ impl Side {
}
/// The point at the normalized station `t` along the side.
fn at(surf: &mt::Surface, ranges: &[(f64, f64); 2], side: Side, t: f64) -> Vec3 {
fn at(surf: &SurfaceEval, ranges: &[(f64, f64); 2], side: Side, t: f64) -> Result<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)) }
surf.point(if side.is_u() { [fixed, t] } else { [t, fixed] }).ok_or_else(|| KernelError::Failed("match: a side has no point there".into()))
}
}
/// 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])
/// A face read as a tensor patch: its record surface and its parameter box.
struct Patch {
surface: SurfaceEval,
ranges: [(f64, f64); 2],
}
/// How far `p` is from the face's own surface.
fn face_distance(face: &mt::Face, p: Vec3) -> Option<f64> {
let surf = face.surface();
let (u, v) = surf.search_nearest_parameter(p3(p), None, 100)?;
Some(from_p3(surf.subs(u, v)).dist(p))
impl Patch {
fn of(r: &b::Brep, face: usize) -> Result<Patch> {
let f = r.faces.get(face).ok_or_else(|| KernelError::Invalid(format!("no face {face}")))?;
let surface = SurfaceEval::new(&f.surface).map_err(|e| KernelError::Failed(format!("match: face {face}: {e}")))?;
let [[u0, u1], [v0, v1]] = f.domain;
Ok(Patch { surface, ranges: [(u0, u1), (v0, v1)] })
}
fn at(&self, side: Side, t: f64) -> Result<Vec3> {
Side::at(&self.surface, &self.ranges, side, t)
}
/// The surface's unit normal at the point nearest `p`, and how far `p` is from that point.
fn normal_near(&self, p: Vec3) -> Option<(Vec3, f64)> {
let uv = self.surface.project(p, None, f64::INFINITY)?;
let (q, su, sv) = self.surface.derivs(uv[0], uv[1])?;
Some((su.cross(sv).normalized()?, q.dist(p)))
}
/// The parameters of the point nearest `p`.
fn parameters(&self, p: Vec3) -> Option<[f64; 2]> {
self.surface.project(p, None, f64::INFINITY)
}
/// The shape of the 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(&self, near: Side, t: f64) -> Result<(Vec3, Vec3, Vec3, Vec3)> {
let ranges = self.ranges;
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| self.surface.point(if near.is_u() { [x, station] } else { [station, x] });
let q: Vec<Vec3> =
(0..5).map(|k| at(c0 + sgn * k as f64 * h)).collect::<Option<_>>().ok_or_else(|| KernelError::Failed("match: seam samples".into()))?;
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))
}
}
/// 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<mt::Face> {
/// The face of the record the picks lie on: they must all pick the same one, each within `reach`
/// of its surface (a pick is a point the caller clicked).
fn face_near(r: &b::Brep, picks: &[Vec3], reach: f64) -> Result<usize> {
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<mt::Face> = None;
let mut found: Option<usize> = None;
for p in picks {
let hit = body
.solid
.face_iter()
.filter_map(|f| face_distance(f, *p).map(|d| (d, f)))
let hit = (0..r.faces.len())
.filter_map(|f| Patch::of(r, f).ok().and_then(|x| x.normal_near(*p)).map(|(_, d)| (d, f)))
.min_by(|a, b| a.0.total_cmp(&b.0))
.filter(|(d, _)| *d <= tol)
.map(|(_, f)| f.clone())
.filter(|(d, _)| *d <= reach)
.map(|(_, f)| f)
.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()) {
if found.is_some_and(|f| f != hit) {
return Err(KernelError::Invalid("the picks are on different faces: match one face".into()));
}
found = Some(hit);
@@ -310,28 +336,27 @@ fn face_near(body: &Body, picks: &[Vec3]) -> Result<mt::Face> {
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<Vec<Vec3>> {
/// A chain of the record's edges as a polyline (each edge sampled the chain's way), for lengths
/// and distances.
fn chain_polyline(r: &b::Brep, chain: &Chain) -> Result<Vec<Vec3>> {
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 &(e, forward) in chain {
let edge = r.edges.get(e).ok_or_else(|| KernelError::Invalid("an edge reference".into()))?;
let c = CurveEval::new(&edge.curve).map_err(|err| KernelError::Failed(format!("match: {err}")))?;
let [t0, t1] = edge.interval;
let (a, z) = if forward { (t0, t1) } else { (t1, t0) };
for k in 0..=PER_EDGE {
out.push(from_p3(c.subs(t0 + (t1 - t0) * k as f64 / PER_EDGE as f64)));
out.push(c.point(a + (z - a) * k as f64 / PER_EDGE as f64).ok_or_else(|| KernelError::Failed("match: an edge has no point".into()))?);
}
}
Ok(out)
}
/// The point at the middle of a chain, by length.
fn chain_mid(chain: &[mt::Edge]) -> Result<Vec3> {
let poly = chain_polyline(chain)?;
/// The point at the middle of a polyline, by length.
fn chain_mid(poly: &[Vec3]) -> Result<Vec3> {
let mut cum = Vec::with_capacity(poly.len());
cum.push(0.0);
for w in poly.windows(2) {
@@ -345,10 +370,7 @@ fn chain_mid(chain: &[mt::Edge]) -> Result<Vec3> {
}
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 {
let (Some(a), Some(b), Some(&lo), Some(&up)) = (poly.get(hi - 1), poly.get(hi), cum.get(hi - 1), cum.get(hi)) else {
return Err(KernelError::Failed("match: chain".into()));
};
let span = up - lo;
@@ -368,19 +390,15 @@ fn chain_distance(poly: &[Vec3], p: Vec3) -> f64 {
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<Side> {
let surf = face.surface();
let ranges = ranges(face)?;
let poly = chain_polyline(chain)?;
/// The side of the face's own parameter rectangle the picked chain (as a polyline) sits nearest.
fn nearest_side(face: &Patch, poly: &[Vec3]) -> Result<Side> {
/// 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));
total += chain_distance(poly, face.at(side, k as f64 / (SAMPLES - 1) as f64)?);
}
let mean = total / SAMPLES as f64;
if best.is_none_or(|(b, _)| mean < b) {
@@ -392,49 +410,48 @@ fn nearest_side(face: &mt::Face, chain: &[mt::Edge]) -> Result<Side> {
/// 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<Vec<mt::Edge>> {
let surf = face.surface();
let ranges = ranges(face)?;
let (fixed, along) = side.fixed_and_along(ranges[0], ranges[1]);
fn side_edges(r: &b::Brep, face: usize, patch: &Patch, side: Side, tol: f64) -> Result<Chain> {
let (fixed, along) = side.fixed_and_along(patch.ranges[0], patch.ranges[1]);
let band = (along.1 - along.0).abs().max(1e-12) * 1e-3;
let mut pool: Vec<mt::Edge> = 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());
let f = r.faces.get(face).ok_or_else(|| KernelError::Invalid(format!("no face {face}")))?;
let mut pool: Vec<usize> = Vec::new();
for l in &f.loops {
let b::Loop::Edges { edges } = l else { continue };
for u in edges {
let e = u.edge as usize;
let Some(edge) = r.edges.get(e) else { continue };
let Ok(c) = CurveEval::new(&edge.curve) else { continue };
let Some(p) = c.point(0.5 * (edge.interval[0] + edge.interval[1])) else { continue };
let Some(uv) = patch.parameters(p) else { continue };
if patch.surface.point(uv).is_none_or(|q| q.dist(p) > tol) {
continue;
}
let got = if side.is_u() { uv[0] } else { uv[1] };
if (got - fixed).abs() <= band && !pool.contains(&e) {
pool.push(e);
}
}
}
if pool.is_empty() {
return Err(KernelError::Invalid("the face has no edges along the boundary a match would keep".into()));
}
let ends = |e: usize| r.edges.get(e).map(|x| x.vertices).unwrap_or([u32::MAX; 2]);
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 mut chain: Chain = vec![(first, true)];
let [mut start, mut end] = ends(first);
while let Some(k) = pool.iter().position(|e| ends(*e).contains(&end)) {
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 [a, z] = ends(e);
let forward = a == end;
end = if forward { z } else { a };
chain.push((e, forward));
}
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()) {
while let Some(k) = pool.iter().position(|e| ends(*e).contains(&start)) {
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);
let [a, z] = ends(e);
let forward = z == start;
start = if forward { a } else { z };
chain.insert(0, (e, forward));
}
if !pool.is_empty() {
return Err(KernelError::Invalid("the face's edges along that boundary do not run on from one another".into()));
@@ -442,23 +459,26 @@ fn side_edges(face: &mt::Face, side: Side, tol: f64) -> Result<Vec<mt::Edge>> {
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<Vec<(mt::Face, f64)>> {
let mid = chain_mid(chain)?;
/// The faces of `target` that bound every edge of 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: &Chain, poly: &[Vec3], leave: Vec3) -> Result<Vec<(usize, f64)>> {
let r = target.record();
let mid = chain_mid(poly)?;
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())) {
let bounds = |f: usize, e: usize| r.edges.get(e).is_some_and(|x| x.uses.iter().any(|u| u.face as usize == f));
let mut out: Vec<(usize, f64)> = Vec::new();
for f in 0..r.faces.len() {
if !chain.iter().all(|(e, _)| bounds(f, *e)) {
continue;
}
let Some((n, d)) = normal_near(face, mid) else { continue };
let Some((n, d)) = Patch::of(&r, f).ok().and_then(|p| p.normal_near(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));
out.push((f, score));
}
}
if out.is_empty() {
@@ -474,25 +494,29 @@ fn support_faces(target: &Body, chain: &[mt::Edge], leave: Vec3) -> Result<Vec<(
/// 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<Continuity> {
let Some((first, best)) = candidates.first() else {
fn supply_order(target: &Body, candidates: &[(usize, f64)], chain: &Chain, leave: Vec3) -> Result<Continuity> {
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 keep: Vec<usize> = 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<Frame> = bridge::frames(first, chain, leave, STATIONS)?;
for other in keep.iter().skip(1) {
let r = target.record();
let size = target.size();
let first_patch = Patch::of(&r, first)?;
let primary: Vec<Frame> = bridge::frames(target, 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 Ok(other_frames) = bridge::frames(target, other, chain, leave, STATIONS) else { continue };
let other_patch = Patch::of(&r, other)?;
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)) {
if let (Some((na, _)), Some((nb, _))) = (first_patch.normal_near(a.p), other_patch.normal_near(b.p)) {
worst_deg = worst_deg.max(angle_deg(na, nb));
}
let scale = a.bend.len().max(b.bend.len()).max(1.0 / size);
@@ -544,46 +568,18 @@ fn shape(d1: Vec3, d2: Vec3, d3: Vec3) -> Result<(Vec3, Vec3, Vec3)> {
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<Vec3> = (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<f64> {
let (surf, os) = (made.surface(), original.surface());
let ranges = ranges(made)?;
fn deviation(made: &Patch, original: &Patch) -> Result<f64> {
let ranges = made.ranges;
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);
let Some(q) = made.surface.point([u, v]) else { continue };
let Some((_, d)) = original.normal_near(q) else { continue };
if d.is_finite() {
sum += d * d;
n += 1;