Split Face (FusionSplitFaceCommand): a timeline feature dividing the picked faces where a plane, another body's face (its surface extended) or a sketch's curves meet them, through kernel::split_faces; the solid is unchanged and the pieces take the face's name with #k. Rebase fixes: duplicate Chamfer and Combine specs; the Delete selection's face test expects Delete Face's refusal

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-09 12:47:56 +00:00
1 parent 3db6119cc6
commit 68271137a5
9 files changed
+168 -7

No files matched your search

+7
View File
@@ -162,6 +162,13 @@ impl FeatureKind {
pt(&mut t.point);
}
}
FeatureKind::SplitFace { faces, plane, tool, .. } => {
faces.iter_mut().for_each(pt);
map_plane_ref(m, plane);
if let Some(t) = tool {
pt(&mut t.point);
}
}
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
pt(origin);
dir(d);
+6
View File
@@ -40,6 +40,12 @@ impl FeatureKind {
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Split { plane, .. } => {
plane_names(plane, names)
}
FeatureKind::SplitFace { plane, sketch, .. } => {
plane_names(plane, names);
if let Some(s) = sketch {
sk(s);
}
}
FeatureKind::Extrude { sketch, .. }
| FeatureKind::Revolve { sketch, .. }
| FeatureKind::Emboss { sketch, .. }
+21 -4
View File
@@ -355,6 +355,19 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "Option::is_none")]
tool: Option<FaceAt>,
},
/// Split faces (at the given points) where a tool meets them, the solid unchanged: a plane,
/// a face of a body (`tool`, its surface extended) or a sketch's curves (`sketch`, swept
/// along its normal). The pieces are named after the face (`#k`).
SplitFace {
faces: Vec<Vec3>,
#[serde(default, skip_serializing_if = "Option::is_none")]
body: Option<String>,
plane: PlaneRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
tool: Option<FaceAt>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sketch: Option<u64>,
},
/// Scale bodies about a point (uniform, or per axis).
Scale {
bodies: Vec<String>,
@@ -936,6 +949,7 @@ impl FeatureKind {
FeatureKind::Shell { .. } => "ShellFeature",
FeatureKind::Draft { .. } => "DraftFeature",
FeatureKind::Split { .. } => "SplitBodyFeature",
FeatureKind::SplitFace { .. } => "SplitFaceFeature",
FeatureKind::Move { .. } => "MoveFeature",
FeatureKind::Scale { .. } => "ScaleFeature",
FeatureKind::OffsetFace { .. } => "OffsetFacesFeature",
@@ -997,6 +1011,7 @@ impl FeatureKind {
FeatureKind::Shell { .. } => "Shell",
FeatureKind::Draft { .. } => "Draft",
FeatureKind::Split { .. } => "Split",
FeatureKind::SplitFace { .. } => "SplitFace",
FeatureKind::Move { .. } => "Move",
FeatureKind::Scale { .. } => "Scale",
FeatureKind::OffsetFace { .. } => "OffsetFace",
@@ -1042,6 +1057,7 @@ impl FeatureKind {
| FeatureKind::Draft { faces, .. }
| FeatureKind::OffsetFace { faces, .. }
| FeatureKind::DeleteFace { faces, .. }
| FeatureKind::SplitFace { faces, .. }
| FeatureKind::ReplaceFace { faces, .. } => faces.iter_mut().collect(),
FeatureKind::Hole { position, points: None, to, .. } => std::iter::once(position).chain(to.iter_mut()).collect(),
FeatureKind::Hole { to: Some(t), .. } => vec![t],
@@ -1060,9 +1076,10 @@ impl FeatureKind {
pub fn expressions(&self) -> Vec<&str> {
let mut v: Vec<&str> = Vec::new();
match self {
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Split { plane, .. } => {
plane_exprs(plane, &mut v)
}
FeatureKind::Sketch { plane, .. }
| FeatureKind::ConstructionPlane { plane }
| FeatureKind::Split { plane, .. }
| FeatureKind::SplitFace { plane, .. } => plane_exprs(plane, &mut v),
FeatureKind::Extrude { extent, .. } => {
v.push(&extent.distance);
if let Some(d) = &extent.distance2 {
@@ -1551,7 +1568,7 @@ impl Document {
FeatureKind::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } if *path_sketch == id => gone.push(f.id),
FeatureKind::Emboss { sketch, .. } | FeatureKind::Rib { sketch, .. } if *sketch == id => gone.push(f.id),
FeatureKind::SheetBase { sketch, .. } | FeatureKind::SheetContour { sketch, .. } if *sketch == id => gone.push(f.id),
FeatureKind::SheetFold { sketch: Some(s), .. } if *s == id => gone.push(f.id),
FeatureKind::SheetFold { sketch: Some(s), .. } | FeatureKind::SplitFace { sketch: Some(s), .. } if *s == id => gone.push(f.id),
_ => {}
}
}
+40
View File
@@ -2414,6 +2414,46 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
apply_op(st, f, made, *operation, &[])
}
FeatureKind::SplitFace { faces, body, plane, tool, sketch } => {
let i = body_at(st, body, faces)?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let mut picked: Vec<usize> = Vec::new();
for p in faces {
let fi = crate::appearance::face_index_at(&mb, *p)
.ok_or_else(|| DocError::Invalid(format!("Split Face: no face of {} at {p:?}", mb.name)))?;
if !picked.contains(&fi) {
picked.push(fi);
}
}
let nb = match (tool, sketch) {
(Some(t), _) => {
let tb = st.body(&t.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", t.body)))?;
let face = crate::appearance::face_index_at(tb, t.point)
.ok_or_else(|| DocError::Invalid(format!("no face of `{}` at the tool point", t.body)))?;
kernel::split_faces(&mb.body, &picked, &kernel::SplitTool::Face { body: &tb.body, face })?
}
(None, Some(sid)) => {
let ss = st.sketch(*sid).ok_or_else(|| DocError::Unknown(format!("sketch {sid}")))?;
let curves: Vec<Vec<Vec2>> = (0..ss.sketch.curves.len())
.filter(|ci| ss.sketch.curves.get(*ci).is_some_and(|c| !c.construction))
.map(|ci| ss.sketch.polyline(ci))
.filter(|pl| pl.len() >= 2)
.collect();
if curves.is_empty() {
return Err(DocError::Invalid(format!("sketch `{}` has no curves to split with", ss.name)));
}
kernel::split_faces(&mb.body, &picked, &kernel::SplitTool::Curves { plane: ss.plane, curves })?
}
(None, None) => {
let pl = doc.resolve_plane(vals, plane, 0)?;
kernel::split_faces(&mb.body, &picked, &kernel::SplitTool::Plane(pl))?
}
};
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::new(mb.name, nb, mb.feature);
}
Ok(())
}
FeatureKind::Split { body, plane, tool } => {
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::Unknown(format!("body `{body}`"))) };
+1
View File
@@ -34,6 +34,7 @@ impl FeatureKind {
FeatureKind::Sketch { plane, .. }
| FeatureKind::ConstructionPlane { plane }
| FeatureKind::Split { plane, .. }
| FeatureKind::SplitFace { plane, .. }
| FeatureKind::Mirror { plane, .. } => plane_inputs_mut(plane, &mut v),
FeatureKind::Extrude { extent, .. } => {
v.push(("Distance", &mut extent.distance, L));