Untrim: holes removed and a face restored to its natural bounds (#1150)

Adds surface.untrim (#761); the SurfaceUntrim feature is first saved in format 21.
This commit is contained in:
Irakli Kourtis authored and GitHub committed 2026-10-14 20:29:25 -04:00
1 parent 734f69ad72
commit 16c49c3e86
25 files changed
+961 -18

No files matched your search

+15
View File
@@ -1019,6 +1019,19 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
faces: Vec<Vec3>,
},
/// The face at `face` restored towards its surface: the inner loops at the `holes` points
/// taken away (every one when `None`; none when empty), and with `outer` its outer boundary
/// back at the surface's natural bounds (an analytic surface's: the face's own span). A new
/// surface body; with `in_place`, a surface body's face is untrimmed where it is.
SurfaceUntrim {
face: FaceAt,
#[serde(default, skip_serializing_if = "Option::is_none")]
holes: Option<Vec<Vec3>>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
outer: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
in_place: bool,
},
/// A surface body turned about an axis from open curve chains and the boundaries of
/// profiles: walls only, no caps (a solid where a full turn closes them).
SurfaceRevolve {
@@ -1444,6 +1457,7 @@ impl FeatureKind {
FeatureKind::SurfaceExtend { .. } => "ExtendFeature",
FeatureKind::Unstitch { .. } => "UnstitchFeature",
FeatureKind::ReverseNormal { .. } => "ReverseNormalFeature",
FeatureKind::SurfaceUntrim { .. } => "UntrimFeature",
FeatureKind::Import { .. } => "BaseFeature",
FeatureKind::MeshImport { .. } => "MeshFeature",
FeatureKind::MeshConvert { .. } => "MeshConvertFeature",
@@ -1518,6 +1532,7 @@ impl FeatureKind {
FeatureKind::SurfaceExtend { .. } => "Extend",
FeatureKind::Unstitch { .. } => "Unstitch",
FeatureKind::ReverseNormal { .. } => "ReverseNormal",
FeatureKind::SurfaceUntrim { .. } => "Untrim",
FeatureKind::Import { .. } => "Import",
FeatureKind::MeshImport { .. } => "Mesh",
FeatureKind::MeshConvert { .. } => "ConvertMesh",
+2 -1
View File
@@ -3925,7 +3925,8 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
| FeatureKind::SurfaceRevolve { .. }
| FeatureKind::SurfaceSweep { .. }
| FeatureKind::SurfaceLoft { .. }
| FeatureKind::SurfaceOffset { .. } => surface::eval(doc, vals, f, st),
| FeatureKind::SurfaceOffset { .. }
| FeatureKind::SurfaceUntrim { .. } => surface::eval(doc, vals, f, st),
FeatureKind::Emboss { .. }
| FeatureKind::Rib { .. }
| FeatureKind::ReplaceFace { .. }
+24 -2
View File
@@ -1,6 +1,6 @@
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend, Surface Extrude,
//! Revolve, Sweep and Loft, Offset, Unstitch and Reverse Normal (kernel `surfaces`,
//! `surface_offset`).
//! Revolve, Sweep and Loft, Offset, Untrim, Unstitch and Reverse Normal (kernel `surfaces`,
//! `surface_offset`, `surface_untrim`).
use super::*;
use solidcraft_geom::Seg2;
@@ -234,6 +234,28 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
st.bodies.push(ModelBody::traced_new(name, made, f.id));
Ok(())
}
FeatureKind::SurfaceUntrim { face, holes, outer, in_place } => {
let i = st.bodies.iter().position(|b| b.name == face.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", face.body)))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let fi = crate::appearance::face_index_at(&mb, face.point)
.ok_or_else(|| DocError::Invalid(format!("Untrim: no face of {} at {:?}", mb.name, face.point)))?;
let which = match holes {
None => kernel::UntrimHoles::All,
Some(points) => kernel::UntrimHoles::At(points),
};
// A surface body's face is untrimmed where it is when asked; a solid's is copied out.
let replace = *in_place && mb.body.is_surface();
let made = mb.traced().then(kernel::untrim(&mb.body, fi, which, *outer, replace)?);
if replace {
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, made, mb.feature);
}
} else {
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::traced_new(name, made, f.id));
}
Ok(())
}
_ => Err(DocError::Invalid("not a surface feature".into())),
}
}
+9
View File
@@ -157,6 +157,15 @@ macro_rules! geometry_walk {
f(Pos(p));
}
}
// The face is picked (and named); the holes are picked by points on their edges.
FeatureKind::SurfaceUntrim { face, holes, .. } => {
f(Pick(& $($m)? face.point));
if let Some(points) = holes {
for p in points {
f(Pos(p));
}
}
}
FeatureKind::BoundaryFill { cells, .. } => {
for p in cells {
f(Pos(p));
+1
View File
@@ -174,6 +174,7 @@ macro_rules! feature_walk {
f(B(b));
}
}
FeatureKind::SurfaceUntrim { face, .. } => f(B(& $($m)? face.body)),
FeatureKind::SheetConvert { body, .. }
| FeatureKind::SurfaceExtend { body, .. }
| FeatureKind::Unstitch { body, .. }
@@ -151,6 +151,7 @@ pub(super) fn fixtures() -> Vec<(&'static str, Value)> {
("surface_offset", json!({"type": "surface_offset", "faces": [o], "distance": "soff.distance", "tolerance": "soff.tolerance"})),
("unstitch", json!({"type": "unstitch", "body": "B"})),
("reverse_normal", json!({"type": "reverse_normal", "body": "B"})),
("surface_untrim", json!({"type": "surface_untrim", "face": {"body": "B", "point": o}, "holes": [o], "outer": true})),
("surface_extend", json!({"type": "surface_extend", "body": "B", "edges": [o], "distance": "extend.distance"})),
("surface_trim", json!({"type": "surface_trim", "body": "B", "plane": plane("trim"), "keep": o})),
("move", json!({"type": "move", "bodies": ["B"], "translate": ["move.x", "move.y", "move.z"], "angle": "move.angle"})),
@@ -325,6 +325,7 @@ surface_offset
expressions: soff.distance, soff.tolerance
unstitch
reverse_normal
surface_untrim
surface_extend
inputs:
Distance = extend.distance (Length)
@@ -150,6 +150,9 @@ unstitch
moves: faces[0][0], faces[0][1], faces[0][2]
reverse_normal
moves: faces[0][0], faces[0][1], faces[0][2]
surface_untrim
moves: face.point[0], face.point[1], face.point[2], holes[0][0], holes[0][1], holes[0][2]
face points: [0, 0, 0]
surface_extend
moves: edges[0][0], edges[0][1], edges[0][2]
surface_trim
@@ -168,6 +168,8 @@ unstitch
remap_bodies: body
reverse_normal
remap_bodies: body
surface_untrim
remap_bodies: face.body
surface_extend
remap_bodies: body
surface_trim
+3
View File
@@ -167,6 +167,9 @@
//! Under the same version, looks can explicitly select opaque PBR parameters (`pbr`: metal
//! fraction, roughness, dielectric IOR). Absent fields keep the scalar shading a look always
//! had; 0.11.0 asks for an update instead of silently dropping the response.
//! A `SurfaceUntrim` feature (Untrim: a `face` as `body` and `point`, optional `holes`
//! points, `outer` and `in_place`) is also first saved in 21; 0.11.0 asks for an update
//! instead of meeting a feature it can't read.
use serde_json::Value;
+1 -1
View File
@@ -354,7 +354,7 @@ macro_rules! feature_walk {
FeatureKind::Thicken { thickness, .. } => f("Thickness", thickness, L, I),
FeatureKind::SurfaceTrim { plane, .. } => $plane(plane, f),
FeatureKind::SurfaceExtend { distance, .. } => f("Distance", distance, L, I),
FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } => {}
FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } | FeatureKind::SurfaceUntrim { .. } => {}
FeatureKind::SurfaceOffset { distance, tolerance, .. } => {
f("Distance", distance, L, I);
if let Some(t) = tolerance {