Delete Face (face.delete, and faces in the Delete selection): a timeline feature that removes faces and heals the gap from the neighbouring planes through kernel::delete_faces; a picked face brings the rest of its surface (both halves of a hole's wall); face picks are stored by persistent name; a face its neighbours can't close is refused with the kernel's reason
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
9219eb5dc1
commit
3db6119cc6
11 files changed
+117
-6
No files matched your search
@@ -6,6 +6,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_doc::FeatureKind;
|
||||
use solvecraft_geom::Vec3;
|
||||
|
||||
use super::CommandSpec;
|
||||
use crate::params::{bad, bool_};
|
||||
@@ -63,6 +64,7 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut entities = Vec::new();
|
||||
let mut features: Vec<u64> = Vec::new();
|
||||
let mut bodies: Vec<String> = Vec::new();
|
||||
let mut faces: Vec<(String, Vec<Vec3>)> = Vec::new();
|
||||
for x in &items {
|
||||
match x {
|
||||
Sel::SketchCurve { id } | Sel::SketchPoint { id } | Sel::SketchConstraint { id } => entities.push(id.clone()),
|
||||
@@ -78,7 +80,10 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
},
|
||||
Sel::Axis { .. } => skip("the origin axes can't be deleted".into()),
|
||||
Sel::Body { name } => bodies.push(name.clone()),
|
||||
Sel::Face { .. } => skip("Delete Face: not available yet".into()),
|
||||
Sel::Face { body, point, .. } => match faces.iter_mut().find(|(b, _)| b == body) {
|
||||
Some((_, ps)) => ps.push(*point),
|
||||
None => faces.push((body.clone(), vec![*point])),
|
||||
},
|
||||
Sel::Edge { .. } | Sel::Vertex { .. } => skip("edges and vertices are deleted with their body or face".into()),
|
||||
Sel::Profile { .. } => skip("a profile goes with its sketch".into()),
|
||||
}
|
||||
@@ -93,6 +98,11 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
// Faces of a body that is going anyway are left to it; the rest become Delete Face features.
|
||||
for (body, points) in faces.iter().filter(|(b, _)| !bodies.contains(b)) {
|
||||
sub(s, "face.delete", json!({ "faces": points, "body": body }))?;
|
||||
done = true;
|
||||
}
|
||||
if !features.is_empty() {
|
||||
let ids: Vec<String> = features.iter().map(u64::to_string).collect();
|
||||
sub(s, "timeline.delete", json!({ "features": ids }))?;
|
||||
|
||||
@@ -66,6 +66,10 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("scale")
|
||||
.params("bodies: [names]; factor: expr | factors: [x, y, z] exprs; origin?: [x,y,z]"),
|
||||
CommandSpec::new("face.delete", "Delete Face", delete_face)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("delete")
|
||||
.params("faces: [[x,y,z] points on the faces]; body?. The neighbouring planes close the gap (a fillet, chamfer, hole or boss taken off)"),
|
||||
CommandSpec::new("solid.offset_face", "Offset Face", offset_face)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("offset_face")
|
||||
@@ -1188,6 +1192,11 @@ fn scale(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add_feature(s, p, FeatureKind::Scale { bodies, origin, factor, factors })
|
||||
}
|
||||
|
||||
fn delete_face(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let faces = face_points(p, "face.delete")?;
|
||||
add_feature(s, p, FeatureKind::DeleteFace { faces, body: str_(p, "body").map(str::to_string) })
|
||||
}
|
||||
|
||||
fn offset_face(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.offset_face";
|
||||
let faces = face_points(p, cmd)?;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//! Delete Face: a fillet, a hole and a chamfer taken off, healed from the neighbouring planes.
|
||||
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn volume(s: &mut Session, b: &str) -> f64 {
|
||||
run(s, "inspect.measure", json!({ "bodies": [b] }))["bodies"][0]["volume_mm3"].as_f64().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_a_fillet_face() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20, "body_name": "B"}));
|
||||
run(&mut s, "solid.fillet", json!({"edges": [[40, 15, 20]], "radius": 5}));
|
||||
assert!(volume(&mut s, "B") < 24000.0 - 1.0);
|
||||
// A point on the fillet's surface: 45° round from its axis (35, ·, 15).
|
||||
let h = 5.0 * std::f64::consts::FRAC_1_SQRT_2;
|
||||
let r = run(&mut s, "face.delete", json!({"faces": [[35.0 + h, 15, 15.0 + h]]}));
|
||||
assert!(r["feature"].is_u64(), "{r}");
|
||||
assert!((volume(&mut s, "B") - 24000.0).abs() < 1e-3 * 24000.0, "{}", volume(&mut s, "B"));
|
||||
// Undo-able as a timeline feature, and named after its kind.
|
||||
let f = s.doc.features.last().unwrap();
|
||||
assert_eq!(f.kind.type_name(), "DeleteFaceFeature");
|
||||
}
|
||||
|
||||
/// A through hole picked in the Delete selection: both halves of its wall go, and the plate
|
||||
/// closes up.
|
||||
#[test]
|
||||
fn delete_a_hole_through_the_selection() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 50, "width": 30, "height": 10, "body_name": "Plate"}));
|
||||
run(&mut s, "solid.cylinder", json!({"base": [25, 15, -1], "radius": 4, "height": 12, "body_name": "Drill"}));
|
||||
run(&mut s, "solid.combine", json!({"target": "Plate", "tools": ["Drill"], "operation": "cut"}));
|
||||
let v = volume(&mut s, "Plate");
|
||||
assert!(v < 15000.0 - 100.0, "{v}");
|
||||
run(&mut s, "selection.delete", json!({"items": [{"type": "face", "body": "Plate", "index": 0, "point": [29, 15, 5]}]}));
|
||||
assert!((volume(&mut s, "Plate") - 15000.0).abs() < 1e-3 * 15000.0, "{}", volume(&mut s, "Plate"));
|
||||
}
|
||||
|
||||
/// A face whose neighbours can't close the gap is refused, and the design is unchanged.
|
||||
#[test]
|
||||
fn a_box_face_cannot_be_deleted() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "B"}));
|
||||
let n = s.doc.features.len();
|
||||
let e = s.execute("face.delete", &json!({"faces": [[5, 5, 10]]})).unwrap_err().to_string();
|
||||
assert!(e.contains("Delete Face"), "{e}");
|
||||
assert_eq!(s.doc.features.len(), n);
|
||||
}
|
||||
@@ -425,6 +425,8 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod delete_face_tests;
|
||||
#[cfg(test)]
|
||||
mod extent_tests;
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in new issue
Block a user