Shell: an open face next to a concave edge no longer cuts the wall past it (#1495)
The pushed-out cavity is trimmed so that past an open face's plane it only reaches through the open face itself.
This commit is contained in:
1 parent
723c7b61c4
commit
2141a3802a
2 files changed
+143
No files matched your search
@@ -0,0 +1,58 @@
|
|||||||
|
//! A shell whose open face meets a concave edge must not cut into the wall past that edge.
|
||||||
|
//!
|
||||||
|
//! Lower box `[0, 25] × [0, 17.5] × [0, 35]`, top box `[7.5, 22.5] × [0, w] × [35, 42.5]` with
|
||||||
|
//! `w ≤ 17.5`, and the shell opens the top box's left face (x = 7.5). That face's bottom edge is
|
||||||
|
//! concave: the lower box's top runs on from it. With walls `t` thick, the cavity is
|
||||||
|
//!
|
||||||
|
//! - in the lower box: `[t, 25 − t] × [t, 17.5 − t] × [t, 35 − t]`
|
||||||
|
//! - in the top box, out to the open face: `[7.5, 22.5 − t] × [t, w − t] × [35 − t, 42.5 − t]`
|
||||||
|
//!
|
||||||
|
//! The shell's volume is the solid's volume less those two boxes. The open face used to be
|
||||||
|
//! pushed out past the lower box's top, cutting a strip through that wall.
|
||||||
|
use serde_json::json;
|
||||||
|
use solidcraft_engine::Session;
|
||||||
|
|
||||||
|
fn run(s: &mut Session, id: &str, p: serde_json::Value) {
|
||||||
|
if let Err(e) = s.execute(id, &p) {
|
||||||
|
panic!("{id} {p}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shell_volume(w: f64, t: f64) -> f64 {
|
||||||
|
let solid = 25.0 * 17.5 * 35.0 + 15.0 * w * 7.5;
|
||||||
|
let lower = (25.0 - 2.0 * t) * (17.5 - 2.0 * t) * (35.0 - 2.0 * t);
|
||||||
|
let upper = (15.0 - t) * (w - 2.0 * t) * 7.5;
|
||||||
|
solid - lower - upper
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shelled(w: f64, t: f64) -> Session {
|
||||||
|
let mut s = Session::default();
|
||||||
|
run(&mut s, "solid.box", json!({"height": 35.0, "length": 25.0, "width": 17.5}));
|
||||||
|
run(&mut s, "solid.box", json!({"corner": [7.5, 0.0, 35.0], "height": 7.5, "length": 15.0, "operation": "join", "width": w}));
|
||||||
|
run(&mut s, "solid.shell", json!({"faces": [[7.5, w / 2.0, 38.75]], "thickness": t}));
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(s: &Session, want: f64) {
|
||||||
|
let errors: Vec<_> = s.model.results.iter().filter_map(|r| r.error.clone()).collect();
|
||||||
|
assert!(errors.is_empty(), "{errors:?}");
|
||||||
|
let st = s.model.state();
|
||||||
|
assert_eq!(st.bodies.len(), 1, "one body");
|
||||||
|
let b = &st.bodies[0].body;
|
||||||
|
assert!(b.validity().is_empty(), "{:?}", b.validity());
|
||||||
|
assert_eq!(b.shell_count(), 1, "one shell");
|
||||||
|
let got = solidcraft_kernel::measure(b).map(|m| m.volume).unwrap_or(f64::NAN);
|
||||||
|
assert!((got - want).abs() < 1e-6 * want, "shell volume {got}, want {want}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_face_flush_with_the_back_keeps_the_wall_past_its_concave_edge() {
|
||||||
|
for t in [1.0, 1.5] {
|
||||||
|
check(&shelled(17.5, t), shell_volume(17.5, t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_face_inside_the_footprint_keeps_the_wall_past_its_concave_edge() {
|
||||||
|
check(&shelled(15.0, 1.5), shell_volume(15.0, 1.5));
|
||||||
|
}
|
||||||
@@ -561,6 +561,14 @@ fn shell_opened(b: &Body, healed: &Body, open: &[Vec3], opened: &[usize], thickn
|
|||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => return shell_by_opening(healed, opened, thickness, margin).map_err(|_| e),
|
Err(e) => return shell_by_opening(healed, opened, thickness, margin).map_err(|_| e),
|
||||||
};
|
};
|
||||||
|
let cavity = match opening_overreach(healed, &cavity, opened, thickness, margin) {
|
||||||
|
Ok(Some(trimmed)) => trimmed,
|
||||||
|
Ok(None) => cavity,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("shell: could not trim the opening to the open faces ({e}); the wall next to it may be cut");
|
||||||
|
cavity
|
||||||
|
}
|
||||||
|
};
|
||||||
let r = crate::ops::boolean(healed, &cavity, crate::BoolOp::Cut);
|
let r = crate::ops::boolean(healed, &cavity, crate::BoolOp::Cut);
|
||||||
if r.is_err()
|
if r.is_err()
|
||||||
&& let Ok(x) = shell_by_opening(healed, opened, thickness, margin)
|
&& let Ok(x) = shell_by_opening(healed, opened, thickness, margin)
|
||||||
@@ -605,6 +613,83 @@ fn shell_opened(b: &Body, healed: &Body, open: &[Vec3], opened: &[usize], thickn
|
|||||||
/// A shell made from the cavity with every face moved in by the thickness (open faces too),
|
/// A shell made from the cavity with every face moved in by the thickness (open faces too),
|
||||||
/// joined with each open face's moved copy swept out through the body. No history: the
|
/// joined with each open face's moved copy swept out through the body. No history: the
|
||||||
/// opening's walls, swept from the cavity's edges, come from nothing of `b`.
|
/// opening's walls, swept from the cavity's edges, come from nothing of `b`.
|
||||||
|
/// The cavity with each open face pushed out (`cavity`), trimmed so that beyond an open face's
|
||||||
|
/// plane it only reaches through the open face itself; `None` when it already does.
|
||||||
|
///
|
||||||
|
/// The pushed-out face carries the cavity past every edge of the open face. Past a convex edge
|
||||||
|
/// that is outside the body, but past a concave one (a box on a box, its side face open where it
|
||||||
|
/// meets the lower box's top) it runs on into the neighbouring wall and cuts a strip through it,
|
||||||
|
/// as wide as the push. The trimmed cavity is the shell's cavity with every face moved in,
|
||||||
|
/// together with the pushed-out cavity's parts that are on the body's side of every open face's
|
||||||
|
/// plane or in front of that open face.
|
||||||
|
fn opening_overreach(healed: &Body, cavity: &Body, opened: &[usize], thickness: f64, margin: f64) -> Result<Option<Body>> {
|
||||||
|
let size = healed.size();
|
||||||
|
let tol = size * 1e-6 + 1e-9;
|
||||||
|
let faces: Vec<mt::Face> = healed.solid.face_iter().cloned().collect();
|
||||||
|
// Each open face's plane (outward normal, offset) and its triangles.
|
||||||
|
let mesh = healed.tessellate((size * 1e-3).max(1e-3))?;
|
||||||
|
let mut open: Vec<(&mt::Face, Vec3, f64, Vec<[Vec3; 3]>)> = Vec::new();
|
||||||
|
for fi in opened {
|
||||||
|
let Some(f) = faces.get(*fi) else { return Ok(None) };
|
||||||
|
let mt::Surface::Plane(pl) = f.oriented_surface() else { return Ok(None) };
|
||||||
|
let n = pl.normal();
|
||||||
|
let Some(n) = Vec3::new(n.x, n.y, n.z).normalized() else { return Ok(None) };
|
||||||
|
let d = n.dot(from_p3(pl.origin()));
|
||||||
|
let tris: Vec<[Vec3; 3]> =
|
||||||
|
mesh.triangles.iter().zip(&mesh.tri_face).filter(|(_, f)| **f as usize == *fi).filter_map(|(t, _)| mesh.tri(t)).collect();
|
||||||
|
if tris.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
open.push((f, n, d, tris));
|
||||||
|
}
|
||||||
|
// Only a corner of the cavity past an open face's plane and not in front of that face can be
|
||||||
|
// in a wall; with none, the cavity is right as it is.
|
||||||
|
let overreach = cavity.solid.vertex_iter().any(|v| {
|
||||||
|
let q = from_p3(v.point());
|
||||||
|
open.iter().any(|(_, n, d, tris)| {
|
||||||
|
let h = n.dot(q) - d;
|
||||||
|
h > tol && {
|
||||||
|
let on = q - *n * h;
|
||||||
|
!tris.iter().any(|[a, b, c]| point_tri(on, *a, *b, *c) < size * 1e-4 + 1e-6)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !overreach {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let closed = offset_planar(healed, |_, _| -thickness)?;
|
||||||
|
// Keep: on the body's side of each open plane, or in front of the open face.
|
||||||
|
let reach = size * 4.0;
|
||||||
|
let mut keep: Option<Body> = None;
|
||||||
|
for (f, n, d, _) in &open {
|
||||||
|
let c = *n * *d;
|
||||||
|
let mut hs = vec![HalfSpace { n: *n, d: *d }];
|
||||||
|
for axis in [Vec3::X, Vec3::Y, Vec3::Z] {
|
||||||
|
hs.push(HalfSpace { n: axis, d: axis.dot(c) + reach });
|
||||||
|
hs.push(HalfSpace { n: axis * -1.0, d: -axis.dot(c) + reach });
|
||||||
|
}
|
||||||
|
let (side, _) = polyhedron(&hs)?;
|
||||||
|
// In front of the face: its prism, from half a wall inside the body (overlapping `side`
|
||||||
|
// rather than touching it) to past the pushed-out face.
|
||||||
|
let inset = thickness * 0.5;
|
||||||
|
let prism = guard("shell opening prism", || {
|
||||||
|
let start = builder::translated(*f, v3(*n * -inset));
|
||||||
|
let solid: Solid = builder::tsweep(&start, v3(*n * (inset + margin * 2.0)));
|
||||||
|
Ok(solid)
|
||||||
|
})?;
|
||||||
|
let prism = Body::new(prism)?;
|
||||||
|
let k = crate::ops::boolean(&side, &prism, crate::BoolOp::Union)?.ok_or_else(|| KernelError::Failed("shell: opening".into()))?;
|
||||||
|
keep = Some(match keep {
|
||||||
|
None => k,
|
||||||
|
Some(acc) => crate::ops::boolean(&acc, &k, crate::BoolOp::Intersect)?.ok_or_else(|| KernelError::Failed("shell: opening".into()))?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let Some(keep) = keep else { return Ok(None) };
|
||||||
|
let kept = crate::ops::boolean(cavity, &keep, crate::BoolOp::Intersect)?.ok_or_else(|| KernelError::Failed("shell: opening".into()))?;
|
||||||
|
let trimmed = crate::ops::boolean(&closed, &kept, crate::BoolOp::Union)?.ok_or_else(|| KernelError::Failed("shell: opening".into()))?;
|
||||||
|
Ok(Some(trimmed))
|
||||||
|
}
|
||||||
|
|
||||||
fn shell_by_opening(b: &Body, opened: &[usize], thickness: f64, reach: f64) -> Result<Body> {
|
fn shell_by_opening(b: &Body, opened: &[usize], thickness: f64, reach: f64) -> Result<Body> {
|
||||||
let closed = offset_planar(b, |_, _| -thickness)?;
|
let closed = offset_planar(b, |_, _| -thickness)?;
|
||||||
let faces: Vec<mt::Face> = closed.solid.face_iter().cloned().collect();
|
let faces: Vec<mt::Face> = closed.solid.face_iter().cloned().collect();
|
||||||
|
|||||||
Reference in new issue
Block a user