Boundary Fill (SurfaceSculpt): solids from the cells tool bodies enclose (inside some tools, outside the rest), each picked by a point, as new bodies or joined/cut/intersected, tools optionally removed
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
0daeb0c9fc
commit
1e515547d8
6 files changed
+81
-1
No files matched your search
@@ -216,6 +216,7 @@ impl FeatureKind {
|
|||||||
dir(direction);
|
dir(direction);
|
||||||
}
|
}
|
||||||
FeatureKind::Lip { face, .. } => pt(face),
|
FeatureKind::Lip { face, .. } => pt(face),
|
||||||
|
FeatureKind::BoundaryFill { cells, .. } => cells.iter_mut().for_each(pt),
|
||||||
FeatureKind::Rest { position, direction, along, .. } => {
|
FeatureKind::Rest { position, direction, along, .. } => {
|
||||||
pt(position);
|
pt(position);
|
||||||
dir(direction);
|
dir(direction);
|
||||||
|
|||||||
@@ -633,6 +633,17 @@ pub enum FeatureKind {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
},
|
},
|
||||||
|
/// Boundary Fill: solids from the cells the tool bodies enclose (a cell: inside some
|
||||||
|
/// tools and outside the rest), each picked by a point inside it.
|
||||||
|
BoundaryFill {
|
||||||
|
tools: Vec<String>,
|
||||||
|
cells: Vec<Vec3>,
|
||||||
|
#[serde(default)]
|
||||||
|
operation: Operation,
|
||||||
|
/// Remove the tool bodies afterwards.
|
||||||
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
remove_tools: bool,
|
||||||
|
},
|
||||||
/// Remove bodies from the model (from here on in the timeline).
|
/// Remove bodies from the model (from here on in the timeline).
|
||||||
Remove {
|
Remove {
|
||||||
bodies: Vec<String>,
|
bodies: Vec<String>,
|
||||||
@@ -925,6 +936,7 @@ impl FeatureKind {
|
|||||||
FeatureKind::Lip { .. } => "GrooveFeature",
|
FeatureKind::Lip { .. } => "GrooveFeature",
|
||||||
FeatureKind::SnapFit { .. } => "SnapFitFeature",
|
FeatureKind::SnapFit { .. } => "SnapFitFeature",
|
||||||
FeatureKind::Rest { .. } => "RestFeature",
|
FeatureKind::Rest { .. } => "RestFeature",
|
||||||
|
FeatureKind::BoundaryFill { .. } => "BoundaryFillFeature",
|
||||||
FeatureKind::SheetBase { .. } => "BaseFlangeFeature",
|
FeatureKind::SheetBase { .. } => "BaseFlangeFeature",
|
||||||
FeatureKind::SheetContour { .. } => "ContourFlangeFeature",
|
FeatureKind::SheetContour { .. } => "ContourFlangeFeature",
|
||||||
FeatureKind::SheetFlange { .. } => "EdgeFlangeFeature",
|
FeatureKind::SheetFlange { .. } => "EdgeFlangeFeature",
|
||||||
@@ -984,6 +996,7 @@ impl FeatureKind {
|
|||||||
FeatureKind::Lip { .. } => "Groove",
|
FeatureKind::Lip { .. } => "Groove",
|
||||||
FeatureKind::SnapFit { .. } => "SnapFit",
|
FeatureKind::SnapFit { .. } => "SnapFit",
|
||||||
FeatureKind::Rest { .. } => "Rest",
|
FeatureKind::Rest { .. } => "Rest",
|
||||||
|
FeatureKind::BoundaryFill { .. } => "BoundaryFill",
|
||||||
FeatureKind::SheetBase { .. } => "BaseFlange",
|
FeatureKind::SheetBase { .. } => "BaseFlange",
|
||||||
FeatureKind::SheetContour { .. } => "ContourFlange",
|
FeatureKind::SheetContour { .. } => "ContourFlange",
|
||||||
FeatureKind::SheetFlange { .. } => "EdgeFlange",
|
FeatureKind::SheetFlange { .. } => "EdgeFlange",
|
||||||
@@ -1151,7 +1164,7 @@ impl FeatureKind {
|
|||||||
v.extend(depth.iter().map(String::as_str));
|
v.extend(depth.iter().map(String::as_str));
|
||||||
}
|
}
|
||||||
FeatureKind::ReplaceFace { target, .. } => plane_exprs(target, &mut v),
|
FeatureKind::ReplaceFace { target, .. } => plane_exprs(target, &mut v),
|
||||||
FeatureKind::Align { .. } | FeatureKind::Remove { .. } => {}
|
FeatureKind::Align { .. } | FeatureKind::Remove { .. } | FeatureKind::BoundaryFill { .. } => {}
|
||||||
FeatureKind::Stitch { tolerance, .. } => v.push(tolerance),
|
FeatureKind::Stitch { tolerance, .. } => v.push(tolerance),
|
||||||
FeatureKind::Thicken { thickness, .. } => v.push(thickness),
|
FeatureKind::Thicken { thickness, .. } => v.push(thickness),
|
||||||
FeatureKind::SurfaceTrim { plane, .. } => plane_exprs(plane, &mut v),
|
FeatureKind::SurfaceTrim { plane, .. } => plane_exprs(plane, &mut v),
|
||||||
|
|||||||
@@ -2241,6 +2241,36 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
|||||||
doc.resolve_plane(vals, plane, 0)?;
|
doc.resolve_plane(vals, plane, 0)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
FeatureKind::BoundaryFill { tools, cells, operation, remove_tools } => {
|
||||||
|
if tools.is_empty() || tools.len() > 64 || cells.is_empty() || cells.len() > 1000 {
|
||||||
|
return Err(DocError::Invalid("Boundary Fill needs 1…64 tool bodies and 1…1000 cells".into()));
|
||||||
|
}
|
||||||
|
let tb: Vec<ModelBody> =
|
||||||
|
tools.iter().map(|t| st.body(t).cloned().ok_or_else(|| DocError::Unknown(format!("body `{t}`")))).collect::<Result<_>>()?;
|
||||||
|
let mut made = Vec::new();
|
||||||
|
for p in cells {
|
||||||
|
// The cell: inside the tools that hold the point, outside the others.
|
||||||
|
let inside: Vec<bool> = tb.iter().map(|b| b.mesh().contains(*p)).collect();
|
||||||
|
let mut acc: Option<Body> = None;
|
||||||
|
for (b, _) in tb.iter().zip(&inside).filter(|(_, i)| **i) {
|
||||||
|
acc = match acc {
|
||||||
|
None => Some(b.body.clone()),
|
||||||
|
Some(a) => kernel::boolean(&a, &b.body, BoolOp::Intersect)?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let Some(mut cell) = acc else {
|
||||||
|
return Err(DocError::Invalid(format!("the point {p:?} is outside every tool: no enclosed cell there")));
|
||||||
|
};
|
||||||
|
for (b, _) in tb.iter().zip(&inside).filter(|(_, i)| !**i) {
|
||||||
|
cell = kernel::boolean(&cell, &b.body, BoolOp::Cut)?.ok_or_else(|| DocError::Invalid("a cell came out empty".into()))?;
|
||||||
|
}
|
||||||
|
made.push(cell);
|
||||||
|
}
|
||||||
|
if *remove_tools {
|
||||||
|
st.bodies.retain(|b| !tools.contains(&b.name));
|
||||||
|
}
|
||||||
|
apply_op(st, f, made, *operation, &[])
|
||||||
|
}
|
||||||
FeatureKind::Split { body, plane, tool } => {
|
FeatureKind::Split { body, plane, tool } => {
|
||||||
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
|
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}`"))) };
|
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Unknown(format!("body `{body}`"))) };
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ impl FeatureKind {
|
|||||||
| FeatureKind::SheetConvert { .. }
|
| FeatureKind::SheetConvert { .. }
|
||||||
| FeatureKind::Align { .. }
|
| FeatureKind::Align { .. }
|
||||||
| FeatureKind::Remove { .. }
|
| FeatureKind::Remove { .. }
|
||||||
|
| FeatureKind::BoundaryFill { .. }
|
||||||
| FeatureKind::Loft { .. }
|
| FeatureKind::Loft { .. }
|
||||||
| FeatureKind::Sweep { .. }
|
| FeatureKind::Sweep { .. }
|
||||||
| FeatureKind::Import { .. }
|
| FeatureKind::Import { .. }
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec3};
|
|||||||
use crate::{Result, Session};
|
use crate::{Result, Session};
|
||||||
|
|
||||||
pub static COMMANDS: &[CommandSpec] = &[
|
pub static COMMANDS: &[CommandSpec] = &[
|
||||||
|
CommandSpec::new("SurfaceSculpt", "Boundary Fill", boundary_fill).at("SOLID", "CREATE").icon("boundary_fill").params(
|
||||||
|
"tools: [bodies]; cells: [[x,y,z] a point inside each cell to fill (a cell: inside some tools, outside the rest)]; operation?: new|join|cut|intersect; remove_tools?: bool",
|
||||||
|
),
|
||||||
CommandSpec::new("solid.coil", "Coil", coil).at("SOLID", "CREATE").icon("coil").params(
|
CommandSpec::new("solid.coil", "Coil", coil).at("SOLID", "CREATE").icon("coil").params(
|
||||||
"diameter; two of revolutions (or turns), height, pitch; section_size; section?: circular|square; section_position?: inside|center|outside; \
|
"diameter; two of revolutions (or turns), height, pitch; section_size; section?: circular|square; section_position?: inside|center|outside; \
|
||||||
base?: [x,y,z]; axis?: X|Y|Z|[x,y,z] (default Z); start_angle?; clockwise?: bool; operation?, targets?, name?, body_name?",
|
base?: [x,y,z]; axis?: X|Y|Z|[x,y,z] (default Z); start_angle?; clockwise?: bool; operation?, targets?, name?, body_name?",
|
||||||
@@ -271,3 +274,18 @@ pub(super) fn snap_point(s: &Session, kind: &str, at: Vec3) -> Option<Vec3> {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn boundary_fill(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
let cmd = "SurfaceSculpt";
|
||||||
|
let tools = string_list(p, "tools");
|
||||||
|
let st = s.model.state();
|
||||||
|
if tools.is_empty() || tools.len() > 64 || tools.iter().any(|t| st.body(t).is_none()) {
|
||||||
|
return Err(bad(cmd, "`tools` must list 1…64 bodies"));
|
||||||
|
}
|
||||||
|
let cells: Vec<Vec3> = p.get("cells").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default();
|
||||||
|
if cells.is_empty() || cells.len() > 1000 {
|
||||||
|
return Err(bad(cmd, "`cells` must list 1…1000 points [x, y, z], one inside each cell"));
|
||||||
|
}
|
||||||
|
let operation = super::features::operation(p, cmd)?;
|
||||||
|
add_feature(s, p, FeatureKind::BoundaryFill { tools, cells, operation, remove_tools: bool_(p, "remove_tools").unwrap_or(false) })
|
||||||
|
}
|
||||||
@@ -1808,3 +1808,20 @@ fn align_with_snap_points() {
|
|||||||
assert!((min(0) - 20.0).abs() < 1e-6 && (min(1) - 20.0).abs() < 1e-6 && (min(2) - 10.0).abs() < 1e-6, "{m}");
|
assert!((min(0) - 20.0).abs() < 1e-6 && (min(1) - 20.0).abs() < 1e-6 && (min(2) - 10.0).abs() < 1e-6, "{m}");
|
||||||
assert!(s.execute("solid.align", &json!({"bodies": ["Cube"], "from": {"snap": "nope", "at": [0, 0, 0]}, "to": [0, 0, 0]})).is_err());
|
assert!(s.execute("solid.align", &json!({"bodies": ["Cube"], "from": {"snap": "nope", "at": [0, 0, 0]}, "to": [0, 0, 0]})).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Boundary Fill: the lens two overlapping boxes share, and one box less the other.
|
||||||
|
#[test]
|
||||||
|
fn boundary_fill_cells() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 20, "body_name": "A"}));
|
||||||
|
run(&mut s, "solid.box", json!({"corner": [10, 0, 0], "length": 20, "width": 20, "height": 20, "body_name": "B"}));
|
||||||
|
let r = run(&mut s, "SurfaceSculpt", json!({"tools": ["A", "B"], "cells": [[15, 10, 10]], "remove_tools": true}));
|
||||||
|
let m = run(&mut s, "inspect.measure", json!({}));
|
||||||
|
assert_eq!(m["body_count"], 1, "{r} {m}");
|
||||||
|
assert!(rel(m["total"]["volume_mm3"].as_f64().unwrap_or(0.0), 10.0 * 20.0 * 20.0) < 1e-9);
|
||||||
|
run(&mut s, "edit.undo", json!({}));
|
||||||
|
run(&mut s, "SurfaceSculpt", json!({"tools": ["A", "B"], "cells": [[5, 10, 10], [25, 10, 10]], "remove_tools": true}));
|
||||||
|
let m = run(&mut s, "inspect.measure", json!({}));
|
||||||
|
assert!(rel(m["total"]["volume_mm3"].as_f64().unwrap_or(0.0), 2.0 * 10.0 * 20.0 * 20.0) < 1e-9, "{m}");
|
||||||
|
assert!(s.execute("SurfaceSculpt", &json!({"tools": ["A"], "cells": [[100, 100, 100]]})).is_err());
|
||||||
|
}
|
||||||
Reference in new issue
Block a user