mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 12:01:10 +00:00
engine: a shell that opens no face makes a closed hollow body
solid.shell refused an empty face list ("select 1…1000 faces"), and the
shell feature found its body from the first face picked. SMLib shells with no
face opened (the void sealed inside, its own shell): the command now takes
`faces: []`, the feature shells the one body there is or the one named
(`body`), and an outside or both-ways shell pushes no open face out of the
cavity. Rig shell/4009 declares the void's shell (`shells: 2`), as its method
says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6785,7 +6785,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
}
|
||||
FeatureKind::Shell { faces, thickness, body, direction, tangent_chain } => {
|
||||
let t = val(vals, thickness, Kind::Length)?;
|
||||
let i = body_at(st, body, &crate::plain(faces))?;
|
||||
// No face opened (a closed hollow body) picks no body: the one there is, or the named one.
|
||||
let i = match (faces.is_empty(), body, st.bodies.len()) {
|
||||
(true, None, 1) => 0,
|
||||
(true, None, _) => return Err(DocError::Invalid("a shell that opens no face needs the body named".into())),
|
||||
_ => body_at(st, body, &crate::plain(faces))?,
|
||||
};
|
||||
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
|
||||
let picks = crate::plain(faces);
|
||||
let open = if *tangent_chain {
|
||||
@@ -6823,7 +6828,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
// The cavity's faces are the shell's inner walls, named after the faces they offset.
|
||||
let inner = mb.before_as(crate::lineage::Operand::Inner(mb.name.clone()));
|
||||
let cavity = if inn > 0.0 { Traced::step(kernel::offset_faces(&mb.body, &walls, -inn)?, &[&inner]) } else { inner };
|
||||
let cavity = Traced::step(kernel::offset_faces(&cavity.body, &open, 2.0 * t + 1.0)?, &[&cavity]);
|
||||
// The open faces pushed out of the way (none: the cavity stays sealed inside).
|
||||
let cavity =
|
||||
if open.is_empty() { cavity } else { Traced::step(kernel::offset_faces(&cavity.body, &open, 2.0 * t + 1.0)?, &[&cavity]) };
|
||||
let cut = kernel::boolean(&outer.body, &cavity.body, BoolOp::Cut)?
|
||||
.ok_or_else(|| DocError::Invalid("the shell removed everything".into()))?;
|
||||
(cut, vec![outer, cavity])
|
||||
|
||||
@@ -97,7 +97,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("solid.shell", "Shell", shell)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("shell")
|
||||
.params("faces: [[x,y,z] points on the faces to remove, or face names from model.faces]; thickness: expr; direction?: inside|outside|both; tangent_chain?: bool (also open faces joined smoothly to the picked ones); body?"),
|
||||
.params("faces: [[x,y,z] points on the faces to remove, or face names from model.faces; none: a closed hollow body]; thickness: expr; direction?: inside|outside|both; tangent_chain?: bool (also open faces joined smoothly to the picked ones); body? (needed with no faces when there are several bodies)"),
|
||||
CommandSpec::new("solid.draft", "Draft", draft)
|
||||
.at("SOLID", "MODIFY")
|
||||
.icon("draft")
|
||||
@@ -2649,7 +2649,11 @@ fn face_points(s: &Session, p: &Value, cmd: &str) -> Result<Vec<Vec3>> {
|
||||
|
||||
fn shell(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "solid.shell";
|
||||
let faces = face_points(s, p, cmd)?;
|
||||
// No face opened: a closed hollow body, its void sealed inside.
|
||||
let faces = match p.get("faces").and_then(Value::as_array) {
|
||||
Some(a) if a.is_empty() => Vec::new(),
|
||||
_ => face_points(s, p, cmd)?,
|
||||
};
|
||||
let thickness = req_expr(cmd, p, "thickness")?;
|
||||
check_expr(s, &thickness, Kind::Length, cmd, "thickness")?;
|
||||
let direction = str_(p, "direction").map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
|
||||
@@ -86,3 +86,22 @@ fn sound(s: &Session) {
|
||||
assert!(b.body.validity().is_empty(), "{:?}", b.body.validity());
|
||||
}
|
||||
}
|
||||
|
||||
/// A shell that opens no face (rig shell/4009): a closed hollow body, the void sealed inside. A
|
||||
/// 40 × 30 × 12.5 box with a 2.5 wall inside leaves a 35 × 25 × 7.5 void; grown 2.5 outside, the
|
||||
/// walls are a 45 × 35 × 17.5 box less the original.
|
||||
#[test]
|
||||
fn a_shell_that_opens_no_face_seals_its_void() {
|
||||
for (direction, want, shells) in [("inside", 15000.0 - 35.0 * 25.0 * 7.5, 2), ("outside", 45.0 * 35.0 * 17.5 - 15000.0, 2)] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40.0, "width": 30.0, "height": 12.5}));
|
||||
run(&mut s, "solid.shell", json!({"faces": [], "thickness": 2.5, "direction": direction}));
|
||||
let errors: Vec<_> = s.model.results.iter().filter_map(|r| r.error.clone()).collect();
|
||||
assert!(errors.is_empty(), "{direction}: {errors:?}");
|
||||
let st = s.model.state();
|
||||
assert_eq!(st.bodies.len(), 1, "{direction}: one body");
|
||||
assert_eq!(st.bodies[0].body.shell_count(), shells, "{direction}: an outer and an inner shell");
|
||||
let got = volume(&s);
|
||||
assert!((got - want).abs() < 1e-6 * want, "{direction}: volume {got}, want {want}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"source": "analytic",
|
||||
"method": "outer box 40x30x12.5, void = inward offset box 35x25x7.5 (2.5 wall every side). V=15000-6562.5=8437.5; area=outer 2(1200+500+375)=4150 + void 2(875+187.5+262.5)... computed in python; faces 6+6=12; one body with an outer and an inner shell; centroid (20,15,6.25) by symmetry",
|
||||
"body_count": 1,
|
||||
"shells": 2,
|
||||
"volume_mm3": 8437.5,
|
||||
"area_mm2": 6800.0,
|
||||
"faces": 12,
|
||||
|
||||
Reference in New Issue
Block a user