Thanks @SychPL.
This commit is contained in:
1 parent
e2667b749d
commit
31b2ddc0cf
7 files changed
+131
-36
No files matched your search
+23
-6
@@ -4435,13 +4435,14 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
FeatureKind::Split { body, plane, tool, sketch, line } => {
|
||||
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 input = mb.traced();
|
||||
let parts = match tool {
|
||||
// A sketch line: the plane through it along the sketch's normal.
|
||||
_ if sketch.is_some() || line.is_some() => {
|
||||
let (Some(sketch), Some(line)) = (sketch, line) else {
|
||||
return Err(DocError::Invalid("a sketch line tool needs both its sketch and its line".into()));
|
||||
};
|
||||
kernel::split_by_plane(&mb.body, &sketch_line_plane(st, sketch.0, line)?)?
|
||||
split_by_plane_traced(&input, &sketch_line_plane(st, sketch.0, line)?)?
|
||||
}
|
||||
// A face of a body (its surface extended).
|
||||
Some(t) => {
|
||||
@@ -4451,19 +4452,18 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
|
||||
// The pieces keep the planar roles of the faces they share with the body (#565).
|
||||
kernel::split_body(&mb.body, &kernel::SplitTool::Face { body: &tb.body, face })?
|
||||
.into_iter()
|
||||
.map(|part| part.with_preserved_planar_sources(&mb.body))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?
|
||||
.map(|part| Ok(Traced::step(part.with_preserved_planar_sources(&mb.body)?, &[&input])))
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
}
|
||||
None => {
|
||||
let pl = doc.resolve_plane_in(vals, plane, 0, Some(st))?;
|
||||
kernel::split_by_plane(&mb.body, &pl)?
|
||||
split_by_plane_traced(&input, &pl)?
|
||||
}
|
||||
};
|
||||
if parts.len() < 2 {
|
||||
return Err(DocError::Invalid("the tool does not split the body".into()));
|
||||
}
|
||||
let input = mb.traced();
|
||||
let mut it = parts.into_iter().map(|part| Traced::step(part, &[&input]));
|
||||
let mut it = parts.into_iter();
|
||||
if let (Some(first), Some(slot)) = (it.next(), st.bodies.get_mut(i)) {
|
||||
*slot = ModelBody::traced_new(mb.name.clone(), first, mb.feature);
|
||||
}
|
||||
@@ -5320,6 +5320,23 @@ mod body_name_tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The parts of a body split by a plane, on its positive side first (only the non-empty ones).
|
||||
/// Each part's cut face is named `section` after the half-space box that cut it, so it takes an
|
||||
/// attachment that follows the plane (#565); the box's other faces never reach the part.
|
||||
fn split_by_plane_traced(input: &Traced, plane: &Plane) -> Result<Vec<Traced>> {
|
||||
let mut parts = Vec::new();
|
||||
for positive in [true, false] {
|
||||
let (part, tool) = kernel::half_space(&input.body, plane, positive)?;
|
||||
let Some(part) = part else { continue };
|
||||
// The box's cap in the plane: its start cap on the positive side, its end cap on the other.
|
||||
let section = Some("section".to_string());
|
||||
let caps = if positive { [section, None] } else { [None, section] };
|
||||
let tool = crate::lineage::ProfileRoles { sketch: None, region: None, caps }.tool(tool, None);
|
||||
parts.push(Traced::step(part, &[input, &tool]));
|
||||
}
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
/// The plane a straight sketch line sweeps along its sketch's normal: through the line, its x
|
||||
/// along the line and its y along the normal (a Split Body tool, #1552).
|
||||
pub fn sketch_line_plane(st: &ModelState, sketch: u64, line: &str) -> Result<Plane> {
|
||||
|
||||
@@ -326,3 +326,76 @@ fn every_flat_face_of_an_elbow_up_and_an_elbow_down_takes_a_sketch() {
|
||||
);
|
||||
every_flat_face_takes_a_sketch(&mut s);
|
||||
}
|
||||
|
||||
/// #565: Split Body by a plane a parameter moves (a raw plane, a construction plane or a sketch
|
||||
/// line). The face the split cuts on each part is named after the half-space that cut it
|
||||
/// (`F<split>:section`): a sketch and an offset plane attach to it and follow the plane.
|
||||
#[test]
|
||||
fn the_face_a_split_cuts_takes_a_sketch_and_an_offset_plane_that_follow_the_plane() {
|
||||
for tool in ["plane", "construction", "line"] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "split_x", "expression": "15 mm"}));
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 20, "height": 10, "body_name": "Block"}));
|
||||
let split = match tool {
|
||||
"plane" => json!({"body": "Block", "plane": {"origin": ["split_x", 0, 0], "normal": [1, 0, 0]}}),
|
||||
"construction" => {
|
||||
run(&mut s, "construct.plane.offset", json!({"base": "YZ", "offset": "split_x", "name": "Cut"}));
|
||||
json!({"body": "Block", "plane": "Cut"})
|
||||
}
|
||||
_ => {
|
||||
let id = run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Guide"}))["sketch"].as_u64().unwrap();
|
||||
run(&mut s, "sketch.line", json!({"points": [[15, -10], [15, 30]], "ids": ["cut"], "connect": false}));
|
||||
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "cut", "mode": "vertical"}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["origin", "cut.start"], "type": "horizontal", "value": "split_x"}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
json!({"body": "Block", "sketch": id, "line": "cut"})
|
||||
}
|
||||
};
|
||||
run(&mut s, "solid.split_body", split);
|
||||
no_errors(&s);
|
||||
// Each part's cut face (the two lie on one another, so each is picked by body and index).
|
||||
let cut_face = |s: &Session, body: &str, x: f64| {
|
||||
let state = s.model.state();
|
||||
let b = &state.body(body).unwrap().body;
|
||||
let index = (0..b.face_count())
|
||||
.find(|i| b.planar_face(*i).is_some_and(|p| p.normal().x.abs() > 0.999 && (p.origin.x - x).abs() < 1e-9))
|
||||
.unwrap_or_else(|| panic!("{tool}: no cut face on `{body}`"));
|
||||
json!({"face": [x, 10, 5], "body": body, "index": index})
|
||||
};
|
||||
let mut normals = Vec::new();
|
||||
for (k, body) in ["Block", "Block (1)"].into_iter().enumerate() {
|
||||
let pick = cut_face(&s, body, 15.);
|
||||
run(&mut s, "sketch.create", json!({"plane": pick, "name": format!("OnCut{k}")}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
let (plane, certificate) = healthy(&s, &format!("OnCut{k}"));
|
||||
assert_eq!(certificate, Some(FaceCertificate::Lineage), "{tool} {body}");
|
||||
assert!((plane.origin.x - 15.).abs() < 1e-9 && plane.normal().x.abs() > 0.999, "{tool} {body}: {plane:?}");
|
||||
normals.push(plane.normal().x);
|
||||
let pick = cut_face(&s, body, 15.);
|
||||
run(&mut s, "construct.plane.offset", json!({"base": pick, "offset": 2, "name": format!("Off{k}")}));
|
||||
}
|
||||
assert!(normals[0] * normals[1] < 0., "{tool}: the two cut faces face each other's way: {normals:?}");
|
||||
no_errors(&s);
|
||||
|
||||
run(&mut s, "parameters.change", json!({"name": "split_x", "expression": "20 mm"}));
|
||||
no_errors(&s);
|
||||
let offset = |s: &Session, name: &str| {
|
||||
let feature = s.doc.find_feature(name).map(|f| f.id).unwrap();
|
||||
let state = s.model.state();
|
||||
match state.construct.iter().find(|c| c.feature == feature).map(|c| &c.geom) {
|
||||
Some(solidcraft_doc::construct::ConstructGeom::Plane(p)) => *p,
|
||||
other => panic!("{tool} {name}: {other:?}"),
|
||||
}
|
||||
};
|
||||
for (k, n) in normals.iter().enumerate() {
|
||||
let (plane, _) = healthy(&s, &format!("OnCut{k}"));
|
||||
assert!((plane.origin.x - 20.).abs() < 1e-9 && (plane.normal().x - n).abs() < 1e-9, "{tool} OnCut{k}: {plane:?}");
|
||||
let off = offset(&s, &format!("Off{k}"));
|
||||
assert!((off.origin.x - (20. + 2. * n)).abs() < 1e-9, "{tool} Off{k}: {off:?}");
|
||||
}
|
||||
let t = reopened(&s);
|
||||
for k in 0..2 {
|
||||
assert!((healthy(&t, &format!("OnCut{k}")).0.origin.x - 20.).abs() < 1e-9, "{tool} reopened OnCut{k}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ pub use mesh_convert::convert_prismatic_mesh;
|
||||
pub use meshbody::{MAX_TRIANGLES, mesh_body};
|
||||
pub use native::{decode_native_body, encode_native_body, native_preview_mesh};
|
||||
pub use offset::with_same_surface;
|
||||
pub use ops::{BoolOp, boolean, can_join_touching_faces, join_apart, split_by_plane, transform, transform_matrix, union_apart};
|
||||
pub use ops::{BoolOp, boolean, can_join_touching_faces, half_space, join_apart, split_by_plane, transform, transform_matrix, union_apart};
|
||||
pub use polybool::{faceted_boolean, planar_boolean};
|
||||
pub use polyhedron::{HalfSpace, convex_polyhedron, draft, offset_faces, shell};
|
||||
pub use prism::{PLANAR_FIT_TOL, extrude_planar_regions, planar_regions_boolean};
|
||||
|
||||
@@ -1428,16 +1428,19 @@ pub fn transform(body: &Body, translate: Vec3, origin: Vec3, axis: Vec3, angle:
|
||||
pub fn split_by_plane(body: &Body, plane: &solidcraft_geom::Plane) -> Result<Vec<Body>> {
|
||||
let mut out = Vec::new();
|
||||
for positive in [true, false] {
|
||||
out.extend(half_space(body, plane, positive)?);
|
||||
out.extend(half_space(body, plane, positive)?.0);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The part of a body on one side of a plane (`None` when nothing is there): its intersection
|
||||
/// with [`half_box`] (whose history it carries, over the body and the box).
|
||||
pub(crate) fn half_space(body: &Body, plane: &solidcraft_geom::Plane, positive: bool) -> Result<Option<Body>> {
|
||||
/// The part of a body on one side of a plane (`None` when nothing is there), and the tool it is
|
||||
/// cut with: the part is the body's intersection with [`half_box`] (the tool), whose history it
|
||||
/// carries, over the body and the box. The box's face in the plane, the part's cut face, is its
|
||||
/// start cap on the positive side and its end cap on the negative side.
|
||||
pub fn half_space(body: &Body, plane: &solidcraft_geom::Plane, positive: bool) -> Result<(Option<Body>, Body)> {
|
||||
// The box's own construction roles describe no face of the design: the cut face has none.
|
||||
boolean(body, &half_box(body, plane, positive)?.with_face_sources(None), BoolOp::Intersect)
|
||||
let tool = half_box(body, plane, positive)?.with_face_sources(None);
|
||||
Ok((boolean(body, &tool, BoolOp::Intersect)?, tool))
|
||||
}
|
||||
|
||||
/// A box covering everything of `body` on one side of a plane, its start cap in the plane.
|
||||
|
||||
Reference in new issue
Block a user