mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
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:
+229
-233
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user