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 <[email protected]> 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);
|
||||
}
|
||||
FeatureKind::Lip { face, .. } => pt(face),
|
||||
FeatureKind::BoundaryFill { cells, .. } => cells.iter_mut().for_each(pt),
|
||||
FeatureKind::Rest { position, direction, along, .. } => {
|
||||
pt(position);
|
||||
dir(direction);
|
||||
|
||||
@@ -633,6 +633,17 @@ pub enum FeatureKind {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
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: Vec<String>,
|
||||
@@ -925,6 +936,7 @@ impl FeatureKind {
|
||||
FeatureKind::Lip { .. } => "GrooveFeature",
|
||||
FeatureKind::SnapFit { .. } => "SnapFitFeature",
|
||||
FeatureKind::Rest { .. } => "RestFeature",
|
||||
FeatureKind::BoundaryFill { .. } => "BoundaryFillFeature",
|
||||
FeatureKind::SheetBase { .. } => "BaseFlangeFeature",
|
||||
FeatureKind::SheetContour { .. } => "ContourFlangeFeature",
|
||||
FeatureKind::SheetFlange { .. } => "EdgeFlangeFeature",
|
||||
@@ -984,6 +996,7 @@ impl FeatureKind {
|
||||
FeatureKind::Lip { .. } => "Groove",
|
||||
FeatureKind::SnapFit { .. } => "SnapFit",
|
||||
FeatureKind::Rest { .. } => "Rest",
|
||||
FeatureKind::BoundaryFill { .. } => "BoundaryFill",
|
||||
FeatureKind::SheetBase { .. } => "BaseFlange",
|
||||
FeatureKind::SheetContour { .. } => "ContourFlange",
|
||||
FeatureKind::SheetFlange { .. } => "EdgeFlange",
|
||||
@@ -1151,7 +1164,7 @@ impl FeatureKind {
|
||||
v.extend(depth.iter().map(String::as_str));
|
||||
}
|
||||
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::Thicken { thickness, .. } => v.push(thickness),
|
||||
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)?;
|
||||
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 } => {
|
||||
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}`"))) };
|
||||
|
||||
@@ -246,6 +246,7 @@ impl FeatureKind {
|
||||
| FeatureKind::SheetConvert { .. }
|
||||
| FeatureKind::Align { .. }
|
||||
| FeatureKind::Remove { .. }
|
||||
| FeatureKind::BoundaryFill { .. }
|
||||
| FeatureKind::Loft { .. }
|
||||
| FeatureKind::Sweep { .. }
|
||||
| FeatureKind::Import { .. }
|
||||
|
||||
@@ -11,6 +11,9 @@ use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec3};
|
||||
use crate::{Result, Session};
|
||||
|
||||
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(
|
||||
"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?",
|
||||
@@ -271,3 +274,18 @@ pub(super) fn snap_point(s: &Session, kind: &str, at: Vec3) -> Option<Vec3> {
|
||||
_ => 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!(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