Class-A surfacing 2/3: surface match, boundary patch and CV fairing (#1487)
Thanks @midasdf.
This commit is contained in:
1 parent
7f79443b46
commit
07da7f8065
27 files changed
+3513
-11
No files matched your search
@@ -145,6 +145,78 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
|
||||
st.bodies.push(ModelBody::new(name, made, f.id));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceBoundary { sketch, curves, orders, crown, samples } => {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
|
||||
f.component,
|
||||
)?;
|
||||
let boundaries = boundary_frames(&ss, curves, orders, *samples)?;
|
||||
let body = kernel::boundary_patch(&boundaries, *crown, *samples)?;
|
||||
let name = new_name(st, f, 0);
|
||||
st.bodies.push(ModelBody::new(name, body, f.id));
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceMatch { body, faces, target, edges, order } => {
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let tb = st.body(target).ok_or_else(|| DocError::Unknown(format!("body `{target}`")))?.body.clone();
|
||||
if edges.is_empty() {
|
||||
return Err(DocError::Invalid("a match needs a chain of the target's edges".into()));
|
||||
}
|
||||
// Picked by a point on the face; without picks, the body's first face.
|
||||
let picked: Vec<solidcraft_geom::Vec3> = if faces.is_empty() {
|
||||
let faces = mb.body.faces(mb.body.size() * 1e-2)?;
|
||||
vec![faces.first().ok_or_else(|| DocError::Invalid(format!("`{body}` has no face to match")))?.centroid]
|
||||
} else {
|
||||
crate::plain(faces)
|
||||
};
|
||||
let made = kernel::match_surface(&mb.body, &picked, &tb, &crate::plain(edges), *order)?;
|
||||
// A match rebuilds the face in place: the body keeps its name, and whatever meets it
|
||||
// downstream still finds it.
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::new(mb.name, made, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceFair { body, faces, strength, iterations, keep_edges, tolerance } => {
|
||||
let tol = val(vals, tolerance, Kind::Length)?;
|
||||
let opts = kernel::FairOptions::new(*strength, *iterations as usize, *keep_edges, tol)?;
|
||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
// Picked by a point on the face; without picks, every face of the body.
|
||||
let mut picked: Vec<usize> = Vec::new();
|
||||
if faces.is_empty() {
|
||||
picked.extend(0..mb.body.face_count());
|
||||
} else {
|
||||
for p in faces {
|
||||
let fi =
|
||||
crate::appearance::face_index_at(&mb, p.0).ok_or_else(|| DocError::Invalid(format!("Fair: no face of {body} at {p:?}")))?;
|
||||
if !picked.contains(&fi) {
|
||||
picked.push(fi);
|
||||
}
|
||||
}
|
||||
}
|
||||
if picked.is_empty() {
|
||||
return Err(DocError::Invalid("the body has no faces to fair".into()));
|
||||
}
|
||||
// The face keeps every edge it had: only the surface between them is faired. The
|
||||
// energy before and after goes into the feature's warning, so the reply can say how
|
||||
// much the face moved.
|
||||
let (faired, report) = kernel::fair_faces(&mb.body, &picked, &opts)?;
|
||||
*warning = Some(format!(
|
||||
"faired {} face(s): energy {:.6} → {:.6} in {} step(s), control points moved up to {:.4} mm",
|
||||
picked.len(),
|
||||
report.energy_before,
|
||||
report.energy_after,
|
||||
report.steps,
|
||||
report.movement
|
||||
));
|
||||
if let Some(slot) = st.bodies.get_mut(i) {
|
||||
*slot = ModelBody::new(mb.name, faired, mb.feature);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
|
||||
let ss = sketch_in_feature_frame(
|
||||
doc,
|
||||
@@ -349,6 +421,70 @@ fn push_sheet(st: &mut ModelState, f: &Feature, traced: crate::lineage::Traced)
|
||||
st.bodies.push(ModelBody::traced_new(name, traced, f.id));
|
||||
}
|
||||
|
||||
/// The boundaries `surface.boundary` builds its patch from: each picked curve's own stations in
|
||||
/// the sketch's frame, every one of them leaving the boundary square into the loop — the direction
|
||||
/// from the curve towards the middle of the loop, taken across its tangent — over the plane the
|
||||
/// curve lies in (so a chain of planar curves makes a flat patch, and a network that is not
|
||||
/// coplanar is followed as the plane through the curve's own tangent). `orders` sets how much of
|
||||
/// that plane each side repeats.
|
||||
fn boundary_frames(
|
||||
ss: &SolvedSketch,
|
||||
curves: &[String],
|
||||
orders: &[solidcraft_kernel::Continuity],
|
||||
samples: usize,
|
||||
) -> Result<Vec<solidcraft_kernel::Boundary>> {
|
||||
use solidcraft_kernel::{Boundary, Frame};
|
||||
// The stations along each curve, evenly in its own arclength, so every side carries the same
|
||||
// number of frames and the patch's net is square.
|
||||
let mut sides: Vec<Vec<solidcraft_geom::Vec3>> = Vec::with_capacity(curves.len());
|
||||
for id in curves {
|
||||
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("sketch curve `{id}`")))?;
|
||||
let pieces = ss.sketch.exact_pieces(ci);
|
||||
if pieces.is_empty() {
|
||||
return Err(DocError::Invalid(format!("curve `{id}` has no exact geometry to build on")));
|
||||
}
|
||||
let lengths: Vec<f64> = pieces.iter().map(Seg2::length).collect();
|
||||
let total: f64 = lengths.iter().sum();
|
||||
if !total.is_finite() || total <= 1e-9 {
|
||||
return Err(DocError::Invalid(format!("curve `{id}` has no length")));
|
||||
}
|
||||
let mut pts = Vec::with_capacity(samples);
|
||||
let (mut used, mut at) = (0.0, 0usize);
|
||||
for k in 0..samples {
|
||||
let want = total * k as f64 / (samples.saturating_sub(1)).max(1) as f64;
|
||||
while at + 1 < pieces.len() && used + lengths[at] < want {
|
||||
used += lengths[at];
|
||||
at += 1;
|
||||
}
|
||||
let (Some(piece), Some(len)) = (pieces.get(at), lengths.get(at)) else { return Err(DocError::Invalid("boundary piece".into())) };
|
||||
let t = ((want - used) / len.max(1e-12)).clamp(0.0, 1.0);
|
||||
pts.push(ss.plane.to_world(piece.point_at(t)));
|
||||
}
|
||||
sides.push(pts);
|
||||
}
|
||||
// The middle of the loop, so every side leaves towards the inside of the patch.
|
||||
let count = sides.iter().map(Vec::len).sum::<usize>().max(1) as f64;
|
||||
let centre = sides.iter().flatten().fold(solidcraft_geom::Vec3::ZERO, |a, p| a + *p) * (1.0 / count);
|
||||
let mut out = Vec::with_capacity(sides.len());
|
||||
for (side, pts) in sides.iter().enumerate() {
|
||||
let mut frames = Vec::with_capacity(pts.len());
|
||||
for (i, p) in pts.iter().enumerate() {
|
||||
// The tangent from the stations on either side, one-sided at the ends (where the side
|
||||
// only has one neighbour).
|
||||
let before = if i == 0 { *p } else { pts[i - 1] };
|
||||
let after = if i + 1 == pts.len() { *p } else { pts[i + 1] };
|
||||
let tangent = (after - before).normalized().ok_or_else(|| DocError::Invalid("a boundary curve has a stationary station".into()))?;
|
||||
let across = centre - *p;
|
||||
let leave = (across - tangent * across.dot(tangent))
|
||||
.normalized()
|
||||
.ok_or_else(|| DocError::Invalid("a boundary runs through the middle of the loop: no patch can leave it".into()))?;
|
||||
frames.push(Frame { p: *p, leave, bend: Vec3::ZERO, bend_change: Vec3::ZERO });
|
||||
}
|
||||
out.push(Boundary { frames, order: orders.get(side).copied().unwrap_or(solidcraft_kernel::Continuity::G1) });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// What a surface builder sweeps: the boundary loops of the picked profiles, then the picked
|
||||
/// curves chained end to end.
|
||||
fn sheet_chains(ss: &SolvedSketch, profiles: Option<&ProfileSel>, curves: &[String]) -> Result<Vec<Vec<Seg2>>> {
|
||||
|
||||
Reference in new issue
Block a user