mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
sew, step: face forms from the boundary's extent; trimmed profiles exact
sew and the STEP reader build a revolved face's NURBS form over `form::extent` (its boundary on its label) instead of a pass of pcurves on the label, and compute its pcurves within the form's box. The STEP reader gives revolutions their form too, and a trimmed swept profile its file's trim range exactly (points on the axis fit either side of it). The STEP seam tests read edge angles through `Edge::curve_interval`, intervals and domains being in the forms' parameters. step 37/37. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
This commit is contained in:
+20
-13
@@ -1365,12 +1365,12 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
|
||||
// Each edge's label and NURBS form (`form::edge`): a circle about a revolved face's axis on
|
||||
// that face's frame, so its form lies on the face's quarter grid; an edge the caller gave a
|
||||
// pcurve for keeps the parameter that pcurve is on.
|
||||
let mut frames_of: Vec<Vec<&b::Frame>> = vec![Vec::new(); edges.len()];
|
||||
let mut frames_of: Vec<Vec<b::Frame>> = vec![Vec::new(); edges.len()];
|
||||
for f in &faces {
|
||||
let Some(frame) = b::form::frame_of(&f.surface) else { continue };
|
||||
for u in f.loops.iter().flatten() {
|
||||
if let Some(slot) = frames_of.get_mut(u.edge as usize) {
|
||||
slot.push(frame);
|
||||
slot.push(frame.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1383,7 +1383,7 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
|
||||
continue;
|
||||
}
|
||||
let (label, interval, form) =
|
||||
b::form::edge(&e.curve, e.interval, frames.iter().copied(), tol).ok_or_else(|| fail("an arc of more than one turn".into()))?;
|
||||
b::form::edge(&e.curve, e.interval, frames.iter(), tol).ok_or_else(|| fail("an arc of more than one turn".into()))?;
|
||||
labels.push((label, interval));
|
||||
forms.push(form);
|
||||
}
|
||||
@@ -1405,18 +1405,26 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let folded_label = folded_on(None);
|
||||
// A cylinder, cone, sphere or torus on its NURBS form, over the extent of its boundary on
|
||||
// its label.
|
||||
let form = match f.surface {
|
||||
b::Surface::Cylinder { .. } | b::Surface::Cone { .. } | b::Surface::Sphere { .. } | b::Surface::Torus { .. } => {
|
||||
let label = pcurve::pcurves(&f.surface, &uses_of(&edges, &forms, ls, false, &folded_label, tol)?, tol).map_err(failed)?;
|
||||
let extent = domain_of(&f.surface, &label).ok_or_else(|| fail(format!("face {fi} has no area in its parameters")))?;
|
||||
Some(b::form::surface(&f.surface, extent).ok_or_else(|| fail(format!("face {fi}: more than one turn")))?.0)
|
||||
let boundary: Vec<Vec<(&b::Curve, [f64; 2], bool)>> = ls
|
||||
.iter()
|
||||
.map(|l| l.iter().filter_map(|&(e, forward, _)| labels.get(e as usize).map(|(c, i)| (c, *i, forward))).collect())
|
||||
.collect();
|
||||
let extent = b::form::extent(&f.surface, &boundary).ok_or_else(|| fail(format!("face {fi}: its boundary is not on its surface")))?;
|
||||
Some(b::form::surface(&f.surface, extent).ok_or_else(|| fail(format!("face {fi}: more than one turn")))?)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let (form, form_box) = match form {
|
||||
Some((s, d)) => (Some(s), Some(d)),
|
||||
None => (None, None),
|
||||
};
|
||||
let on = form.as_ref().unwrap_or(&f.surface);
|
||||
let folded_form = if form.is_some() { folded_on(form.as_ref()) } else { Vec::new() };
|
||||
let pc = pcurve::pcurves(on, &uses_of(&edges, &forms, ls, true, &folded_form, tol)?, tol).map_err(failed)?;
|
||||
let pc = pcurve::pcurves_within(on, &uses_of(&edges, &forms, ls, &folded_form, tol)?, tol, form_box).map_err(failed)?;
|
||||
domains.push(domain_of(on, &pc).ok_or_else(|| fail(format!("face {fi} has no area in its parameters")))?);
|
||||
pcs.push(pc);
|
||||
face_forms.push(form);
|
||||
@@ -1568,14 +1576,13 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
|
||||
/// A face's parameter box: around its pcurves' control points (a B-spline lies in their hull),
|
||||
/// held inside the surface's natural domain (one turn where periodic). None when it has no
|
||||
/// width in either direction.
|
||||
/// A face's edge uses for `pcurve::pcurves`: on each edge's NURBS form where it has one and
|
||||
/// `formed`, else on its curve as given; `folded` gives the pcurves of edges folded across the
|
||||
/// face (see [`folded_pcurve`]).
|
||||
/// A face's edge uses for `pcurve::pcurves`: on each edge's NURBS form where it has one, else on
|
||||
/// its curve as given; `folded` gives the pcurves of edges folded across the face (see
|
||||
/// [`folded_pcurve`]).
|
||||
fn uses_of<'a>(
|
||||
edges: &'a [PartEdge],
|
||||
forms: &'a [Option<(b::Curve, [f64; 2])>],
|
||||
ls: &[Vec<(u32, bool, Option<&'a Curve2>)>],
|
||||
formed: bool,
|
||||
folded: &'a [(u32, Curve2)],
|
||||
tol: f64,
|
||||
) -> Result<Vec<Vec<pcurve::EdgeUse<'a>>>> {
|
||||
@@ -1585,7 +1592,7 @@ fn uses_of<'a>(
|
||||
.map(|&(e, forward, given)| {
|
||||
let edge = edges.get(e as usize).ok_or_else(|| KernelError::Invalid("sew: edge reference".into()))?;
|
||||
let (curve, interval) = match forms.get(e as usize).and_then(Option::as_ref) {
|
||||
Some((c, i)) if formed => (c, *i),
|
||||
Some((c, i)) => (c, *i),
|
||||
_ => (&edge.curve, edge.interval),
|
||||
};
|
||||
let given = given.or_else(|| folded.iter().find(|(f, _)| *f == e).map(|(_, c)| c));
|
||||
|
||||
@@ -965,34 +965,47 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
// Which loop index in `face.loops` each edge loop is.
|
||||
let positions: Vec<usize> = face.loops.iter().enumerate().filter(|(_, l)| matches!(l, LoopB::Edges(_))).map(|(i, _)| i).collect();
|
||||
let tol = self.tol;
|
||||
// The face's pcurves on `on`, each edge on its NURBS form when `formed`.
|
||||
let pcurves_on = |on: &BSurface, formed: bool| -> R<Vec<Vec<Curve2>>> {
|
||||
// The face's pcurves on `on`, each edge on its NURBS form.
|
||||
let pcurves_on = |on: &BSurface, domain: Option<[[f64; 2]; 2]>| -> R<Vec<Vec<Curve2>>> {
|
||||
let mut uses = Vec::with_capacity(edge_loops.len());
|
||||
for l in &edge_loops {
|
||||
let mut u = Vec::with_capacity(l.len());
|
||||
for &(e, forward) in l {
|
||||
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
|
||||
let (curve, interval) = match &edge.form {
|
||||
Some((c, i)) if formed => (c, *i),
|
||||
Some((c, i)) => (c, *i),
|
||||
_ => (&edge.curve, edge.interval),
|
||||
};
|
||||
u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: 0.0, given: None });
|
||||
}
|
||||
uses.push(u);
|
||||
}
|
||||
pcurve::pcurves(on, &uses, tol).map_err(|e| e.to_string())
|
||||
pcurve::pcurves_within(on, &uses, tol, domain).map_err(|e| e.to_string())
|
||||
};
|
||||
// A cylinder, cone, sphere or torus on its NURBS form, over the extent its label's pcurves
|
||||
// give; then every pcurve on the forms.
|
||||
// A cylinder, cone, sphere, torus or revolution on its NURBS form, over the extent of its
|
||||
// boundary on its label; then every pcurve on the forms.
|
||||
let form = match brep::form::frame_of(&label) {
|
||||
Some(_) => {
|
||||
let extent = face_domain(&label, &pcurves_on(&label, false)?)?;
|
||||
Some(brep::form::surface(&label, extent).ok_or_else(|| unsupported("a face of more than one turn"))?.0)
|
||||
let mut boundary = Vec::with_capacity(edge_loops.len());
|
||||
for l in &edge_loops {
|
||||
let mut b = Vec::with_capacity(l.len());
|
||||
for &(e, forward) in l {
|
||||
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
|
||||
b.push((&edge.curve, edge.interval, forward));
|
||||
}
|
||||
boundary.push(b);
|
||||
}
|
||||
let extent = brep::form::extent(&label, &boundary).ok_or_else(|| unsupported("a face whose boundary does not lie on its surface"))?;
|
||||
Some(brep::form::surface(&label, extent).ok_or_else(|| unsupported("a face of more than one turn"))?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let (form, box_on_form) = match form {
|
||||
Some((f, d)) => (Some(f), Some(d)),
|
||||
None => (None, None),
|
||||
};
|
||||
let surface = form.clone().unwrap_or(label);
|
||||
let pcs = pcurves_on(&surface, true)?;
|
||||
let pcs = pcurves_on(&surface, box_on_form)?;
|
||||
// Signed areas of the loops that close in (u, v).
|
||||
let periods = natural_periods(&surface);
|
||||
// Each use starts near where the one before it ends, along a bounded direction that
|
||||
|
||||
@@ -237,6 +237,14 @@ fn whole(c: &Curve) -> R<Option<BCurve>> {
|
||||
/// The middle parameter of a TRIMMED_CURVE trimmed by parameters (on a closed conic, the
|
||||
/// middle of the arc its sense runs over).
|
||||
fn trimmed_middle(c: &Curve) -> Option<f64> {
|
||||
let (a, b) = trimmed_range(c)?;
|
||||
let m = 0.5 * (a + b);
|
||||
m.is_finite().then_some(m)
|
||||
}
|
||||
|
||||
/// A trimmed profile's range on its basis, low end first, where both trims are parameters: on
|
||||
/// a closed basis (a circle, an ellipse) the way round `sense` gives.
|
||||
fn trimmed_range(c: &Curve) -> Option<(f64, f64)> {
|
||||
let Curve::Trimmed { basis, trim_1, trim_2, sense, .. } = c else { return None };
|
||||
let param = |t: &[crate::geom::Trim]| {
|
||||
t.iter().find_map(|x| match x {
|
||||
@@ -248,8 +256,8 @@ fn trimmed_middle(c: &Curve) -> Option<f64> {
|
||||
if is_closed(basis) {
|
||||
b = if *sense { a + (b - a).rem_euclid(TAU) } else { a - (a - b).rem_euclid(TAU) };
|
||||
}
|
||||
let m = 0.5 * (a + b);
|
||||
m.is_finite().then_some(m)
|
||||
let (lo, hi) = (a.min(b), a.max(b));
|
||||
(lo.is_finite() && hi.is_finite() && lo < hi).then_some((lo, hi))
|
||||
}
|
||||
|
||||
fn is_closed(c: &Curve) -> bool {
|
||||
@@ -309,7 +317,12 @@ fn revolution(profile: &Curve, axis: &Axis1, ext: &Extent) -> R<BSurface> {
|
||||
// The profile's side of the axis where the face is.
|
||||
let mid = ts.get(ts.len() / 2).copied().ok_or("a face without boundary points")?;
|
||||
let e = probe(mid).map(radial).and_then(|r| r.normalized()).unwrap_or(e);
|
||||
let s = span(&ts, &seam, is_closed(c))?;
|
||||
// A trimmed profile is the file's range of its basis exactly; otherwise the face's
|
||||
// reach along it.
|
||||
let s = match trimmed_range(profile) {
|
||||
Some(r) => Span::Open { inner: r, outer: r },
|
||||
None => span(&ts, &seam, is_closed(c))?,
|
||||
};
|
||||
// Stop where the profile crosses the axis outside the face.
|
||||
let side = |t: f64| probe(t).map(|q| (q - o).dot(e));
|
||||
let s = match s {
|
||||
@@ -394,7 +407,11 @@ fn extrusion(profile: &Curve, direction: P3, magnitude: f64, ext: &Extent) -> R<
|
||||
return Err(unsupported("an extrusion along its profile's plane"));
|
||||
}
|
||||
let seam: Vec<f64> = ext.seam.iter().filter_map(|q| onto(*q)).collect();
|
||||
bounded(c, span(&ts, &seam, is_closed(c))?)?
|
||||
let s = match trimmed_range(profile) {
|
||||
Some(r) => Span::Open { inner: r, outer: r },
|
||||
None => span(&ts, &seam, is_closed(c))?,
|
||||
};
|
||||
bounded(c, s)?
|
||||
}
|
||||
};
|
||||
let (knots, homogeneous, rational) = match curve {
|
||||
|
||||
@@ -373,14 +373,20 @@ fn a_cylinder_has_a_seam_used_twice_by_its_side() {
|
||||
assert_eq!(seam.uses[0].face, seam.uses[1].face);
|
||||
for e in &s.brep.edges {
|
||||
if matches!(e.curve, BCurve::Circle { .. }) {
|
||||
assert!((e.interval[1] - e.interval[0] - TAU).abs() < 1e-12);
|
||||
// On its NURBS form: a whole turn in angle, its interval in the form's parameter.
|
||||
let [a0, a1] = e.curve_interval().unwrap();
|
||||
assert!(e.nurbs.is_some() && (a1 - a0 - TAU).abs() < 1e-12);
|
||||
assert_eq!(e.vertices[0], e.vertices[1]);
|
||||
}
|
||||
}
|
||||
faces_away(&s.brep, 0, |p| [1.0, 2.0, p[2].clamp(1.0, 9.0)]);
|
||||
// The side face spans one turn of the cylinder and its height.
|
||||
// The side face spans one turn of the cylinder (four quarters of its NURBS form) and its height.
|
||||
let f = s.brep.faces.iter().find(|f| matches!(f.surface, BSurface::Cylinder { .. })).unwrap();
|
||||
assert!((f.domain[0][1] - f.domain[0][0] - TAU).abs() < 1e-9 && f.domain[1][0] < 1e-6 && f.domain[1][1] > 10.0 - 1e-6, "{:?}", f.domain);
|
||||
assert!(
|
||||
f.nurbs.is_some() && (f.domain[0][1] - f.domain[0][0] - 4.0).abs() < 1e-9 && f.domain[1][0] < 1e-6 && f.domain[1][1] > 10.0 - 1e-6,
|
||||
"{:?}",
|
||||
f.domain
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -402,7 +408,8 @@ fn a_ball_bounded_by_its_seam_alone() {
|
||||
let s = one_solid(&w.file(&[solid]));
|
||||
assert_eq!(counts(&s.brep), [2, 1, 1, 1, 2]);
|
||||
let e = &s.brep.edges[0];
|
||||
assert!((e.interval[1] - e.interval[0] - PI).abs() < 1e-12, "{:?}", e.interval);
|
||||
let [a0, a1] = e.curve_interval().unwrap();
|
||||
assert!((a1 - a0 - PI).abs() < 1e-12, "{:?}", e.interval);
|
||||
faces_away(&s.brep, 0, |_| [0.0; 3]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user