mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-12 03:31:57 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30a306b376 | |||
| a78e36bd9e | |||
| 1982f7dfa3 |
@@ -321,6 +321,27 @@ fn shallow_loft_cut_from_the_top_face() {
|
||||
assert!((got - want).abs() < 1e-9 * want, "{got} vs {want}");
|
||||
}
|
||||
|
||||
/// A carved top lofted through six closed spline sections from its slab's top face and joined
|
||||
/// onto the slab. The loft lies on and above that face and shares only it with the slab, so the
|
||||
/// joined top is exactly the slab plus the loft built as its own body.
|
||||
#[test]
|
||||
fn carved_top_lofted_onto_its_slab_joins() {
|
||||
let script = include_str!("../../../examples/regressions/loft_join_carved_top_on_slab.json");
|
||||
let joined = replay(script);
|
||||
assert_eq!(joined.model.state().bodies.len(), 1);
|
||||
let mut apart: Value = serde_json::from_str(script).unwrap();
|
||||
let loft = apart["commands"].as_array_mut().unwrap().last_mut().unwrap();
|
||||
loft["params"]["operation"] = json!("new_body");
|
||||
loft["params"]["body_name"] = json!("Arch");
|
||||
loft["params"].as_object_mut().unwrap().remove("targets");
|
||||
let mut s = Session::default();
|
||||
s.run_script(&apart).unwrap();
|
||||
let (slab, arch) = (volume(&s, "Top"), volume(&s, "Arch"));
|
||||
assert!(slab > 0.0 && arch > 0.0, "{slab} {arch}");
|
||||
let got = volume(&joined, "Top");
|
||||
assert!((got - (slab + arch)).abs() < 1e-7 * got, "{got} vs {slab} + {arch}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oblique_cut_through_a_small_arc_matches_a_split() {
|
||||
let scripts = [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -589,6 +589,7 @@ fn rust(k: &Kernel, i: &Inputs) -> Result<Output, HarnessError> {
|
||||
intersections_with_rails: params.as_deref(),
|
||||
deriv_surfs: ds.as_ref().map(|[a, b]| [a.as_ref(), b.as_ref()]),
|
||||
degree: *degree,
|
||||
section_params: None,
|
||||
};
|
||||
BSplineSurface::create_skinned_surface(k, &secs, opts).map(|s| {
|
||||
o.push(1.0);
|
||||
|
||||
@@ -1467,6 +1467,7 @@ impl EiffelTowerCrossSectionFsg {
|
||||
d1.as_ref().map(SurfOfExtrusion::base),
|
||||
]),
|
||||
degree: 3,
|
||||
section_params: None,
|
||||
};
|
||||
BSplineSurface::create_skinned_surface(k, &cross_sections, opts)
|
||||
}
|
||||
|
||||
@@ -3167,6 +3167,7 @@ impl CircularCrossSectionFsg {
|
||||
intersections_with_rails: Some(¶ms),
|
||||
deriv_surfs: Some([d0.as_ref(), d1.as_ref()]),
|
||||
degree: 3,
|
||||
section_params: None,
|
||||
};
|
||||
let fillet_surface = BSplineSurface::create_skinned_surface(k, §ions, opts)?;
|
||||
|
||||
|
||||
@@ -220,6 +220,10 @@ pub struct SkinnedSurfaceOptions<'a> {
|
||||
pub deriv_surfs: Option<[Option<&'a BSplineSurface>; 2]>,
|
||||
/// Degree in the sweep direction (`lDegree`, upstream default 3).
|
||||
pub degree: usize,
|
||||
/// Not upstream: the skinning parameter of each cross section, increasing from 0 to 1,
|
||||
/// used where there are no rails (with rails, `intersections_with_rails` governs);
|
||||
/// `None` takes the chord-length parameters, as upstream.
|
||||
pub section_params: Option<&'a [f64]>,
|
||||
}
|
||||
|
||||
impl Default for SkinnedSurfaceOptions<'_> {
|
||||
@@ -234,6 +238,7 @@ impl Default for SkinnedSurfaceOptions<'_> {
|
||||
intersections_with_rails: None,
|
||||
deriv_surfs: None,
|
||||
degree: 3,
|
||||
section_params: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1029,6 +1034,7 @@ impl BSplineSurface {
|
||||
intersections_with_rails,
|
||||
deriv_surfs,
|
||||
degree,
|
||||
section_params,
|
||||
} = opts;
|
||||
let num_sections = cross_sections.len();
|
||||
|
||||
@@ -1303,7 +1309,12 @@ impl BSplineSurface {
|
||||
deg: nl_deg,
|
||||
es: approx_tol,
|
||||
dir: nl_dir,
|
||||
ts: int_params,
|
||||
// Not upstream: without rails, the caller's section parameters (if any).
|
||||
ts: if has_rails {
|
||||
int_params
|
||||
} else {
|
||||
int_params.or_else(|| section_params.map(<[f64]>::to_vec))
|
||||
},
|
||||
kns: nl_knots,
|
||||
};
|
||||
match create_skin_srf_boundary_continuity(&mut curves, &mut sk) {
|
||||
|
||||
+165
-13
@@ -26,6 +26,10 @@
|
||||
//! Not upstream (the SolidCraft fork's tangency-labs/smlib 5d3939e, WR-523, #1450):
|
||||
//! [`PrimitiveCreation::create_curve_sweep_with_placements`] reports the rigid motions
|
||||
//! carrying the profile onto the first, middle and last sections.
|
||||
//! Not upstream: [`PrimitiveCreation::create_skin_from_faces`] skins sections on
|
||||
//! parallel planes at their planes' distances (`SectionPlanes`) where the chord-length
|
||||
//! skin's control points leave the slab between the first and last planes, so the skin
|
||||
//! stays between them; every other skin keeps the chord-length parameters.
|
||||
//! **Statuses pass on** (opt-in `Kernel::fixes.sweep_statuses_pass_on`, WR-662;
|
||||
//! SolidCraft change, tangency-labs/smlib c8f6a31): the statuses upstream drops here
|
||||
//! are returned instead (`SER`).
|
||||
@@ -73,6 +77,116 @@ fn face_surface(a: &impl PrimitiveAccess, face: FaceRef) -> Result<&Surface> {
|
||||
.ok_or_else(|| SolidError::primitive_missing(OP, "the face's surface"))
|
||||
}
|
||||
|
||||
/// Not upstream: section faces that are planes, three or more, all parallel to the first
|
||||
/// (within `EFF_ZERO_DEG`) and strictly in order along its normal.
|
||||
///
|
||||
/// A skin through them is meant to stay in the slab between the first and last planes.
|
||||
/// Upstream's chord-length parameters come from the control points' whole motion: where
|
||||
/// sections move far sideways for a small step along the normal (a carved top's first
|
||||
/// steps), the height's interpolant overshoots, the skin dips behind its first section's
|
||||
/// plane and crosses its own cap face, and a union with a body whose face is coplanar
|
||||
/// with that cap fails. [`SectionPlanes::leaves`] finds that on the chord-length skin's
|
||||
/// control points; only then is the skin built again at [`SectionPlanes::params`].
|
||||
struct SectionPlanes {
|
||||
origin: crate::math::Point3,
|
||||
unit: Vec3,
|
||||
/// The last plane's height above the first along `unit` (non-zero).
|
||||
last: f64,
|
||||
/// Each plane's height over `last`: 0 at the first section, 1 at the last.
|
||||
params: Vec<f64>,
|
||||
}
|
||||
|
||||
impl SectionPlanes {
|
||||
/// `None` for sections that are not all on parallel planes in order.
|
||||
fn of(a: &impl PrimitiveAccess, faces: &[FaceRef]) -> Result<Option<Self>> {
|
||||
use crate::math::EFF_ZERO_DEG;
|
||||
use crate::surface::surface::SurfaceKind;
|
||||
if faces.len() < 3 {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut planes = Vec::new();
|
||||
for &face in faces {
|
||||
match face_surface(a, face)?.kind() {
|
||||
SurfaceKind::Plane(p) => {
|
||||
let (point, normal, _) = p.is_planar();
|
||||
planes.push((point, normal));
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
let Some(&(origin, normal)) = planes.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Ok(unit) = normal.unitize() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut heights = Vec::new();
|
||||
for &(point, n) in &planes {
|
||||
if !unit.is_parallel_to(n, EFF_ZERO_DEG) {
|
||||
return Ok(None);
|
||||
}
|
||||
heights.push(unit.dot(point - origin));
|
||||
}
|
||||
let Some(&last) = heights.last() else {
|
||||
return Ok(None);
|
||||
};
|
||||
// Strictly in order along the normal, either way (NaN fails every comparison).
|
||||
let increasing = heights
|
||||
.iter()
|
||||
.zip(heights.iter().skip(1))
|
||||
.all(|(p, q)| q > p);
|
||||
let decreasing = heights
|
||||
.iter()
|
||||
.zip(heights.iter().skip(1))
|
||||
.all(|(p, q)| q < p);
|
||||
if !(increasing || decreasing) || !last.is_finite() || last == 0.0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut params: Vec<f64> = heights.iter().map(|h| h / last).collect();
|
||||
// The ends exactly 0 and 1, as the interpolation's knot vector needs.
|
||||
if let Some(f) = params.first_mut() {
|
||||
*f = 0.0;
|
||||
}
|
||||
if let Some(l) = params.last_mut() {
|
||||
*l = 1.0;
|
||||
}
|
||||
if !params.iter().all(|t| t.is_finite()) {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(Self {
|
||||
origin,
|
||||
unit,
|
||||
last,
|
||||
params,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Whether a control point of `surface` lies farther than `tol` outside the slab
|
||||
/// between the first and last planes. A non-rational skin lies in its control points'
|
||||
/// convex hull, so with none outside it stays in the slab (within `tol`); a rational
|
||||
/// one (positive weights) likewise, read on its Euclidean points.
|
||||
fn leaves(&self, surface: &BSplineSurface, tol: f64) -> Result<bool> {
|
||||
let (low, high) = if self.last > 0.0 {
|
||||
(-tol, self.last + tol)
|
||||
} else {
|
||||
(self.last - tol, tol)
|
||||
};
|
||||
let (_, _, points) = surface.control_points()?;
|
||||
Ok(points.into_iter().any(|p| {
|
||||
let h = self.unit.dot(p.to_euclid() - self.origin);
|
||||
// NaN counts as leaving.
|
||||
!(h >= low && h <= high)
|
||||
}))
|
||||
}
|
||||
|
||||
/// Plane-distance parameters (each section's height is then one linear function of
|
||||
/// its parameter, which the interpolation reproduces exactly: the control rows lie
|
||||
/// on the planes at their Greville abscissae's heights and the skin in the slab).
|
||||
fn params(&self) -> &[f64] {
|
||||
&self.params
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for `CreateBlendPrimitive`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BlendPrimitiveOptions {
|
||||
@@ -292,25 +406,63 @@ impl PrimitiveCreation {
|
||||
layers.resize(skin_curves.len(), 0);
|
||||
untraced = Some(NotTraced::OpenProfiles);
|
||||
}
|
||||
let mut traced: Vec<(FaceId, Vec<usize>)> = Vec::new();
|
||||
let mut made = Vec::new();
|
||||
// Each layer's skin at chord-length parameters, as upstream.
|
||||
let skin = |k: &crate::kernel::Kernel, curves: &[BSplineCurve], params: Option<&[f64]>| {
|
||||
BSplineSurface::create_skinned_surface(
|
||||
k,
|
||||
curves,
|
||||
SkinnedSurfaceOptions {
|
||||
approx_tol,
|
||||
degree,
|
||||
deriv_surfs: Some([None, None]),
|
||||
section_params: params,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
};
|
||||
let mut layer_curves = Vec::new();
|
||||
let mut surfaces = Vec::new();
|
||||
for layer in 0..layer_count {
|
||||
let curves: Vec<_> = skin_curves
|
||||
let curves: Vec<BSplineCurve> = skin_curves
|
||||
.iter()
|
||||
.zip(&layers)
|
||||
.filter(|(_, l)| **l == layer)
|
||||
.map(|(c, _)| c.clone())
|
||||
.collect();
|
||||
let surface = BSplineSurface::create_skinned_surface(
|
||||
a.kernel(brep)?,
|
||||
&curves,
|
||||
SkinnedSurfaceOptions {
|
||||
approx_tol,
|
||||
degree,
|
||||
deriv_surfs: Some([None, None]),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
surfaces.push(skin(a.kernel(brep)?, &curves, None)?);
|
||||
layer_curves.push(curves);
|
||||
}
|
||||
// Not upstream: sections on parallel planes whose chord-length skin leaves the slab
|
||||
// between the first and last planes are skinned at their planes' distances, every
|
||||
// layer alike (`SectionPlanes`).
|
||||
// A failure of the check or of the second build keeps the chord-length skins, as
|
||||
// upstream builds them; only cancellation and unported calls stop the loft.
|
||||
let replanned = (|| -> Result<Option<Vec<BSplineSurface>>> {
|
||||
let Some(planes) = SectionPlanes::of(&*a, faces)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let tol = a.brep(brep)?.tolerance();
|
||||
let mut leaves = false;
|
||||
for s in &surfaces {
|
||||
leaves = leaves || planes.leaves(s, tol)?;
|
||||
}
|
||||
if !leaves {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut again = Vec::new();
|
||||
for curves in &layer_curves {
|
||||
again.push(skin(a.kernel(brep)?, curves, Some(planes.params()))?);
|
||||
}
|
||||
Ok(Some(again))
|
||||
})();
|
||||
match replanned {
|
||||
Ok(Some(again)) => surfaces = again,
|
||||
Err(e) if e.is_unported_or_cancelled() => return Err(e),
|
||||
Ok(None) | Err(_) => {}
|
||||
}
|
||||
let mut traced: Vec<(FaceId, Vec<usize>)> = Vec::new();
|
||||
let mut made = Vec::new();
|
||||
for (layer, surface) in surfaces.into_iter().enumerate() {
|
||||
// (the trace: the surface's knots across the sections, V)
|
||||
let surface_knots = trace
|
||||
.is_some()
|
||||
|
||||
@@ -6181,6 +6181,7 @@ impl DerivSurfDefinition<'_> {
|
||||
intersections_with_rails: Some(params),
|
||||
deriv_surfs: Some([None, None]),
|
||||
degree,
|
||||
section_params: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -445,6 +445,7 @@ pub fn create_skin(k: &Kernel, profiles: &[BSplineCurve]) -> Result<BSplineSurfa
|
||||
intersections_with_rails: None,
|
||||
deriv_surfs: Some([None, None]), // derivative surfaces
|
||||
degree: 3,
|
||||
section_params: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//! A skin through sections on parallel planes stays between its first and last planes.
|
||||
//!
|
||||
//! The skin takes upstream's chord-length parameters across the sections. Only where that
|
||||
//! skin's control points leave the slab between the first and last planes does it take the
|
||||
//! planes' distances from the first instead (`SectionPlanes` in
|
||||
//! `primitive_creation::p3::construct`): every section's height above the first plane is then
|
||||
//! one linear function of the parameter, which the interpolating skin reproduces exactly, so
|
||||
//! every control point of the side faces lies between the two end planes and, by the convex
|
||||
//! hull, so does the skin. Checked on the control points, never on samples. A skin whose
|
||||
//! chord-length control points stay in the slab is built as before, bit for bit.
|
||||
//!
|
||||
//! The carved top's sections take wide steps sideways for small steps up near the first
|
||||
//! section (0.6 high, up to 16 in), larger ones after. Chord-length parameters come from the
|
||||
//! whole motion of the control points, so the first step's parameter is far larger than its
|
||||
//! share of the height; the cubic interpolant of the height overshoots: control points fall
|
||||
//! 0.24 below the first plane, the skin dips behind it and crosses its own start cap, and its
|
||||
//! union with a slab whose top is that cap's plane comes out with a negative volume.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)]
|
||||
|
||||
use solidcore::boolean::api_brep::boolean_union;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
|
||||
use solidcore::math::Point3;
|
||||
use solidcore::nurbs::b_spline_curve::BSplineCurve;
|
||||
use solidcore::nurbs::b_spline_surface::SurfParamType;
|
||||
use solidcore::nurbs::crv_fit::p1::ParamMethod;
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::surface::surface::SurfaceKind;
|
||||
use solidcore::sweep::api_surfaces::create_skin_primitive;
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::Kernel;
|
||||
|
||||
/// (height, half-length, half-width, shift along x) of each section, as a carved top's.
|
||||
const SECTIONS: [(f64, f64, f64, f64); 6] = [
|
||||
(0.0, 200.0, 150.0, 0.0),
|
||||
(0.6, 184.0, 132.0, 6.0),
|
||||
(3.2, 160.0, 105.0, 23.0),
|
||||
(6.4, 120.0, 68.0, 43.0),
|
||||
(8.8, 90.0, 45.0, 58.0),
|
||||
(10.0, 66.0, 30.0, 68.0),
|
||||
];
|
||||
|
||||
/// A closed cubic through 16 points of an ellipse bulging toward -x (the waist of a
|
||||
/// body), at height `z`.
|
||||
fn section(z: f64, a: f64, b: f64, dx: f64) -> Curve {
|
||||
let mut pts: Vec<Point3> = (0..16)
|
||||
.map(|j| {
|
||||
let t = 2.0 * std::f64::consts::PI * f64::from(j) / 16.0;
|
||||
let r = 1.0 + 0.12 * (2.0 * t).cos();
|
||||
Point3::new(dx + a * r * t.cos(), b * r * t.sin(), z)
|
||||
})
|
||||
.collect();
|
||||
pts.extend(pts.first().copied());
|
||||
Curve::from(
|
||||
BSplineCurve::interpolate_points(&pts, None, 3, None, None, true, ParamMethod::ChordLength)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn carved_top(k: &mut Kernel) -> Brep {
|
||||
let cs: Vec<Curve> = SECTIONS
|
||||
.iter()
|
||||
.map(|&(z, a, b, dx)| section(z, a, b, dx))
|
||||
.collect();
|
||||
let refs: Vec<Option<&Curve>> = cs.iter().map(Some).collect();
|
||||
create_skin_primitive(k, &vec![1; cs.len()], &refs, 3, true)
|
||||
.unwrap()
|
||||
.brep
|
||||
}
|
||||
|
||||
fn volume(k: &Kernel, b: &mut Brep) -> f64 {
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes {
|
||||
store: &store,
|
||||
key_of: &|_| ObjectKey(0),
|
||||
};
|
||||
brep_volume(k, b, 1e-9, &attrs).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_skin_through_parallel_sections_stays_between_its_end_planes() {
|
||||
let mut k = Kernel::default();
|
||||
let b = carved_top(&mut k);
|
||||
let top = SECTIONS[5].0;
|
||||
// the heights' rounding: a few ulps of the largest coordinate
|
||||
let slack = 64.0 * f64::EPSILON * 300.0;
|
||||
let mut sides = 0;
|
||||
for &f in b.face_list() {
|
||||
let Some(s) = b.face(f).unwrap().surface.clone() else {
|
||||
continue;
|
||||
};
|
||||
let SurfaceKind::BSpline(bs) = s.kind() else {
|
||||
continue;
|
||||
};
|
||||
sides += 1;
|
||||
// cubic across the six sections, and polynomial: its hull is its control points'
|
||||
assert!(bs.degree(SurfParamType::U).max(bs.degree(SurfParamType::V)) >= 3);
|
||||
assert!(!bs.is_rational(), "{f:?}");
|
||||
let (_, _, cps) = bs.control_points().unwrap();
|
||||
for p in cps {
|
||||
let p = p.to_euclid();
|
||||
assert!(
|
||||
p.z >= -slack && p.z <= top + slack,
|
||||
"{f:?}: a control point at height {} leaves the slab [0, {top}]",
|
||||
p.z
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(sides > 0, "no B-spline side face");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_skin_through_parallel_sections_joins_a_slab_under_its_first_section() {
|
||||
// The slab's top face is the skin's start cap's plane; they share only that face, so
|
||||
// the union's volume is the sum of theirs.
|
||||
let mut k = Kernel::default();
|
||||
let mut skin = carved_top(&mut k);
|
||||
let v_skin = volume(&k, &mut skin);
|
||||
let slab = create_box(&mut k, Point3::new(-260.0, -200.0, -5.0), 520.0, 400.0, 5.0).unwrap();
|
||||
let mut u = boolean_union(&mut k, slab, skin).unwrap();
|
||||
let v = volume(&k, &mut u);
|
||||
let want = 520.0 * 400.0 * 5.0 + v_skin;
|
||||
assert!(
|
||||
(v - want).abs() <= 1e-7 * want,
|
||||
"union volume {v} against slab + skin {want}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A closed cubic through 16 points of the circle of radius `r` about the z axis at height
|
||||
/// `z`.
|
||||
fn circle(z: f64, r: f64) -> Curve {
|
||||
let mut pts: Vec<Point3> = (0..16)
|
||||
.map(|j| {
|
||||
let t = 2.0 * std::f64::consts::PI * f64::from(j) / 16.0;
|
||||
Point3::new(r * t.cos(), r * t.sin(), z)
|
||||
})
|
||||
.collect();
|
||||
pts.extend(pts.first().copied());
|
||||
Curve::from(
|
||||
BSplineCurve::interpolate_points(&pts, None, 3, None, None, true, ParamMethod::ChordLength)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The capped skin through circles given as (height, radius), and its volume.
|
||||
fn round_loft(sections: &[(f64, f64)]) -> f64 {
|
||||
let mut k = Kernel::default();
|
||||
let cs: Vec<Curve> = sections.iter().map(|&(z, r)| circle(z, r)).collect();
|
||||
let refs: Vec<Option<&Curve>> = cs.iter().map(Some).collect();
|
||||
let mut b = create_skin_primitive(&mut k, &vec![1; cs.len()], &refs, 3, true)
|
||||
.unwrap()
|
||||
.brep;
|
||||
volume(&k, &mut b)
|
||||
}
|
||||
|
||||
/// The volumes of the chord-length skins below, measured with the skin as it was before
|
||||
/// plane distances (solidcore at origin/main of 2026-11-11).
|
||||
const WAIST_VOLUME: f64 = 16791.357595192487;
|
||||
const BULGE_VOLUME: f64 = 272283.3927004977;
|
||||
|
||||
/// Lofts whose chord-length skins stay between their end planes keep their shape: a waist
|
||||
/// (r 20, 10, 20 at z 0, 5, 40) that plane distances would pinch through its axis, and four
|
||||
/// unevenly spaced sections that plane distances would grow by 29%. Their volumes are the
|
||||
/// chord-length skins', as built before plane distances existed.
|
||||
#[test]
|
||||
fn lofts_that_stay_between_their_end_planes_keep_their_chord_length_shape() {
|
||||
let waist = round_loft(&[(0.0, 20.0), (5.0, 10.0), (40.0, 20.0)]);
|
||||
assert!(
|
||||
(waist - WAIST_VOLUME).abs() <= 1e-9 * WAIST_VOLUME,
|
||||
"waist {waist}"
|
||||
);
|
||||
let bulge = round_loft(&[(0.0, 30.0), (10.0, 50.0), (60.0, 15.0), (80.0, 25.0)]);
|
||||
assert!(
|
||||
(bulge - BULGE_VOLUME).abs() <= 1e-9 * BULGE_VOLUME,
|
||||
"bulge {bulge}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user