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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
3db6119cc6
commit
68271137a5
9 files changed
+168
-7
No files matched your search
@@ -231,7 +231,8 @@ mod tests {
|
||||
let (mut s, _) = part();
|
||||
let b = s.model.state().bodies[0].name.clone();
|
||||
let e = s.execute("selection.delete", &json!({"items": [{"type": "face", "body": b, "index": 0, "point": [0, 0, 0]}]})).unwrap_err();
|
||||
assert!(e.to_string().contains("not available yet"), "{e}");
|
||||
// A side of the part its neighbours can't close: Delete Face refuses it.
|
||||
assert!(e.to_string().contains("Delete Face"), "{e}");
|
||||
for p in [
|
||||
json!({}),
|
||||
json!({"items": "x"}),
|
||||
|
||||
@@ -33,8 +33,6 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.key("F")
|
||||
.params("edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other); radius2?; start?: [x,y,z]"),
|
||||
CommandSpec::new("solid.chamfer", "Chamfer", chamfer).at("SOLID", "MODIFY").icon("chamfer").params("edges: [[x,y,z] | {body, index}]; distance: expr; distance2?: expr (along the second face) | angle?: expr (from the first face); flip?: bool (which face is first: by default the one facing up most); body?"),
|
||||
CommandSpec::new("solid.combine", "Combine", combine).at("SOLID", "MODIFY").icon("combine").params("target: body; tools: [body]; operation?: join|cut|intersect; keep_tools?: bool"),
|
||||
CommandSpec::new("solid.chamfer", "Chamfer", chamfer).at("SOLID", "MODIFY").icon("chamfer").params("edges: [[x,y,z] | {body, index}]; distance: expr; body?"),
|
||||
CommandSpec::new("solid.combine", "Combine", combine).at("SOLID", "MODIFY").icon("combine").params("target: body; tools: [body]; operation?: join|cut|intersect; keep_tools?: bool; new_component?: bool (the result goes into a new component; component_name?)"),
|
||||
CommandSpec::new("solid.pattern.rectangular", "Rectangular Pattern", pattern_rect)
|
||||
.at("SOLID", "CREATE")
|
||||
@@ -94,6 +92,12 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SOLID", "CONSTRUCT")
|
||||
.icon("plane")
|
||||
.params("base: XY|XZ|YZ|plane name; axis: X|Y|Z|{origin, dir}; angle: expr; name?"),
|
||||
CommandSpec::new("solid.split_face", "Split Face", split_face)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("split")
|
||||
.params(
|
||||
"faces: [[x,y,z] points on the faces to split]; body?; and the splitting tool: plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id or name (its curves, along the sketch's normal)",
|
||||
),
|
||||
CommandSpec::new("solid.split_body", "Split Body", split_body)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("split")
|
||||
@@ -835,6 +839,25 @@ fn plane_angle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
)
|
||||
}
|
||||
|
||||
fn split_face(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.split_face";
|
||||
let faces = face_points(p, cmd)?;
|
||||
let body = str_(p, "body").map(str::to_string);
|
||||
let xy = || solvecraft_doc::PlaneRef::Origin { name: "XY".into() };
|
||||
let kind = if let Some(t) = p.get("tool").filter(|t| !t.is_null()) {
|
||||
let tb = t.get("body").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`tool.body` must name a body"))?.to_string();
|
||||
let point = t.get("point").and_then(crate::params::vec3).ok_or_else(|| bad(cmd, "`tool.point` must be [x, y, z] on the tool face"))?;
|
||||
FeatureKind::SplitFace { faces, body, plane: xy(), tool: Some(solvecraft_doc::FaceAt { body: tb, point }), sketch: None }
|
||||
} else if p.get("sketch").is_some_and(|v| !v.is_null()) {
|
||||
let sketch = sketch_id(s, p.get("sketch"), cmd, "sketch")?;
|
||||
FeatureKind::SplitFace { faces, body, plane: xy(), tool: None, sketch: Some(sketch) }
|
||||
} else {
|
||||
let plane = plane_param(s, p.get("plane"), cmd)?;
|
||||
FeatureKind::SplitFace { faces, body, plane, tool: None, sketch: None }
|
||||
};
|
||||
add_feature(s, p, kind)
|
||||
}
|
||||
|
||||
fn split_body(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.split_body";
|
||||
let body = str_(p, "body").ok_or_else(|| bad(cmd, "`body` is required"))?.to_string();
|
||||
|
||||
@@ -430,6 +430,8 @@ mod delete_face_tests;
|
||||
#[cfg(test)]
|
||||
mod extent_tests;
|
||||
#[cfg(test)]
|
||||
mod split_face_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod thread_tests;
|
||||
@@ -0,0 +1,64 @@
|
||||
//! Split Face: the top of a box divided by a plane, by another body's face and by sketch curves.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
match s.execute(id, &p) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("{id} {p}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// (kernel faces, volume) of a body: the merged count joins pieces of one surface back up.
|
||||
fn faces_volume(s: &mut Session, b: &str) -> (u64, f64) {
|
||||
let m = run(s, "inspect.measure", json!({ "bodies": [b] }))["bodies"][0].clone();
|
||||
(m["kernel"]["faces"].as_u64().unwrap_or(0), m["volume_mm3"].as_f64().unwrap_or(0.0))
|
||||
}
|
||||
|
||||
fn boxed() -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "body_name": "B"}));
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_a_face_by_a_plane() {
|
||||
let mut s = boxed();
|
||||
run(&mut s, "solid.split_face", json!({"faces": [[10, 15, 20]], "plane": {"origin": [20, 0, 0], "normal": [1, 0, 0]}}));
|
||||
let (f, v) = faces_volume(&mut s, "B");
|
||||
assert_eq!(f, 7);
|
||||
assert!((v - 24000.0).abs() < 1e-6 * 24000.0, "{v}");
|
||||
assert_eq!(s.doc.features.last().unwrap().kind.type_name(), "SplitFaceFeature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_a_face_by_another_bodys_face() {
|
||||
let mut s = boxed();
|
||||
run(&mut s, "solid.box", json!({"corner": [-5, 10, 25], "length": 50, "width": 5, "height": 5, "body_name": "Tool"}));
|
||||
// The tool's face at y = 10, extended down through the box's top.
|
||||
run(&mut s, "solid.split_face", json!({"faces": [[20, 5, 20]], "body": "B", "tool": {"body": "Tool", "point": [20, 10, 27]}}));
|
||||
assert_eq!(faces_volume(&mut s, "B").0, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_a_face_by_sketch_curves() {
|
||||
let mut s = boxed();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Cuts"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[10, -5], [10, 35]]}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[30, -5], [30, 35]]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.split_face", json!({"faces": [[20, 15, 20]], "body": "B", "sketch": "Cuts"}));
|
||||
let (f, v) = faces_volume(&mut s, "B");
|
||||
assert_eq!(f, 8, "two lines make three pieces of the top");
|
||||
assert!((v - 24000.0).abs() < 1e-6 * 24000.0, "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_that_misses_is_refused() {
|
||||
let mut s = boxed();
|
||||
let n = s.doc.features.len();
|
||||
assert!(s.execute("solid.split_face", &json!({"faces": [[10, 15, 20]], "plane": {"origin": [100, 0, 0], "normal": [1, 0, 0]}})).is_err());
|
||||
assert_eq!(s.doc.features.len(), n);
|
||||
}
|
||||
Reference in new issue
Block a user