kernel: the general fill on loops of five or more edges, its patches about a centre

general_fill takes loops of three to sixteen edges. quilt_body assembles fill_iso's
patches into one sheet: each loop edge whole, or for five or more its two halves at the
middle of its interval, both on the edge's exact curve record and sharing a midpoint
vertex; each spoke a new edge from that vertex to the centre; every pcurve the straight
line in (u, v) of its side. The history marks every face generated, each edge or half a
piece of the body's own edge, each spoke new. A reversed loop edge's knots keep the
interval's ends exactly, so pieces name the edge's own vertices.

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:22:56 -04:00
parent 1f8c91873a
commit 7c0d9b5e82
+153 -15
View File
@@ -16,7 +16,7 @@ use solidcraft_geom::brep::eval::SurfaceEval;
use solidcraft_geom::brep::{self as b, Curve2, eval::CurveEval};
use solidcraft_geom::bspline::{HPoint, SplineError};
use solidcraft_geom::fill_general::{Across, MAX_FILL_POINTS};
use solidcraft_geom::fill_iso::{IsoEdge, IsoSpec};
use solidcraft_geom::fill_iso::{IsoEdge, IsoFill, IsoSpec};
use crate::body::Body;
use crate::bridge::Continuity;
@@ -139,8 +139,12 @@ fn general_fill(
points: &[Vec3],
crown: f64,
) -> Result<(Body, PatchReport)> {
if !(3..=4).contains(&chain.len()) {
return Err(KernelError::Invalid(format!("not supported yet: a general patch on a loop of {} edges (three or four)", chain.len())));
if !(3..=solidcraft_geom::fill_iso::MAX_ISO_EDGES).contains(&chain.len()) {
return Err(KernelError::Invalid(format!(
"not supported yet: a general patch on a loop of {} edges (three to {})",
chain.len(),
solidcraft_geom::fill_iso::MAX_ISO_EDGES
)));
}
let edges: Vec<IsoEdge> = chain.iter().map(|&(e, forward)| iso_edge(record, e, forward)).collect::<Result<_>>()?;
let size = body.size().max(solidcraft_geom::LINEAR_TOL);
@@ -169,8 +173,7 @@ fn general_fill(
};
let spec = IsoSpec { tol: face_tolerance(body), angle_tol: PATCH_ANGLE_TOL_DEG.to_radians(), crown, points };
let fill = solidcraft_geom::fill_iso::iso_fill(&edges, &mut across, &clash, &spec).map_err(spline_error)?;
let surface = surface_record(&fill.surface)?;
let made = patch_body(body, record, chain, surface, true, &Pcurves::Sides(&fill.sides), face_tolerance(body))?;
let made = quilt_body(body, record, chain, &fill, face_tolerance(body))?;
let a = fill.achieved;
let report = PatchReport {
general: true,
@@ -221,6 +224,15 @@ fn iso_edge(record: &b::Brep, e: usize, forward: bool) -> Result<IsoEdge> {
if !forward {
knots = knots.iter().rev().map(|k| a + z - k).collect();
points.reverse();
// The interval's ends exactly (the reflection's rounding), so a piece's ends name them.
let n = knots.len();
for (i, k) in knots.iter_mut().enumerate() {
if i <= degree {
*k = a;
} else if i + degree + 1 >= n {
*k = z;
}
}
}
// Its ends exactly on the loop's vertices.
let vertex = |end: usize| record.vertices.get(edge.vertices[end] as usize).map(|v| v3(&v.point));
@@ -536,12 +548,11 @@ fn conic_on(frame: &b::Frame, radii: [f64; 2], on: &dyn Fn([f64; 3]) -> bool, n:
on(frame.origin) && radii[0] * axis(frame.x_axis) + radii[1] * axis(frame.y_axis) <= tol
}
/// Where a patch's pcurves come from: the record's routine (a plane), the exact projection onto
/// a height field's plane, or the patch's own sides (each edge a straight line in (u, v)).
/// Where a patch's pcurves come from: the record's routine (a plane), or the exact projection
/// onto a height field's plane.
enum Pcurves<'a> {
Computed,
Projected(&'a Projection),
Sides(&'a [[[f64; 2]; 2]]),
}
/// The plane a height field stands on: its parameters are coordinates along `axes` from `origin`.
@@ -629,13 +640,6 @@ fn patch_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], surface: b
}
}
let given = match pcurves {
Pcurves::Sides(sides) => {
let [s, z] = sides.get(k).copied().ok_or_else(|| fail("side"))?;
let [a, b] = edge.interval;
// The loop's start of the edge is its interval's start when the loop runs along it.
let (at_a, at_b) = if forward { (s, z) } else { (z, s) };
Some(Curve2::Spline { knots: vec![a, a, b, b], points: vec![at_a, at_b] })
}
Pcurves::Projected(p) => Some(p.pcurve(&edge.curve, edge.interval).ok_or_else(|| {
KernelError::Invalid(
"not supported yet: a fitted patch on a circular, elliptic or traced edge (it has no exact pcurve on the fit)".into(),
@@ -669,6 +673,140 @@ fn patch_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], surface: b
patch.with_history(history, &[body])
}
/// The sheet of the general fill's patches on the loop `chain` of the record's edges: each edge
/// whole, or (a loop of five or more) its two halves at the middle of its interval, both on its
/// exact curve record; the spokes from the halves' common vertex to the centre; every pcurve the
/// straight line in (u, v) the patch gives its side. Its history is over `[body]`: every face is
/// new, each loop edge (or half) a piece of the body's own, each spoke new.
fn quilt_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], fill: &IsoFill, tol: f64) -> Result<Body> {
use solidcraft_geom::fill_iso::IsoSide;
let fail = |m: &str| KernelError::Failed(format!("patch: {m}"));
let index = |n: usize| u32::try_from(n).map_err(|_| fail("budget"));
let mut vertices: Vec<[f64; 3]> = Vec::new();
let mut vertex_of: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
let mut middle_of: std::collections::HashMap<usize, u32> = std::collections::HashMap::new();
let mut edges: Vec<PartEdge> = Vec::new();
// Each part edge's origin: the record edge it is a piece of (`None` for a spoke).
let mut origin: Vec<Option<usize>> = Vec::new();
let mut piece_of: std::collections::HashMap<(usize, u64, u64), u32> = std::collections::HashMap::new();
let mut spoke_of: std::collections::HashMap<usize, u32> = std::collections::HashMap::new();
let centre = match fill.spokes.first().and_then(|s| s.points.last()) {
Some(c) => {
vertices.push([c.x, c.y, c.z]);
Some(0u32)
}
None => None,
};
let mut faces = Vec::with_capacity(fill.patches.len());
for patch in &fill.patches {
let mut uses = Vec::with_capacity(patch.sides.len());
for side in &patch.sides {
let line = |interval: [f64; 2], at: [[f64; 2]; 2]| Curve2::Spline {
knots: vec![interval[0], interval[0], interval[1], interval[1]],
points: at.to_vec(),
};
match *side {
IsoSide::Edge { edge: k, from, to, uv } => {
let &(e, forward) = chain.get(k).ok_or_else(|| fail("side"))?;
let edge = record.edges.get(e).ok_or_else(|| fail("edge reference"))?;
let [a, z] = edge.interval;
// The patch's parameter runs the loop's way: the edge's own, reversed where the
// loop runs against it.
let tau = |t: f64| if forward { t } else { a + z - t };
let (tf, tt) = (tau(from), tau(to));
let (lo, hi, along) = if tf < tt { (tf, tt, true) } else { (tt, tf, false) };
let key = (e, lo.to_bits(), hi.to_bits());
let id = match piece_of.get(&key) {
Some(id) => *id,
None => {
let mut ends = [0u32; 2];
for (end, t) in ends.iter_mut().zip([lo, hi]) {
*end = if t == a || t == z {
let [start, finish] = edge.vertices;
let v = if t == a { start } else { finish };
let next = index(vertices.len())?;
let id = *vertex_of.entry(v).or_insert(next);
if id == next {
vertices.push(record.vertices.get(v as usize).ok_or_else(|| fail("vertex reference"))?.point);
}
id
} else {
middle(&mut vertices, &mut middle_of, k, edge, t)?
};
}
let id = index(edges.len())?;
edges.push(PartEdge { curve: edge.curve.clone(), interval: [lo, hi], vertices: ends });
origin.push(Some(e));
piece_of.insert(key, id);
id
}
};
let at = if along { uv } else { [uv[1], uv[0]] };
uses.push(PartUse { edge: id, forward: along, given: Some(line([lo, hi], at)) });
}
IsoSide::Spoke { spoke, forward, uv } => {
let id = match spoke_of.get(&spoke) {
Some(id) => *id,
None => {
let curve = fill.spokes.get(spoke).ok_or_else(|| fail("spoke"))?;
let &(e, _) = chain.get(spoke).ok_or_else(|| fail("spoke"))?;
let edge = record.edges.get(e).ok_or_else(|| fail("edge reference"))?;
let start = middle(&mut vertices, &mut middle_of, spoke, edge, 0.5 * (edge.interval[0] + edge.interval[1]))?;
let end = centre.ok_or_else(|| fail("centre"))?;
let id = index(edges.len())?;
let points = curve.points.iter().map(|p| [p.x, p.y, p.z]).collect();
edges.push(PartEdge {
curve: b::Curve::Spline { knots: curve.knots.clone(), points },
interval: [0.0, 1.0],
vertices: [start, end],
});
origin.push(None);
spoke_of.insert(spoke, id);
id
}
};
let at = if forward { uv } else { [uv[1], uv[0]] };
uses.push(PartUse { edge: id, forward, given: Some(line([0.0, 1.0], at)) });
}
}
}
faces.push(PartFace { surface: surface_record(&patch.surface)?, forward: true, loops: vec![uses] });
}
let made = assemble(Parts { vertices, edges, faces, tolerance: tol }, false)?;
let smlib = solidcraft_smlib::Brep::from_record(&made.brep, true).map_err(|e| KernelError::Failed(format!("patch: SMLib: {e}")))?;
let (sheet, order) = crate::native::body_from_smlib_ordered(&smlib)?;
let Some(order) = order else { return Ok(sheet) };
let place = |list: &[usize], c: usize| if list.is_empty() { Some(c) } else { list.get(c).copied() };
// Each body edge's origin and the body faces beside it.
let mut prov: Vec<Option<EdgeProv>> = vec![None; sheet.edge_count()];
let record_out = &made.brep;
for (pe, c) in made.edges.iter().enumerate() {
let Some(c) = *c else { continue };
let j = place(&order.edges, c).ok_or_else(|| fail("edge order"))?;
let mut beside: Vec<usize> =
record_out.edges.get(c).map(|x| x.uses.iter().filter_map(|u| place(&order.faces, u.face as usize)).collect()).unwrap_or_default();
beside.sort_unstable();
beside.dedup();
let origins = origin.get(pe).copied().flatten().map(|edge| vec![EdgeOrigin::Edge { operand: 0, edge }]).unwrap_or_default();
*prov.get_mut(j).ok_or_else(|| fail("edge order"))? = Some(EdgeProv { faces: beside, origins });
}
let Some(edges) = prov.into_iter().collect::<Option<Vec<EdgeProv>>>() else { return Ok(sheet) };
let history = History { operands: 1, faces: vec![FaceProv { origins: Vec::new(), change: Change::Generated }; sheet.face_count()], edges };
sheet.with_history(history, &[body])
}
/// The vertex at parameter `t` inside loop edge `k` (record edge `edge`), made once.
fn middle(vertices: &mut Vec<[f64; 3]>, made: &mut std::collections::HashMap<usize, u32>, k: usize, edge: &b::Edge, t: f64) -> Result<u32> {
if let Some(v) = made.get(&k) {
return Ok(*v);
}
let p = CurveEval::new(&edge.curve).ok().and_then(|c| c.point(t)).ok_or_else(|| KernelError::Failed("patch: an edge has no middle".into()))?;
let v = u32::try_from(vertices.len()).map_err(|_| KernelError::Failed("patch: vertex budget".into()))?;
vertices.push([p.x, p.y, p.z]);
made.insert(k, v);
Ok(v)
}
/// How far a loop's edges may be from a plane for its patch to be that plane: `10⁻⁷` of the
/// body's size, at least the linear tolerance (`10⁻⁶` mm) — the body's own "same point"
/// tolerance, since a planar face is exact and other operations treat it as containing its