Unstitch and Reverse Normal (#856)

Unstitch takes a body's faces out as surface bodies; Reverse Normal turns a surface body's faces over. Both are saved in format 18. Closes #766.
This commit is contained in:
Irakli Kourtis authored and GitHub committed 2026-10-13 16:45:31 -04:00
1 parent b702f0efde
commit 753b0a2eab
16 files changed
+322 -19

No files matched your search

+1
View File
@@ -337,6 +337,7 @@ impl FeatureKind {
| FeatureKind::MeshConvert { .. }
| FeatureKind::BodyTransfer { .. } => {}
FeatureKind::Patch { edges, .. } | FeatureKind::SurfaceExtend { edges, .. } => edges.iter_mut().for_each(pt),
FeatureKind::Unstitch { faces, .. } | FeatureKind::ReverseNormal { faces, .. } => faces.iter_mut().for_each(pt),
FeatureKind::SurfaceTrim { plane, tool, keep, .. } => {
map_plane_ref(m, plane);
if let Some(t) = tool {
+2
View File
@@ -321,6 +321,8 @@ impl FeatureKind {
| K::SheetConvert { body, .. }
| K::SurfaceExtend { body, .. }
| K::SurfaceTrim { body, .. }
| K::Unstitch { body, .. }
| K::ReverseNormal { body, .. }
| K::MeshConvert { body, .. } => mapped_name(body, bodies),
_ => {}
}
+20
View File
@@ -937,6 +937,19 @@ pub enum FeatureKind {
curves: Vec<String>,
extent: Extent,
},
/// A body taken apart: each face at `faces` (every face when empty) its own surface body,
/// the faces left one surface body under the body's name. A solid comes apart into surfaces.
Unstitch {
body: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
faces: Vec<Vec3>,
},
/// A surface body with the faces at `faces` (every face when empty) turned over.
ReverseNormal {
body: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
faces: Vec<Vec3>,
},
/// A planar surface body extended outward by `distance` past the edges through `edges`.
SurfaceExtend {
body: String,
@@ -1286,6 +1299,8 @@ impl FeatureKind {
| K::Thicken { .. }
| K::SurfaceExtend { .. }
| K::SurfaceTrim { .. }
| K::Unstitch { .. }
| K::ReverseNormal { .. }
| K::Move { .. }
| K::Import { .. }
| K::MeshImport { .. }
@@ -1374,6 +1389,8 @@ impl FeatureKind {
FeatureKind::Thicken { .. } => "ThickenFeature",
FeatureKind::SurfaceTrim { .. } => "TrimFeature",
FeatureKind::SurfaceExtend { .. } => "ExtendFeature",
FeatureKind::Unstitch { .. } => "UnstitchFeature",
FeatureKind::ReverseNormal { .. } => "ReverseNormalFeature",
FeatureKind::Import { .. } => "BaseFeature",
FeatureKind::MeshImport { .. } => "MeshFeature",
FeatureKind::MeshConvert { .. } => "MeshConvertFeature",
@@ -1441,6 +1458,8 @@ impl FeatureKind {
FeatureKind::Thicken { .. } => "Thicken",
FeatureKind::SurfaceTrim { .. } => "Trim",
FeatureKind::SurfaceExtend { .. } => "Extend",
FeatureKind::Unstitch { .. } => "Unstitch",
FeatureKind::ReverseNormal { .. } => "ReverseNormal",
FeatureKind::Import { .. } => "Import",
FeatureKind::MeshImport { .. } => "Mesh",
FeatureKind::MeshConvert { .. } => "ConvertMesh",
@@ -1669,6 +1688,7 @@ impl FeatureKind {
FeatureKind::Thicken { thickness, .. } => v.push(thickness),
FeatureKind::SurfaceTrim { plane, .. } => plane_exprs(plane, &mut v),
FeatureKind::SurfaceExtend { distance, .. } => v.push(distance),
FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } => {}
FeatureKind::Patch { .. } => {}
FeatureKind::Import { .. } | FeatureKind::MeshImport { .. } | FeatureKind::BodyTransfer { .. } => {}
}
+3 -1
View File
@@ -3543,7 +3543,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
| FeatureKind::Thicken { .. }
| FeatureKind::SurfaceTrim { .. }
| FeatureKind::SurfaceExtend { .. }
| FeatureKind::SurfaceExtrude { .. } => surface::eval(doc, vals, f, st),
| FeatureKind::SurfaceExtrude { .. }
| FeatureKind::Unstitch { .. }
| FeatureKind::ReverseNormal { .. } => surface::eval(doc, vals, f, st),
FeatureKind::Emboss { .. }
| FeatureKind::Rib { .. }
| FeatureKind::ReplaceFace { .. }
+33 -2
View File
@@ -1,5 +1,5 @@
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend and Surface Extrude
//! (kernel `surfaces`).
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend, Surface Extrude,
//! Unstitch and Reverse Normal (kernel `surfaces`).
use super::*;
use solidcraft_geom::Seg2;
@@ -113,6 +113,37 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
st.bodies.push(ModelBody::new(name, body, f.id));
Ok(())
}
FeatureKind::Unstitch { body, faces } => {
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 (pieces, rest) = kernel::unstitch(&mb.body, faces)?;
let source = mb.traced();
// The faces left keep the body's name; each face taken out is a new surface body.
match rest {
Some(rest) => {
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name.clone(), source.then(rest), mb.feature);
}
}
None => {
st.bodies.remove(i);
}
}
for (k, piece) in pieces.into_iter().enumerate() {
let name = new_name(st, f, k);
st.bodies.push(ModelBody::traced_new(name, source.then(piece), f.id));
}
Ok(())
}
FeatureKind::ReverseNormal { body, faces } => {
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 turned = mb.traced().then(kernel::reverse_faces(&mb.body, faces)?);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, turned, mb.feature);
}
Ok(())
}
_ => Err(DocError::Invalid("not a surface feature".into())),
}
}
+7 -2
View File
@@ -104,13 +104,17 @@
//! Construction Geometries); a sketch that shows its construction geometry saves as before.
//! A `SurfaceExtrude` feature (Surface Extrude: `sketch`, optional `profiles`, `curves` and
//! `extent`, walls only) is first saved in 17; 0.8.0 would refuse the unknown feature.
//! - 18: surface features 0.9.0 doesn't know: `Unstitch` (a body's faces taken out as surface
//! bodies) and `ReverseNormal` (a surface body's faces turned over), each a `body` and
//! optional `faces` points. Version 17 designs need no transformation; the version stamp makes
//! 0.9.0 (which reads up to 17) ask for an update instead of meeting a feature it can't read.
use serde_json::Value;
use crate::{DocError, Document, Result};
/// The format this build writes.
pub const FORMAT: u32 = 17;
pub const FORMAT: u32 = 18;
pub fn format_string(n: u32) -> String {
format!("solidcraft/{n}")
@@ -127,7 +131,7 @@ pub fn version_of(s: &str) -> Option<u32> {
/// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`.
type JsonStep = fn(&mut Value) -> Result<()>;
const JSON_STEPS: [JsonStep; 16] = [
const JSON_STEPS: [JsonStep; 17] = [
|_| Ok(()),
v2_to_v3,
v3_to_v4,
@@ -144,6 +148,7 @@ const JSON_STEPS: [JsonStep; 16] = [
|_| Ok(()),
|_| Ok(()),
|_| Ok(()),
|_| Ok(()),
];
/// Stored face and edge names were worked out from geometry before format 14; they get the
+1
View File
@@ -277,6 +277,7 @@ impl FeatureKind {
FeatureKind::Thicken { thickness, .. } => v.push(("Thickness", thickness, L)),
FeatureKind::SurfaceTrim { plane, .. } => plane_inputs_mut(plane, &mut v),
FeatureKind::SurfaceExtend { distance, .. } => v.push(("Distance", distance, L)),
FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } => {}
FeatureKind::Patch { .. } => {}
FeatureKind::Combine { .. }
| FeatureKind::SheetBase { .. }