mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
Kernel: a drafted extrusion of a profile with holes, every wall leaning on the material's side
SMLib's draft extrusion takes one loop, so a tapered extrude of a region with holes was refused
("SMLib draft extrusion of a profile with holes"), and designs that use one no longer opened
(modern-plastic's Rest: design_corpus, action_matrix a_small_hole_through_the_wall_of_a_plastic_rest).
As other CAD does, every wall now leans by the taper relative to the pull on the material's
side: the outer loop is drafted, and each hole, taken counter-clockwise, is drafted the other
way and cut from it, so a taper that narrows the outer boundary widens the holes. Exact: each
loop is SMLib's draft extrusion of it, the walls' roles are found as the foot's are. Refused,
saying why, where the taper would make a hole meet the outer wall or another hole at either
end of the extrusion.
Test: a 40 mm square with a 10 mm square hole, 30 up at 5° inward, is the one frustum less the
other to 1e-6 with ten faces; at 30° the hole would pass the outer wall and is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+50
-13
@@ -799,24 +799,61 @@ pub fn extrude_tapered_between(
|
||||
smlib_tapered(plane, region, from, to, foot, dir_sign.signum(), taper, corner)
|
||||
}
|
||||
|
||||
/// [`extrude_tapered_between`] through SMLib's draft extrusion: one loop (SMLib's takes no
|
||||
/// holes), its section at `from` the profile offset by the taper, after any straight foot.
|
||||
/// [`extrude_tapered_between`] through SMLib's draft extrusion, its section at `from` the
|
||||
/// profile offset by the taper, after any straight foot. SMLib's draft extrusion takes one loop:
|
||||
/// a region with holes is its outer loop drafted, less each hole drafted the other way (every
|
||||
/// wall leaning by the taper on the material's side), refused only where the taper would make a
|
||||
/// hole meet the outer wall or another hole within the extrusion.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn smlib_tapered(plane: &Plane, region: &Region2, from: f64, to: f64, foot: f64, sign: f64, taper: f64, corner: DraftCorner) -> Result<Body> {
|
||||
if !region.holes.is_empty() {
|
||||
return Err(crate::smlib_sweeps::refuse("draft extrusion of a profile with holes (SMLib's takes one loop)"));
|
||||
}
|
||||
let outer = oriented(region)?.map_loops(|l| l.merged_lines(continues_straight)).outer;
|
||||
let bottom = if from == 0.0 || taper == 0.0 {
|
||||
outer
|
||||
} else {
|
||||
let moved = offset_loop(&outer.lp, from * taper.tan())?;
|
||||
let segs: Option<Vec<Seg2>> = moved.into_iter().collect();
|
||||
let joined = oriented(region)?.map_loops(|l| l.merged_lines(continues_straight));
|
||||
// A loop's section `at` along the extrusion: counter-clockwise, grown by `taper` (a hole's
|
||||
// the other way, as the wall of the material round it leans).
|
||||
let section = |l: &NumberedLoop, at: f64, taper: f64| -> Result<NumberedLoop> {
|
||||
let ccw = l.oriented(false);
|
||||
if at == 0.0 || taper == 0.0 {
|
||||
return Ok(ccw);
|
||||
}
|
||||
let segs: Option<Vec<Seg2>> = offset_loop(&ccw.lp, at * taper.tan())?.into_iter().collect();
|
||||
let segs = segs.ok_or_else(|| crate::smlib_sweeps::refuse("draft extrusion that starts before its profile where the taper closes a side"))?;
|
||||
NumberedLoop { lp: Loop2 { segs }, from: outer.from }
|
||||
Ok(NumberedLoop { lp: Loop2 { segs }, from: ccw.from })
|
||||
};
|
||||
let outer = section(&joined.outer, from, taper)?;
|
||||
let draft = (taper != 0.0).then_some(crate::smlib_sweeps::Draft { taper, foot, corner });
|
||||
crate::smlib_sweeps::extrude(plane, &crate::smlib_sweeps::pieces([&bottom]), Some(region), from * sign, to - from, sign, draft)
|
||||
if joined.holes.is_empty() {
|
||||
return crate::smlib_sweeps::extrude(plane, &crate::smlib_sweeps::pieces([&outer]), Some(region), from * sign, to - from, sign, draft);
|
||||
}
|
||||
let Some(draft) = draft else {
|
||||
return Err(KernelError::Invalid("a tapered extrusion needs a taper".into()));
|
||||
};
|
||||
let holes: Vec<NumberedLoop> = joined.holes.iter().map(|h| section(h, from, -taper)).collect::<Result<_>>()?;
|
||||
// The sections where the walls are farthest moved, the two ends: each hole stays inside the
|
||||
// outer loop and clear of the other holes there (the walls are planes, cones and their like,
|
||||
// which move monotonically in between).
|
||||
for at in [from, to] {
|
||||
let o = section(&joined.outer, at, taper)?;
|
||||
let hs: Vec<NumberedLoop> = joined.holes.iter().map(|h| section(h, at, -taper)).collect::<Result<_>>()?;
|
||||
let pts = |l: &NumberedLoop| l.lp.segs.iter().flat_map(|s| s.polyline(1e-3)).collect::<Vec<Vec2>>();
|
||||
// Apart: no point of either inside the other.
|
||||
let apart = |a: &NumberedLoop, b: &NumberedLoop| !pts(a).iter().any(|p| b.lp.contains(*p)) && !pts(b).iter().any(|p| a.lp.contains(*p));
|
||||
for (i, h) in hs.iter().enumerate() {
|
||||
let inside = pts(h).iter().all(|p| o.lp.contains(*p)) && !pts(&o).iter().any(|p| h.lp.contains(*p));
|
||||
if !inside || hs.iter().skip(i + 1).any(|k| !apart(h, k)) {
|
||||
return Err(KernelError::Invalid("the draft makes a hole meet the outer wall or another hole".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let all: Vec<NumberedLoop> = std::iter::once(outer.clone()).chain(holes.iter().map(|h| h.oriented(true))).collect();
|
||||
crate::smlib_sweeps::extrude_drafted_with_holes(
|
||||
plane,
|
||||
&crate::smlib_sweeps::pieces([&outer]),
|
||||
&holes.iter().map(|h| crate::smlib_sweeps::pieces([h])).collect::<Vec<_>>(),
|
||||
&crate::smlib_sweeps::pieces(all.iter()),
|
||||
from * sign,
|
||||
to - from,
|
||||
sign,
|
||||
draft,
|
||||
)
|
||||
}
|
||||
|
||||
/// A planar region on `plane` as the schema 4 record of one face (an open sheet): the plane's
|
||||
|
||||
@@ -71,8 +71,8 @@ pub(crate) struct Draft {
|
||||
}
|
||||
|
||||
pub(crate) use imp::{
|
||||
body_from_record, curve, extrude, face_section, loft, loft_rails, loft_to_point, path_curve, project_onto_face, revolve, screw, screw_radial,
|
||||
sphere, sweep, sweep_controlled,
|
||||
body_from_record, curve, extrude, extrude_drafted_with_holes, face_section, loft, loft_rails, loft_to_point, path_curve, project_onto_face,
|
||||
revolve, screw, screw_radial, sphere, sweep, sweep_controlled,
|
||||
};
|
||||
|
||||
/// Refuses what no SMLib sweep does, before any work.
|
||||
@@ -447,6 +447,79 @@ mod imp {
|
||||
with_history(body, &roles, pieces)
|
||||
}
|
||||
|
||||
/// The solid `pieces` (one closed loop) on `plane` sweep, moved `from` along its normal and
|
||||
/// drafted `d` over `distance` along `sign` × the normal, as [`extrude`] builds it, without
|
||||
/// its roles.
|
||||
fn drafted_loop(plane: &Plane, pieces: &[Piece], from: f64, distance: f64, sign: f64, d: Draft) -> Result<Brep> {
|
||||
let base = plane.offset(from);
|
||||
let n = plane.x.cross(plane.y).normalized().ok_or_else(|| KernelError::Invalid("profile plane".into()))?;
|
||||
let dir = n * sign.signum();
|
||||
if d.foot > 0.0 {
|
||||
if !(d.foot < distance) {
|
||||
return Err(KernelError::Invalid(format!("taper foot = {}", d.foot)));
|
||||
}
|
||||
let knee = plane.offset(from + sign.signum() * d.foot);
|
||||
let lean = -(distance * d.taper.tan() / (distance - d.foot)).atan();
|
||||
let prism =
|
||||
Brep::extrude(&curves(&base, pieces)?, p(dir), d.foot, 0.0, DraftCorner::default(), true).map_err(|e| failed("extrude", e))?;
|
||||
let slope =
|
||||
Brep::extrude(&curves(&knee, pieces)?, p(dir), distance - d.foot, lean, corner(d.corner), true).map_err(|e| failed("extrude", e))?;
|
||||
let brep = prism.boolean(&slope, solidcraft_smlib::BooleanOp::Union).map_err(|e| failed("extrude", e))?;
|
||||
return brep.ok_or_else(|| failed("extrude", "nothing is left"));
|
||||
}
|
||||
Brep::extrude(&curves(&base, pieces)?, p(dir), distance, -d.taper, corner(d.corner), true).map_err(|e| failed("extrude", e))
|
||||
}
|
||||
|
||||
/// A drafted extrusion of a region with holes: SMLib's draft extrusion takes one loop, so the
|
||||
/// outer loop is drafted and each hole, turned counter-clockwise, is drafted the other way
|
||||
/// and cut from it. Every wall leans by the taper on the material's side, as a single
|
||||
/// drafted sweep would: a taper that narrows the outer boundary widens the holes. `outer` and
|
||||
/// `holes` are the loops' pieces at `from` as [`extrude`] takes them, `all` the region's
|
||||
/// pieces in their numbering (outer, then the holes as they bound the material), which the
|
||||
/// walls' roles name.
|
||||
pub(crate) fn extrude_drafted_with_holes(
|
||||
plane: &Plane,
|
||||
outer: &[Piece],
|
||||
holes: &[Vec<Piece>],
|
||||
all: &[Piece],
|
||||
from: f64,
|
||||
distance: f64,
|
||||
sign: f64,
|
||||
draft: Draft,
|
||||
) -> Result<Body> {
|
||||
let mut brep = drafted_loop(plane, outer, from, distance, sign, draft)?;
|
||||
for h in holes {
|
||||
let cutter = drafted_loop(plane, h, from, distance, sign, Draft { taper: -draft.taper, ..draft })?;
|
||||
brep = brep
|
||||
.boolean(&cutter, solidcraft_smlib::BooleanOp::Subtract)
|
||||
.map_err(|e| failed("extrude", e))?
|
||||
.ok_or_else(|| KernelError::Invalid("the draft leaves nothing of the profile".into()))?;
|
||||
}
|
||||
let body = body_of(&brep, "extrude")?;
|
||||
let base = plane.offset(from);
|
||||
let top = plane.offset(from + sign.signum() * distance);
|
||||
let low = roles(&body, &base, Some(&top), &seeds(&base, all, |q| q), &[], false)?;
|
||||
let roles: Vec<Role> = if draft.foot > 0.0 {
|
||||
// The sloping walls stand on their edges at the knee, as in [`extrude`].
|
||||
let knee = plane.offset(from + sign.signum() * draft.foot);
|
||||
let high = roles(&body, &knee, Some(&top), &seeds(&knee, all, |q| q), &[], false)?;
|
||||
low.into_iter()
|
||||
.zip(high)
|
||||
.map(|(a, b)| match (a, b) {
|
||||
(Role::Wall(mut x), Role::Wall(y)) => {
|
||||
x.extend(y);
|
||||
Role::Wall(x)
|
||||
}
|
||||
(Role::Unknown, w @ Role::Wall(_)) => w,
|
||||
(a, _) => a,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
low
|
||||
};
|
||||
with_history(body, &roles, all)
|
||||
}
|
||||
|
||||
/// The construction sources of a straight extrusion's faces along the normal of `sketch`, as
|
||||
/// [`crate::extrude`] gives them: each cap the region at its end (both caps found once
|
||||
/// each), each wall swept from one piece that piece as drawn, a line's with the wall's plane.
|
||||
|
||||
@@ -463,6 +463,21 @@ fn tapered_extrudes() {
|
||||
assert!(rel(m.volume, v) < 5e-4, "{} vs {v}", m.volume);
|
||||
assert!(m.bbox.max.z < 1e-6 && m.bbox.min.z > -10.0 - 1e-6, "{:?}", m.bbox);
|
||||
assert!(extrude_tapered(&Plane::XY, &c, 10.0, 1.0, -60f64.to_radians(), crate::DraftCorner::default()).is_err(), "closes the profile");
|
||||
// The square with a 10 × 10 square hole, 30 up, 5° inward: every wall leans in toward the
|
||||
// material's outside, so the outer frustum narrows and the hole's widens; the solid is the
|
||||
// one frustum less the other, with each wall a face of its own (four out, four in, two caps).
|
||||
let t = 5f64.to_radians().tan();
|
||||
let hole = Loop2::polygon(&[Vec2::new(-5.0, -5.0), Vec2::new(-5.0, 5.0), Vec2::new(5.0, 5.0), Vec2::new(5.0, -5.0)]);
|
||||
let ring = Region2 { outer: sq.outer.clone(), holes: vec![hole] };
|
||||
let b = extrude_tapered(&Plane::XY, &ring, 30.0, 1.0, -5f64.to_radians(), crate::DraftCorner::default()).unwrap();
|
||||
let frustum = |a0: f64, a1: f64| 30.0 / 3.0 * (a0 * a0 + a1 * a1 + a0 * a1);
|
||||
let v = frustum(40.0, 40.0 - 60.0 * t) - frustum(10.0, 10.0 + 60.0 * t);
|
||||
let m = measure(&b).unwrap();
|
||||
assert!(rel(m.volume, v) < 1e-6, "{} vs {v}", m.volume);
|
||||
assert_eq!(m.merged.faces, 10);
|
||||
// At 30° the hole would grow past the outer wall: refused, saying why.
|
||||
let e = extrude_tapered(&Plane::XY, &ring, 30.0, 1.0, -30f64.to_radians(), crate::DraftCorner::default()).unwrap_err();
|
||||
assert!(e.to_string().contains("hole"), "{e}");
|
||||
}
|
||||
|
||||
/// A slot (two lines and two half-circles, tangent) tapered inward: every edge moves in by
|
||||
|
||||
Reference in New Issue
Block a user