Pipe (solid or hollow, along sketch curves), Bounding Solid, Scale (uniform or per axis), Offset Face (planar faces), physical materials with mass in Measure; sweeps keep profile holes
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
37f4e4e49e
commit
840eddb875
9 files changed
+395
-24
No files matched your search
+26
-18
@@ -554,28 +554,36 @@ pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body>
|
||||
if path.is_empty() || path.len() > 1000 {
|
||||
return Err(KernelError::Invalid("the path needs 1…1000 segments".into()));
|
||||
}
|
||||
let w0 = wire(plane, ®ion.outer.ccw())?;
|
||||
// The outer loop and each hole, swept along the path; the ends are capped with holes.
|
||||
let mut starts = vec![wire(plane, ®ion.outer.ccw())?];
|
||||
for h in ®ion.holes {
|
||||
starts.push(wire(plane, &h.ccw().reversed())?);
|
||||
}
|
||||
guard("sweep", || {
|
||||
let mut shell = mt::Shell::new();
|
||||
let mut cur = w0.clone();
|
||||
for seg in path {
|
||||
let mut part: mt::Shell = match *seg {
|
||||
PathSeg::Line { a, b } => builder::tsweep(&cur, v3(b - a)),
|
||||
PathSeg::Arc { center, axis, angle, .. } => {
|
||||
let ax = axis.normalized().ok_or_else(|| KernelError::Invalid("arc axis".into()))?;
|
||||
builder::rsweep(&cur, p3(center), v3(ax), mt::Rad(angle))
|
||||
}
|
||||
};
|
||||
let next = part
|
||||
.extract_boundaries()
|
||||
.into_iter()
|
||||
.find(|w| w.edge_iter().all(|e| !cur.edge_iter().any(|c| c.id() == e.id())))
|
||||
.ok_or_else(|| KernelError::Failed("sweep: lost the profile".into()))?;
|
||||
shell.append(&mut part);
|
||||
cur = next.inverse();
|
||||
let mut ends = Vec::new();
|
||||
for w0 in &starts {
|
||||
let mut cur = w0.clone();
|
||||
for seg in path {
|
||||
let mut part: mt::Shell = match *seg {
|
||||
PathSeg::Line { a, b } => builder::tsweep(&cur, v3(b - a)),
|
||||
PathSeg::Arc { center, axis, angle, .. } => {
|
||||
let ax = axis.normalized().ok_or_else(|| KernelError::Invalid("arc axis".into()))?;
|
||||
builder::rsweep(&cur, p3(center), v3(ax), mt::Rad(angle))
|
||||
}
|
||||
};
|
||||
let next = part
|
||||
.extract_boundaries()
|
||||
.into_iter()
|
||||
.find(|w| w.edge_iter().all(|e| !cur.edge_iter().any(|c| c.id() == e.id())))
|
||||
.ok_or_else(|| KernelError::Failed("sweep: lost the profile".into()))?;
|
||||
shell.append(&mut part);
|
||||
cur = next.inverse();
|
||||
}
|
||||
ends.push(cur);
|
||||
}
|
||||
let faces: Vec<mt::Face> = shell.face_iter().cloned().collect();
|
||||
cap_and_close(faces, std::slice::from_ref(&w0), std::slice::from_ref(&cur))
|
||||
cap_and_close(faces, &starts, &ends)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ pub use measure::{BodyMeasure, measure};
|
||||
pub use meshbody::{MAX_TRIANGLES, mesh_body};
|
||||
pub use ops::{BoolOp, boolean, split_by_plane, transform, transform_matrix};
|
||||
pub use polybool::planar_boolean;
|
||||
pub use polyhedron::{HalfSpace, convex_polyhedron, draft, shell};
|
||||
pub use polyhedron::{HalfSpace, convex_polyhedron, draft, offset_faces, shell};
|
||||
pub use step::step_export;
|
||||
pub use step_in::{ImportedBody, StepImport, StepNode, step_import, step_import_shared};
|
||||
pub use topo::{CylinderFace, cylinder_face_at};
|
||||
|
||||
@@ -69,15 +69,15 @@ fn mismatch(a: &solvecraft_geom::Mesh, b: &solvecraft_geom::Mesh, result: &solve
|
||||
Some(bad as f64 / n.max(1) as f64)
|
||||
}
|
||||
|
||||
/// Boolean of two bodies. The result may be empty (`Ok(None)`) for a cut that removes
|
||||
/// everything or an intersection of disjoint bodies. Results are checked against volume bounds
|
||||
/// and retried with shifted copies and other tolerances when they fail or look wrong.
|
||||
thread_local! {
|
||||
/// Set while a boolean runs on bodies whose coincident faces were pushed apart (no
|
||||
/// second round of pushing).
|
||||
static APART: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Boolean of two bodies. The result may be empty (`Ok(None)`) for a cut that removes
|
||||
/// everything or an intersection of disjoint bodies. Results are checked against volume bounds
|
||||
/// and retried with shifted copies and other tolerances when they fail or look wrong.
|
||||
pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
a.require_brep("a boolean")?;
|
||||
b.require_brep("a boolean")?;
|
||||
|
||||
@@ -460,3 +460,33 @@ fn solve3(m: [[f64; 3]; 3], r: [f64; 3]) -> Option<[f64; 3]> {
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Move the planar faces at the given points along their outward normals by `distance`
|
||||
/// (negative: into the body). The body's other faces follow (its topology is kept).
|
||||
pub fn offset_faces(b: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
|
||||
b.require_brep("offset faces")?;
|
||||
if !distance.is_finite() || distance.abs() > 1e6 {
|
||||
return Err(KernelError::Invalid("offset distance".into()));
|
||||
}
|
||||
let healed = Body::new(crate::heal::heal(b.deep_copy(), b.size()))?;
|
||||
let size = healed.size();
|
||||
let mesh = healed.tessellate((size * 1e-3).max(1e-3))?;
|
||||
let mut chosen: Vec<usize> = Vec::new();
|
||||
for p in at {
|
||||
let near = mesh
|
||||
.triangles
|
||||
.iter()
|
||||
.zip(&mesh.tri_face)
|
||||
.filter_map(|(t, f)| mesh.tri(t).map(|[x, y, z]| (point_tri(*p, x, y, z), *f as usize)))
|
||||
.min_by(|a, c| a.0.total_cmp(&c.0));
|
||||
match near {
|
||||
Some((d, f)) if d < size * 1e-3 + 1e-6 => {
|
||||
if !chosen.contains(&f) {
|
||||
chosen.push(f);
|
||||
}
|
||||
}
|
||||
_ => return Err(KernelError::Invalid(format!("no face at {:?}", [p.x, p.y, p.z]))),
|
||||
}
|
||||
}
|
||||
offset_planar(&healed, |fi, _| if chosen.contains(&fi) { distance } else { 0.0 })
|
||||
}
|
||||
Reference in new issue
Block a user