Thanks @fkirill.
This commit is contained in:
1 parent
8be736c33e
commit
2c924fd385
7 files changed
+289
-7
No files matched your search
@@ -475,3 +475,19 @@ fn box_intersect_splits_the_end_off_a_raked_wedge_after_its_end_rounds() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #2005: changing a circular sweep from a separate body to a half-turn cut must create the
|
||||
/// internal channel, rather than failing the Boolean on coincident end caps.
|
||||
#[test]
|
||||
fn closed_sweep_redefines_to_a_half_turn_torus_cut() {
|
||||
let s = replay(include_str!("../../../examples/regressions/closed_sweep_torus_cut.json"));
|
||||
assert_eq!(s.model.state().bodies.len(), 1);
|
||||
let body = &s.model.state().bodies[0].body;
|
||||
assert!(body.validity().is_empty(), "{:?}", body.validity());
|
||||
let mesh = body.tessellate(0.02).unwrap();
|
||||
let expected = (std::f64::consts::PI * 64.0 - 64.0) * std::f64::consts::TAU * 30.0;
|
||||
assert!((mesh.measure().volume - expected).abs() < expected * 0.015);
|
||||
for (point, material) in [(solidcraft_geom::Vec3::new(30.0, 0.0, 0.0), false), (solidcraft_geom::Vec3::new(30.0, 0.0, 7.0), true)] {
|
||||
assert_eq!(mesh.contains(point), material, "material at {point:?}");
|
||||
}
|
||||
}
|
||||
@@ -1562,6 +1562,15 @@ pub(crate) fn cap_and_close(sides: Vec<Side>, w0: &[mt::Wire], w1: &[mt::Wire],
|
||||
Err(KernelError::Failed(format!("closing the shell: {last}")))
|
||||
}
|
||||
|
||||
/// A sweep whose walls already close, including separate shells around profile holes.
|
||||
pub(crate) fn close_sweep(sides: Vec<Side>) -> Result<Body> {
|
||||
let sources: std::collections::HashMap<_, _> = sides.iter().map(|(f, p)| (f.id(), p.clone())).collect();
|
||||
let shell: mt::Shell = sides.into_iter().map(|(f, _)| f).collect();
|
||||
let solid = Solid::try_new(shell.connected_components()).map_err(|e| KernelError::Failed(format!("closing the sweep: {e}")))?;
|
||||
let history = solid.face_iter().map(|f| sources.get(&f.id()).cloned().flatten()).collect();
|
||||
record::attach(Body::new(solid)?, &[], history, Default::default())
|
||||
}
|
||||
|
||||
/// Ruled loft through two or more planar sections (outer loops only). The sections and the
|
||||
/// same sections in reverse order give the same body.
|
||||
pub fn loft(sections: &[(Plane, Loop2)]) -> Result<Body> {
|
||||
@@ -1881,6 +1890,25 @@ pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body>
|
||||
if corner {
|
||||
return crate::sweep_path::sweep_path(plane, region, &path_polyline(path)?);
|
||||
}
|
||||
let points = path_polyline(path)?;
|
||||
if crate::sweep_path::closed_path(&points) {
|
||||
// Sketch circles have two half arcs. Sweep the whole turn in one operation so truck
|
||||
// shares the closing edges itself. Other closed paths use stations with shared ends.
|
||||
if let Some((center, axis, angle)) = full_circle(path) {
|
||||
return guard("closed sweep", || {
|
||||
let profile = region_wires(plane, &oriented(region)?)?;
|
||||
let mut sides = Vec::new();
|
||||
for p in &profile {
|
||||
let shell: mt::Shell = builder::rsweep(&p.wire, p3(center), v3(axis), mt::Rad(angle));
|
||||
let numbers = seeds(std::slice::from_ref(p));
|
||||
let prov = record::columns(shell.face_iter(), &numbers, None, record::Column::ToEnd, &[]);
|
||||
sides.extend(shell.face_iter().enumerate().map(|(i, f)| (f.clone(), prov.as_ref().and_then(|p| p.get(i)).cloned().flatten())));
|
||||
}
|
||||
close_sweep(sides)
|
||||
});
|
||||
}
|
||||
return crate::sweep_path::sweep_path(plane, region, &points);
|
||||
}
|
||||
// The outer loop and each hole, swept along the path; the ends are capped with holes.
|
||||
let profile = region_wires(plane, &oriented(region)?)?;
|
||||
let starts = wires_of(&profile);
|
||||
@@ -1927,6 +1955,23 @@ pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body>
|
||||
})
|
||||
}
|
||||
|
||||
/// A connected, single turn about one centre and axis, possibly split into sketch arcs.
|
||||
fn full_circle(path: &[PathSeg]) -> Option<(Vec3, Vec3, f64)> {
|
||||
let PathSeg::Arc { a: first, center, axis, .. } = *path.first()? else { return None };
|
||||
let axis = axis.normalized()?;
|
||||
let mut end = first;
|
||||
let mut total = 0.0;
|
||||
for seg in path {
|
||||
let PathSeg::Arc { a, center: c, axis: k, angle } = *seg else { return None };
|
||||
if a.dist(end) > 1e-7 || c.dist(center) > 1e-7 || k.normalized()?.dist(axis) > 1e-9 || !angle.is_finite() {
|
||||
return None;
|
||||
}
|
||||
total += angle;
|
||||
end = rotate_about(a, c, axis, angle);
|
||||
}
|
||||
((total.abs() - std::f64::consts::TAU).abs() < 1e-9).then_some((center, axis, total))
|
||||
}
|
||||
|
||||
/// Cox–de Boor basis values N_{j,3}(u) for all j.
|
||||
fn cubic_basis(knots: &[f64], n_ctrl: usize, u: f64) -> Vec<f64> {
|
||||
let p = 3;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Issue #2005: a closed sweep must join its end back to its start, without coincident caps.
|
||||
//! The capped tool used to stall or fail when cutting the torus it lay inside.
|
||||
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
|
||||
|
||||
use crate::{Body, BoolOp, PathSeg, SweepControl, boolean, measure, sweep_with};
|
||||
|
||||
fn square(half: f64) -> Region2 {
|
||||
Region2 {
|
||||
outer: Loop2::polygon(&[Vec2::new(-half, -half), Vec2::new(half, -half), Vec2::new(half, half), Vec2::new(-half, half)]),
|
||||
holes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_plane() -> Plane {
|
||||
Plane { origin: Vec3::new(30.0, 0.0, 0.0), x: Vec3::X, y: Vec3::Z }
|
||||
}
|
||||
|
||||
fn circle_path() -> [PathSeg; 2] {
|
||||
// A sketch circle reaches the kernel as two half arcs.
|
||||
[
|
||||
PathSeg::Arc { a: Vec3::new(30.0, 0.0, 0.0), center: Vec3::ZERO, axis: Vec3::Z, angle: PI },
|
||||
PathSeg::Arc { a: Vec3::new(-30.0, 0.0, 0.0), center: Vec3::ZERO, axis: Vec3::Z, angle: PI },
|
||||
]
|
||||
}
|
||||
|
||||
fn tube(body: &Body, volume: f64, genus: usize) {
|
||||
let m = measure(body).unwrap();
|
||||
assert!((m.volume - volume).abs() < volume * 0.01, "{m:?}, expected {volume}");
|
||||
// A closed orientable tube has Euler characteristic 2 - 2g. Coincident caps gave 2.
|
||||
assert_eq!(m.vertices + m.faces + 2 * genus, m.edges + 2, "{m:?}");
|
||||
assert!(body.solid.boundaries().iter().all(|s| s.extract_boundaries().is_empty()));
|
||||
let h = body.history().expect("sweep provenance");
|
||||
assert!(h.faces.iter().all(|f| f.origins.iter().all(|o| matches!(o, crate::history::FaceOrigin::Profile { .. }))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_circle_sweeps_have_no_caps() {
|
||||
for twist in [0.0, FRAC_PI_2, PI, TAU, -PI] {
|
||||
let ctl = SweepControl { twist, ..SweepControl::default() };
|
||||
let b = sweep_with(&profile_plane(), &square(4.0), &circle_path(), &ctl).unwrap();
|
||||
tube(&b, 64.0 * TAU * 30.0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_full_arc_closes_like_a_sketch_circle() {
|
||||
for angle in [TAU, -TAU] {
|
||||
let path = [PathSeg::Arc { a: Vec3::new(30.0, 0.0, 0.0), center: Vec3::ZERO, axis: Vec3::Z, angle }];
|
||||
let b = sweep_with(&profile_plane(), &square(4.0), &path, &SweepControl::default()).unwrap();
|
||||
tube(&b, 64.0 * TAU * 30.0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_polyline_sweep_shares_its_starting_section() {
|
||||
let path = crate::path_polyline(&circle_path()).unwrap();
|
||||
let b = crate::sweep_path(&profile_plane(), &square(4.0), &path).unwrap();
|
||||
tube(&b, 64.0 * TAU * 30.0, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_sweep_refuses_an_end_section_that_does_not_match() {
|
||||
for ctl in [SweepControl { twist: PI / 4.0, ..SweepControl::default() }, SweepControl { scale: 2.0, ..SweepControl::default() }] {
|
||||
let err = sweep_with(&profile_plane(), &square(4.0), &circle_path(), &ctl).unwrap_err();
|
||||
assert!(err.to_string().contains("closed path"), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_holed_sweep_joins_every_loop() {
|
||||
let region = Region2 { holes: vec![square(2.0).outer], ..square(4.0) };
|
||||
let ctl = SweepControl { twist: PI, ..SweepControl::default() };
|
||||
let b = sweep_with(&profile_plane(), ®ion, &circle_path(), &ctl).unwrap();
|
||||
// Two disjoint toroidal shells, one bounding the cavity.
|
||||
let m = measure(&b).unwrap();
|
||||
assert!((m.volume - 48.0 * TAU * 30.0).abs() < m.volume * 0.02, "{m:?}");
|
||||
assert_eq!(m.vertices + m.faces, m.edges, "{m:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn half_turn_sweep_cuts_a_torus() {
|
||||
let base = crate::torus(Vec3::ZERO, 30.0, 8.0).unwrap();
|
||||
let tool = sweep_with(&profile_plane(), &square(4.0), &circle_path(), &SweepControl { twist: PI, ..SweepControl::default() }).unwrap();
|
||||
let result = boolean(&base, &tool, BoolOp::Cut).unwrap().expect("cut leaves the torus wall");
|
||||
let expected = (PI * 64.0 - 64.0) * TAU * 30.0;
|
||||
let m = measure(&result).unwrap();
|
||||
assert!((m.volume - expected).abs() < expected * 0.015, "{m:?}, expected {expected}");
|
||||
}
|
||||
@@ -59,6 +59,8 @@ mod issue_1080_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_1510_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_2005_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_324_tests;
|
||||
#[cfg(test)]
|
||||
mod issue_616_tests;
|
||||
|
||||
@@ -225,6 +225,19 @@ fn trivial(
|
||||
mod shortcut_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn seam_roundoff_is_not_a_thin_meeting_but_real_thin_solids_are() {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
|
||||
|
||||
// This circle's seams lie on the X axis: its cylinder's vertex box is flat in Y.
|
||||
let region = Region2 { outer: Loop2::circle(Vec2::ZERO, 8.0), holes: vec![] };
|
||||
let periodic = crate::extrude(&Plane::XY, &[region], 0.0, 20.0).unwrap().pop().unwrap();
|
||||
let block = crate::box_solid(Vec3::new(-40.0, -40.0, -10.0), Vec3::new(40.0, 40.0, 10.0)).unwrap();
|
||||
assert!(meeting_size(&periodic, &block).is_infinite());
|
||||
let thin = crate::box_solid(Vec3::ZERO, Vec3::new(80.0, 80.0, 1e-5)).unwrap();
|
||||
assert!((meeting_size(&thin, &thin) - 1e-5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curved_near_contact_never_uses_a_shell_only_boolean_candidate() {
|
||||
let direction = Vec3::new(1.0, 2.0, 3.0).normalized().unwrap();
|
||||
@@ -513,7 +526,11 @@ fn meeting_size(a: &Body, b: &Body) -> f64 {
|
||||
};
|
||||
let (ba, bb) = (bbox(a), bbox(b));
|
||||
let s = solidcraft_geom::Aabb3 { min: ba.min.max(bb.min), max: ba.max.min(bb.max) }.size();
|
||||
if s.x > 0.0 && s.y > 0.0 && s.z > 0.0 { s.x.min(s.y).min(s.z) } else { f64::INFINITY }
|
||||
// A closed sweep's section seams need not span its geometry. Roundoff in a flat
|
||||
// vertex box is not a thin meeting region: refining for it
|
||||
// makes millions of triangles and skips the bounded containment shortcut (#2005).
|
||||
let roundoff = a.size().max(b.size()) * 1e-12;
|
||||
if s.x > roundoff && s.y > roundoff && s.z > roundoff { s.x.min(s.y).min(s.z) } else { f64::INFINITY }
|
||||
}
|
||||
|
||||
/// Smallest side of the box where the bounds of two meshes overlap (infinite when they don't).
|
||||
|
||||
@@ -10,7 +10,7 @@ use solidcraft_geom::{Plane, Region2, Seg2, Vec2, Vec3};
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::body::{Body, p3};
|
||||
use crate::build::{cap_and_close, interpolate_cubic, uniform_wire};
|
||||
use crate::build::{cap_and_close, close_sweep, interpolate_cubic, smooth_loft_faces, uniform_wire};
|
||||
use crate::numbered::{NumberedLoop, NumberedRegion};
|
||||
use crate::record::{self, Side};
|
||||
use crate::{KernelError, Result, guard};
|
||||
@@ -171,6 +171,43 @@ fn clean(path: &[Vec3]) -> Result<Vec<Vec3>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn closed_path(pts: &[Vec3]) -> bool {
|
||||
pts.len() > 3 && pts.first().zip(pts.last()).is_some_and(|(a, b)| a.dist(*b) <= 1e-7)
|
||||
}
|
||||
|
||||
/// The cyclic correspondence of a loop's last section to its first. Matching whole curves
|
||||
/// (rather than vertices alone) prevents sewing an incompatible twist or scale shut.
|
||||
fn closing_offset(plane: &Plane, profile: &NumberedLoop, st: &[Station]) -> Result<usize> {
|
||||
let fail =
|
||||
|| KernelError::Invalid("a sweep along a closed path must return to its starting profile; adjust the twist, end scale or rails".into());
|
||||
let (Some(first), Some(last)) = (st.first(), st.last()) else { return Err(fail()) };
|
||||
let n = profile.lp.segs.len();
|
||||
if n == 0 || n > 4096 {
|
||||
return Err(fail());
|
||||
}
|
||||
let at = |s: &Station, q: Vec2| s.at + s.apply(plane.to_world(q) - first.at);
|
||||
let tol = (profile.lp.signed_area().abs().sqrt() * 1e-7).clamp(1e-8, 1e-5);
|
||||
// Filter offsets by the first corner before comparing all the curve samples.
|
||||
let start = profile.lp.segs.first().map(Seg2::start).ok_or_else(fail)?;
|
||||
(0..n)
|
||||
.filter(|k| profile.lp.segs.get(*k).is_some_and(|s| at(last, start).dist(at(first, s.start())) <= tol))
|
||||
.find(|k| {
|
||||
profile.lp.segs.iter().enumerate().all(|(j, a)| {
|
||||
profile.lp.segs.get((j + k) % n).is_some_and(|b| {
|
||||
(0..SEG_SAMPLES).all(|i| {
|
||||
let t = i as f64 / (SEG_SAMPLES - 1) as f64;
|
||||
at(last, a.point_at(t)).dist(at(first, b.point_at(t))) <= tol
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
.ok_or_else(fail)
|
||||
}
|
||||
|
||||
fn rotated_wire(wire: &mt::Wire, offset: usize) -> mt::Wire {
|
||||
wire.edge_iter().skip(offset).chain(wire.edge_iter().take(offset)).cloned().collect()
|
||||
}
|
||||
|
||||
fn turn(a: Vec3, b: Vec3) -> f64 {
|
||||
match (a.normalized(), b.normalized()) {
|
||||
(Some(a), Some(b)) => a.dot(b).clamp(-1.0, 1.0).acos(),
|
||||
@@ -506,7 +543,12 @@ pub fn sweep_path_with(plane: &Plane, region: &Region2, path: &[Vec3], ctl: &Swe
|
||||
let v = swept_volume(plane, region.area().abs(), &st, &tangents);
|
||||
(st, v)
|
||||
};
|
||||
if let Ok(b) = smooth_walls(plane, &profile.outer, &st, expected) {
|
||||
if closed_path(&pts) {
|
||||
let offset = closing_offset(plane, &profile.outer, &st)?;
|
||||
if let Ok(b) = smooth_closed_walls(plane, &profile.outer, &st, offset, expected) {
|
||||
return Ok(b);
|
||||
}
|
||||
} else if let Ok(b) = smooth_walls(plane, &profile.outer, &st, expected) {
|
||||
return Ok(b);
|
||||
}
|
||||
}
|
||||
@@ -515,10 +557,11 @@ pub fn sweep_path_with(plane: &Plane, region: &Region2, path: &[Vec3], ctl: &Swe
|
||||
|
||||
fn central_tangents(p: &[Vec3]) -> Result<Vec<Vec3>> {
|
||||
let n = p.len();
|
||||
let closed = closed_path(p);
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let a = p.get(i.saturating_sub(1)).copied().unwrap_or_default();
|
||||
let b = p.get((i + 1).min(n - 1)).copied().unwrap_or_default();
|
||||
let a = p.get(if closed && (i == 0 || i + 1 == n) { n - 2 } else { i.saturating_sub(1) }).copied().unwrap_or_default();
|
||||
let b = p.get(if closed && (i == 0 || i + 1 == n) { 1 } else { (i + 1).min(n - 1) }).copied().unwrap_or_default();
|
||||
(b - a).normalized().ok_or_else(|| KernelError::Invalid("the sweep path has no direction".into()))
|
||||
})
|
||||
.collect()
|
||||
@@ -535,12 +578,25 @@ fn ruled_stations(plane: &Plane, pts: &[Vec3], ctl: &SweepControl) -> Result<(Ve
|
||||
let pts = densify(pts, ctl);
|
||||
let pts: Vec<Vec3> = if pts.len() > MAX_RULED_STATIONS { resample(&pts, MAX_RULED_STATIONS) } else { pts };
|
||||
let n = pts.len();
|
||||
let closed = closed_path(&pts);
|
||||
let seg_dir = |i: usize| -> Option<Vec3> { (*pts.get(i + 1)? - *pts.get(i)?).normalized() };
|
||||
let mut tangents = Vec::with_capacity(n);
|
||||
let mut mitre = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let d_in = if i > 0 { seg_dir(i - 1) } else { None };
|
||||
let d_out = if i + 1 < n { seg_dir(i) } else { None };
|
||||
let d_in = if i > 0 {
|
||||
seg_dir(i - 1)
|
||||
} else if closed {
|
||||
seg_dir(n - 2)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let d_out = if i + 1 < n {
|
||||
seg_dir(i)
|
||||
} else if closed {
|
||||
seg_dir(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (t, m) = match (d_in, d_out) {
|
||||
(Some(a), Some(b)) => {
|
||||
let t = (a + b).normalized().ok_or_else(|| KernelError::Invalid("the sweep path turns back on itself".into()))?;
|
||||
@@ -706,6 +762,15 @@ fn ruled(plane: &Plane, profile: &NumberedRegion, pts: &[Vec3], ctl: &SweepContr
|
||||
let pl = station_plane(plane, s0, s);
|
||||
wires.push(loops.iter().map(|l| uniform_wire(&pl, &l.lp)).collect::<Result<Vec<_>>>()?);
|
||||
}
|
||||
let closed = closed_path(&pts);
|
||||
if closed {
|
||||
let offsets = loops.iter().map(|l| closing_offset(plane, l, &st)).collect::<Result<Vec<_>>>()?;
|
||||
let first = wires.first().ok_or_else(|| KernelError::Invalid("sweep path".into()))?;
|
||||
let end: Vec<mt::Wire> = first.iter().zip(offsets).map(|(w, k)| rotated_wire(w, k)).collect();
|
||||
if let Some(last) = wires.last_mut() {
|
||||
*last = end;
|
||||
}
|
||||
}
|
||||
guard("path sweep", || {
|
||||
// Every station's wires have the loops' segments, edge for edge.
|
||||
let mut faces: Vec<Side> = Vec::new();
|
||||
@@ -716,11 +781,38 @@ fn ruled(plane: &Plane, profile: &NumberedRegion, pts: &[Vec3], ctl: &SweepContr
|
||||
faces.extend(record::ruled(&sides, a, b, &l.from, &l.from));
|
||||
}
|
||||
}
|
||||
if closed {
|
||||
return close_sweep(faces);
|
||||
}
|
||||
let (Some(first), Some(last)) = (wires.first(), wires.last()) else { return Err(KernelError::Failed("path sweep".into())) };
|
||||
cap_and_close(faces, first, last, false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Two smooth runs share their middle section and the closing section. Splitting the run
|
||||
/// avoids edges from a vertex back to itself when the profile returns without a permutation.
|
||||
fn smooth_closed_walls(plane: &Plane, profile: &NumberedLoop, st: &[Station], offset: usize, expected: f64) -> Result<Body> {
|
||||
let fail = || KernelError::Failed("closed path sweep: the walls do not close a solid".into());
|
||||
if profile.lp.segs.len() > 64 || st.len() < 7 {
|
||||
return Err(fail());
|
||||
}
|
||||
guard("closed path sweep", || {
|
||||
let first = st.first().ok_or_else(fail)?;
|
||||
let mut wires = st.iter().map(|s| uniform_wire(&station_plane(plane, first.at, s), &profile.lp)).collect::<Result<Vec<_>>>()?;
|
||||
let end = rotated_wire(wires.first().ok_or_else(fail)?, offset);
|
||||
if let Some(last) = wires.last_mut() {
|
||||
*last = end;
|
||||
}
|
||||
let middle = (wires.len() - 1) / 2;
|
||||
let numbers = vec![profile.from.clone(); wires.len()];
|
||||
let mut sides = smooth_loft_faces(wires.get(..=middle).ok_or_else(fail)?, numbers.get(..=middle).ok_or_else(fail)?, true)?;
|
||||
sides.extend(smooth_loft_faces(wires.get(middle..).ok_or_else(fail)?, numbers.get(middle..).ok_or_else(fail)?, true)?);
|
||||
let b = close_sweep(sides)?;
|
||||
let tol = (profile.lp.signed_area().abs().sqrt() * 0.05).max(1e-3);
|
||||
if b.tessellate(tol).is_ok_and(|m| (m.measure().volume - expected).abs() <= 0.15 * expected.abs()) { Ok(b) } else { Err(fail()) }
|
||||
})
|
||||
}
|
||||
|
||||
/// One B-spline wall per profile segment through the stations, capped; checked by volume.
|
||||
fn smooth_walls(plane: &Plane, profile: &NumberedLoop, st: &[Station], expected: f64) -> Result<Body> {
|
||||
let lp = &profile.lp;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"description": "Issue #2005: redefine a 90 degree new-body sweep around a circle to a 180 degree cut inside a torus. A closed tool must join back to its starting section without coincident caps.",
|
||||
"commands": [
|
||||
{"command": "sketch.create", "params": {"plane": "XY", "name": "Path"}},
|
||||
{"command": "sketch.circle.center", "params": {"center": [0, 0], "radius": 30}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "sketch.create", "params": {"plane": "XZ", "name": "Tube"}},
|
||||
{"command": "sketch.circle.center", "params": {"center": [30, 0], "radius": 8}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.revolve", "params": {"axis": "Z", "body_name": "Torus"}},
|
||||
{"command": "sketch.create", "params": {"plane": "XZ", "name": "SweepProfile"}},
|
||||
{"command": "sketch.line", "params": {"points": [[26, -4], [34, -4], [34, 4], [26, 4]], "closed": true}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.sweep", "params": {"sketch": 4, "path_sketch": 1, "path": ["c1"], "operation": "new_body", "twist": "90 deg"}},
|
||||
{"command": "timeline.redefine", "params": {"feature": 5, "command": "solid.sweep", "params": {"sketch": 4, "path_sketch": 1, "path": ["c1"], "profiles": [0], "operation": "cut", "twist": "180 deg"}}}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user