mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
kernel: truck's extruded circles go into SMLib as its cylinders
Truck extrudes a circle into rational splines, so every truck cylinder reached SMLib as splines, and a box cut by one came back with edges 0.78 mm off their faces (SMLib's own check refused it). A rational spline that is a circular arc swept straight along its axis (one direction linear, the second row the first moved by one vector square to the arc's circle, weights alike) now goes as SMLib's cylinder with the face's range on it. The revolution mapping becomes one module for both (import/analytic.rs): each candidate face's analytic surface is checked against the record's at 25 points of its domain, each point placed on the analytic surface by its own (u, v), and turned over where the normals disagree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
@@ -407,8 +407,10 @@ fn smlib_spline_edges_on_part_of_their_curve_decode() {
|
||||
use solidcraft_smlib::BooleanOp;
|
||||
let block = crate::box_solid(Vec3::new(7.5, 15., 0.), Vec3::new(25., 32.5, 27.5)).unwrap();
|
||||
let post = crate::cylinder(Vec3::new(12.5, 20., 10.), Vec3::Z, 5., 30.).unwrap();
|
||||
let (a, c) = (crate::native::smlib_body(&block).unwrap(), crate::native::smlib_body(&post).unwrap());
|
||||
let common = a.boolean(&c, BooleanOp::Intersect).unwrap().unwrap();
|
||||
// Both as truck holds them, the post a rational spline and the block's sides bilinear
|
||||
// patches (the import otherwise sends SMLib's cylinder and planes, whose cut leaves circles).
|
||||
let keeping = |body: &Body| solidcraft_smlib::Brep::from_record_keeping_splines(&crate::brep_record(body).unwrap(), true).unwrap();
|
||||
let common = keeping(&block).boolean(&keeping(&post), BooleanOp::Intersect).unwrap().unwrap();
|
||||
let record = common.to_native().unwrap();
|
||||
assert!(
|
||||
record.edges.iter().any(|e| matches!(e.curve.natural(), b::Natural::Range(lo, hi) if [lo, hi] != e.interval)
|
||||
|
||||
@@ -61,3 +61,22 @@ fn smlib_results_go_back_into_smlib() {
|
||||
assert!((got - exact).abs() <= 1e-6 * exact, "{name}: SMLib volume {got}, exact {exact}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Truck's cylinders are circles extruded as rational splines: they go into SMLib as its
|
||||
/// cylinder, so a boolean with one intersects a plane with a cylinder, not with a spline.
|
||||
#[test]
|
||||
fn extruded_circles_go_as_cylinders() {
|
||||
use solidcraft_smlib::flat;
|
||||
let body = cylinder(v(0., 50., -60.), v(0., 0., 1.), 40.0, 120.0).unwrap();
|
||||
let record = super::brep_record(&body).unwrap();
|
||||
assert!(record.faces.iter().any(|f| matches!(f.surface, solidcraft_geom::brep::Surface::RationalSpline { .. })));
|
||||
let arrays = solidcraft_smlib::import::arrays(&record, true).unwrap();
|
||||
let kinds = &arrays.ints[&flat::FACE_SURFACE_TYPE];
|
||||
assert!(kinds.iter().all(|k| *k == i64::from(flat::SURFACE_PLANE) || *k == i64::from(flat::SURFACE_CYLINDER)), "{kinds:?}");
|
||||
assert_eq!(kinds.iter().filter(|k| **k == i64::from(flat::SURFACE_CYLINDER)).count(), record.faces.len() - 2);
|
||||
// A box cut by it is a valid SMLib solid (as a spline it had edges 0.78 mm off its faces).
|
||||
let block = box_solid(v(-14., 0., -14.), v(14., 20., 14.)).unwrap();
|
||||
let r = super::smlib_body(&block).unwrap().boolean(&super::smlib_body(&body).unwrap(), solidcraft_smlib::BooleanOp::Subtract).unwrap().unwrap();
|
||||
let check = r.check(solidcraft_smlib::CheckLevel::All).unwrap();
|
||||
assert!(check.is_valid(), "{check}");
|
||||
}
|
||||
|
||||
@@ -84,8 +84,22 @@ fn z_axis(f: &b::Frame) -> [f64; 3] {
|
||||
/// rules (SMLib then skips its healer); otherwise SMLib heals it on import.
|
||||
pub fn arrays(brep: &b::Brep, trusted: bool) -> R<Arrays> {
|
||||
brep.validate().map_err(|e| Unsupported::Invalid(e.0))?;
|
||||
let brep = revolved::analytic(brep)?;
|
||||
let brep = brep.as_ref();
|
||||
let brep = analytic::analytic(brep)?;
|
||||
laid_out(brep.as_ref(), trusted, true)
|
||||
}
|
||||
|
||||
/// [`arrays`] with every surface as the record holds it: a round face truck holds as a rational
|
||||
/// spline (an extruded circle) stays one, and a plane off unit orthogonal axes goes as its
|
||||
/// bilinear patch, to compare what SMLib makes of splines with what it makes of its own analytic
|
||||
/// surfaces. A revolution is still refused (SMLib has no such surface).
|
||||
pub fn arrays_keeping_splines(brep: &b::Brep, trusted: bool) -> R<Arrays> {
|
||||
brep.validate().map_err(|e| Unsupported::Invalid(e.0))?;
|
||||
laid_out(brep, trusted, false)
|
||||
}
|
||||
|
||||
/// The flat arrays; `analytic`: a plane on any axes goes as SMLib's plane (else, off unit
|
||||
/// orthogonal axes, as the bilinear patch it is).
|
||||
fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R<Arrays> {
|
||||
let mut a = Arrays::default();
|
||||
|
||||
// Regions and shells. Each region gets its own copy of each shell it lists.
|
||||
@@ -156,7 +170,7 @@ pub fn arrays(brep: &b::Brep, trusted: bool) -> R<Arrays> {
|
||||
for f in &brep.faces {
|
||||
a.i(flat::FACE_LOOP_COUNT).push(int(f.loops.len())?);
|
||||
a.i(flat::FACE_TRIM_TYPE).push(0);
|
||||
let spline = surface(&mut a, &f.surface, f.domain)?;
|
||||
let spline = surface(&mut a, &f.surface, f.domain, analytic)?;
|
||||
for l in &f.loops {
|
||||
match l {
|
||||
b::Loop::Vertex { vertex } => {
|
||||
@@ -234,7 +248,7 @@ fn nurbs_face(a: &mut Arrays, u_knots: &[f64], v_knots: &[f64], nu: usize, nv: u
|
||||
}
|
||||
|
||||
/// The face's surface and range; true for a B-spline surface (which carries trims).
|
||||
fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2]) -> R<bool> {
|
||||
fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_planes: bool) -> R<bool> {
|
||||
let [[u0, u1], [v0, v1]] = domain;
|
||||
let analytic = |a: &mut Arrays, kind: u8, ids: [i32; 3], f: &b::Frame| {
|
||||
a.i(flat::FACE_SURFACE_TYPE).push(i64::from(kind));
|
||||
@@ -244,8 +258,10 @@ fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2]) -> R<bool> {
|
||||
a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]);
|
||||
};
|
||||
match s {
|
||||
b::Surface::Plane { origin, u_axis, v_axis } => plane(a, *origin, *u_axis, *v_axis, domain)?,
|
||||
b::Surface::PlaneThrough { origin, u_point, v_point } => plane(a, *origin, sub(*u_point, *origin), sub(*v_point, *origin), domain)?,
|
||||
b::Surface::Plane { origin, u_axis, v_axis } => plane(a, *origin, *u_axis, *v_axis, domain, analytic_planes)?,
|
||||
b::Surface::PlaneThrough { origin, u_point, v_point } => {
|
||||
plane(a, *origin, sub(*u_point, *origin), sub(*v_point, *origin), domain, analytic_planes)?
|
||||
}
|
||||
b::Surface::Cylinder { frame, radius } => {
|
||||
analytic(a, flat::SURFACE_CYLINDER, [flat::FACE_CYLINDER_ORIGIN, flat::FACE_CYLINDER_AXIS, flat::FACE_CYLINDER_REF_DIRECTION], frame);
|
||||
a.r(flat::FACE_CYLINDER_RADIUS).push(*radius);
|
||||
@@ -393,8 +409,18 @@ fn net_rows(a: &mut Arrays, cps: impl Iterator<Item = ([f64; 3], f64)>) {
|
||||
/// U × V as the record's. The range is the record's domain mapped onto those axes (its bounding
|
||||
/// box: a face's range need not be tight, and SMLib rebuilds an analytic face's trims from its
|
||||
/// edges). Truck's planes run through three points, their axes the face's own sides.
|
||||
fn plane(a: &mut Arrays, origin: [f64; 3], u: [f64; 3], v: [f64; 3], domain: [[f64; 2]; 2]) -> R<()> {
|
||||
/// Off unit orthogonal axes and not `analytic`, it goes as the exact bilinear patch over the
|
||||
/// domain instead.
|
||||
fn plane(a: &mut Arrays, origin: [f64; 3], u: [f64; 3], v: [f64; 3], domain: [[f64; 2]; 2], analytic: bool) -> R<()> {
|
||||
let [[u0, u1], [v0, v1]] = domain;
|
||||
let unit = |x: [f64; 3]| (dot(x, x).sqrt() - 1.0).abs() <= 1e-12;
|
||||
if !analytic && !(unit(u) && unit(v) && dot(u, v).abs() <= 1e-12) {
|
||||
nurbs_face(a, &[u0, u0, u1, u1], &[v0, v0, v1, v1], 2, 2)?;
|
||||
let at = |s: f64, t: f64| add(origin, add(scale(u, s), scale(v, t)));
|
||||
net(a, [at(u0, v0), at(u0, v1), at(u1, v0), at(u1, v1)].into_iter().map(|p| (p, 1.0)));
|
||||
a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]);
|
||||
return Ok(());
|
||||
}
|
||||
let n = cross(u, v);
|
||||
let (lu, ln) = (dot(u, u).sqrt(), dot(n, n).sqrt());
|
||||
if !(lu > 0.0 && ln > 0.0 && lu.is_finite() && ln.is_finite()) {
|
||||
@@ -491,7 +517,7 @@ fn curve(a: &mut Arrays, c: &b::Curve) -> R<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod revolved;
|
||||
mod analytic;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -502,4 +528,10 @@ impl crate::Brep {
|
||||
let a = arrays(brep, trusted).map_err(crate::Error::Record)?;
|
||||
crate::flat::Flat::from_arrays(&a)?.import()
|
||||
}
|
||||
|
||||
/// [`crate::Brep::from_record`] through [`arrays_keeping_splines`].
|
||||
pub fn from_record_keeping_splines(brep: &b::Brep, trusted: bool) -> crate::Result<crate::Brep> {
|
||||
let a = arrays_keeping_splines(brep, trusted).map_err(crate::Error::Record)?;
|
||||
crate::flat::Flat::from_arrays(&a)?.import()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//! Revolutions as SMLib's analytic surfaces. A `placed_revolution` record holds a surface the
|
||||
//! way truck does (a profile turned about an axis, then placed); SMLib has no such surface, but
|
||||
//! every one truck makes for a cylinder, cone, sphere or torus is one of those exactly: a line
|
||||
//! along the axis, a line in a plane through it, a circle centred on it, a circle in a plane
|
||||
//! through it. Each such face becomes that analytic surface, its range mapped from the
|
||||
//! revolution's parameters, and checked point by point over the face's range; where the
|
||||
//! analytic surface's natural normal is the other way round, the face is turned over (its loops
|
||||
//! reversed, its uses in the shells flipped), which leaves the solid as it was.
|
||||
//! Revolutions and extruded circles as SMLib's analytic surfaces.
|
||||
//!
|
||||
//! A `placed_revolution` record holds a surface the way truck does (a profile turned about an
|
||||
//! axis, then placed); SMLib has no such surface, but every one truck makes for a cylinder, cone,
|
||||
//! sphere or torus is one of those exactly: a line along the axis, a line in a plane through it, a
|
||||
//! circle centred on it, a circle in a plane through it. A rational spline that is a circular arc
|
||||
//! swept straight along its axis (truck's extruded circles) is a cylinder. Each such face becomes
|
||||
//! that analytic surface with the face's range on it, checked point by point over the face's
|
||||
//! domain; where the analytic surface's natural normal is the other way round, the face is turned
|
||||
//! over (its loops reversed, its uses in the shells flipped), which leaves the solid as it was.
|
||||
//!
|
||||
//! A revolution of any other profile, or one placed by a matrix that is not a rigid motion, is
|
||||
//! refused.
|
||||
//! refused; any other spline stays a spline.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::f64::consts::{FRAC_PI_2, TAU};
|
||||
@@ -21,41 +23,97 @@ use super::{R, Unsupported};
|
||||
|
||||
const NOT_ANALYTIC: Unsupported = Unsupported::NotYet("revolved surfaces other than cylinders, cones, spheres and tori");
|
||||
|
||||
/// `brep` with every revolution replaced by its analytic surface (borrowed when it has none).
|
||||
/// `brep` with every revolution and extruded circle replaced by its analytic surface (borrowed
|
||||
/// when it has none).
|
||||
pub(super) fn analytic(brep: &b::Brep) -> R<Cow<'_, b::Brep>> {
|
||||
if !brep.faces.iter().any(|f| matches!(f.surface, b::Surface::PlacedRevolution { .. })) {
|
||||
return Ok(Cow::Borrowed(brep));
|
||||
}
|
||||
let mut out = brep.clone();
|
||||
let size = extent(brep);
|
||||
let tol = brep.tolerance.max(1e-9 * size);
|
||||
let mut out: Option<b::Brep> = None;
|
||||
let mut turned = Vec::new();
|
||||
for (i, f) in out.faces.iter_mut().enumerate() {
|
||||
let b::Surface::PlacedRevolution { profile, origin, axis, matrix, forward } = &f.surface else { continue };
|
||||
let rev = Revolution::new(profile, *origin, *axis, matrix, *forward)?;
|
||||
let (mut surface, mut domain, v) = rev.analytic(f.domain)?;
|
||||
for (i, f) in brep.faces.iter().enumerate() {
|
||||
let found = match &f.surface {
|
||||
b::Surface::PlacedRevolution { profile, origin, axis, matrix, forward } => {
|
||||
Some(Revolution::new(profile, *origin, *axis, matrix, *forward)?.analytic(f.domain)?)
|
||||
}
|
||||
b::Surface::RationalSpline { u_knots, v_knots, homogeneous } => {
|
||||
extruded_circle(u_knots, v_knots, homogeneous, f.domain, 1e-9 * size.max(1.0))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let Some((mut surface, mut range)) = found else { continue };
|
||||
// The angle about the axis starts at 0: the frame turns to the face's first angle (SMLib
|
||||
// refuses an analytic range past 360°). A torus's tube angle can't turn so: it starts
|
||||
// within the first turn, where SMLib's degree domains are at home.
|
||||
let start = domain[0][0];
|
||||
turn_frame(&mut surface, start);
|
||||
domain[0] = [0.0, domain[0][1] - start];
|
||||
turn_frame(&mut surface, range[0][0]);
|
||||
range[0] = [0.0, range[0][1] - range[0][0]];
|
||||
if matches!(surface, b::Surface::Torus { .. }) {
|
||||
domain[1] = first_turn(domain[1]);
|
||||
range[1] = first_turn(range[1]);
|
||||
}
|
||||
let map = Map { rev: &rev, start, v: &v };
|
||||
let flip = agree((&f.surface, f.domain), (&surface, domain), map, brep.tolerance.max(1e-9 * size))?;
|
||||
f.surface = surface;
|
||||
f.domain = domain;
|
||||
let flip = agree((&f.surface, f.domain), (&surface, range), tol)?;
|
||||
let face = out.get_or_insert_with(|| brep.clone()).faces.get_mut(i).ok_or(Unsupported::Invalid("face"))?;
|
||||
face.surface = surface;
|
||||
face.domain = range;
|
||||
if flip {
|
||||
turned.push(i);
|
||||
}
|
||||
}
|
||||
let Some(mut out) = out else { return Ok(Cow::Borrowed(brep)) };
|
||||
for i in turned {
|
||||
turn_over(&mut out, i)?;
|
||||
}
|
||||
Ok(Cow::Owned(out))
|
||||
}
|
||||
|
||||
/// A rational spline that is a circular arc swept straight along the arc's axis, as a cylinder
|
||||
/// with the face's range on it: one direction linear (two rows of control points), the second
|
||||
/// row the first moved by one vector at right angles to the circle the first row runs on, the
|
||||
/// weights alike. `None` where it is not.
|
||||
fn extruded_circle(u_knots: &[f64], v_knots: &[f64], net: &[Vec<[f64; 4]>], domain: [[f64; 2]; 2], tol: f64) -> Option<(b::Surface, [[f64; 2]; 2])> {
|
||||
let linear = |k: &[f64]| k.len() == 4 && k[0] == k[1] && k[2] == k[3] && k[1] < k[2];
|
||||
let (rows, arc_knots, arc_range, line_knots, line_range): ([Vec<[f64; 4]>; 2], &[f64], [f64; 2], &[f64], [f64; 2]) =
|
||||
if net.iter().all(|r| r.len() == 2) && linear(v_knots) && !linear(u_knots) {
|
||||
([net.iter().map(|r| r[0]).collect(), net.iter().map(|r| r[1]).collect()], u_knots, domain[0], v_knots, domain[1])
|
||||
} else if net.len() == 2 && linear(u_knots) && !linear(v_knots) {
|
||||
([net.first()?.clone(), net.get(1)?.clone()], v_knots, domain[1], u_knots, domain[0])
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let cart = |h: &[f64; 4]| Vec3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]);
|
||||
let (first, second) = (&rows[0], &rows[1]);
|
||||
let d = cart(second.first()?) - cart(first.first()?);
|
||||
let alike = first.iter().zip(second).all(|(p, q)| (p[3] - q[3]).abs() <= 1e-12 * p[3].abs() && (cart(q) - cart(p) - d).len() <= tol);
|
||||
if !alike || first.len() != second.len() || !(d.len() > tol) {
|
||||
return None;
|
||||
}
|
||||
let arc = CurveEval::new(&b::Curve::RationalSpline { knots: arc_knots.to_vec(), homogeneous: first.clone() }).ok()?;
|
||||
let (center, radius, normal) = circle(&arc, arc_range)?;
|
||||
let z = d.normalized()?;
|
||||
if normal.cross(z).len() > 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let x = (arc.point(arc_range[0])? - center).normalized()?;
|
||||
let y = z.cross(x);
|
||||
// The arc's angles from x, unwrapped along its range.
|
||||
let mut angle = 0.0;
|
||||
let mut prev = 0.0;
|
||||
for i in 1..=32 {
|
||||
let q = arc.point(arc_range[0] + (arc_range[1] - arc_range[0]) * f64::from(i) / 32.0)? - center;
|
||||
let mut a = q.dot(y).atan2(q.dot(x));
|
||||
while a - prev > std::f64::consts::PI {
|
||||
a -= TAU;
|
||||
}
|
||||
while a - prev < -std::f64::consts::PI {
|
||||
a += TAU;
|
||||
}
|
||||
prev = a;
|
||||
angle = a;
|
||||
}
|
||||
let height = |s: f64| (s - line_knots[1]) / (line_knots[2] - line_knots[1]) * d.len();
|
||||
let sorted = |a: f64, b: f64| if a <= b { [a, b] } else { [b, a] };
|
||||
let frame = b::Frame { origin: arr(center), x_axis: arr(x), y_axis: arr(y) };
|
||||
Some((b::Surface::Cylinder { frame, radius }, [sorted(0.0, angle), sorted(height(line_range[0]), height(line_range[1]))]))
|
||||
}
|
||||
|
||||
/// An analytic surface's frame turned by `angle` about its axis: u then counts from there.
|
||||
fn turn_frame(s: &mut b::Surface, angle: f64) {
|
||||
if let b::Surface::Cylinder { frame, .. } | b::Surface::Cone { frame, .. } | b::Surface::Sphere { frame, .. } | b::Surface::Torus { frame, .. } =
|
||||
@@ -171,7 +229,7 @@ impl Revolution {
|
||||
|
||||
/// The analytic surface and the face's range on it: u the angle about z from the profile's
|
||||
/// half-plane, v as the surface's kind has it.
|
||||
fn analytic(&self, domain: [[f64; 2]; 2]) -> R<(b::Surface, [[f64; 2]; 2], V)> {
|
||||
fn analytic(&self, domain: [[f64; 2]; 2]) -> R<(b::Surface, [[f64; 2]; 2])> {
|
||||
let (t, angle) = self.ranges(domain);
|
||||
let at = |s: f64| self.profile.point(s).ok_or(Unsupported::Invalid("revolution profile"));
|
||||
let samples: Vec<Vec3> = (0..=16).map(|i| at(t[0] + (t[1] - t[0]) * f64::from(i) / 16.0)).collect::<R<_>>()?;
|
||||
@@ -205,7 +263,7 @@ impl Revolution {
|
||||
if !(r0 > 0.0) {
|
||||
return Err(NOT_ANALYTIC);
|
||||
}
|
||||
return Ok((b::Surface::Cylinder { frame: frame(foot), radius: r0 }, [angle, v], V::Height { foot, z: self.z }));
|
||||
return Ok((b::Surface::Cylinder { frame: frame(foot), radius: r0 }, [angle, v]));
|
||||
}
|
||||
// A cone on one side of its apex only.
|
||||
let r = |h: f64| r0 + h * dr / dh;
|
||||
@@ -213,7 +271,7 @@ impl Revolution {
|
||||
return Err(NOT_ANALYTIC);
|
||||
}
|
||||
let cone = b::Surface::Cone { frame: frame(foot), radius_at_origin: r0, half_angle: (dr / dh).atan() };
|
||||
return Ok((cone, [angle, v], V::Height { foot, z: self.z }));
|
||||
return Ok((cone, [angle, v]));
|
||||
}
|
||||
// A circular profile.
|
||||
let (center, radius, normal) = circle(&self.profile, t).ok_or(NOT_ANALYTIC)?;
|
||||
@@ -248,73 +306,59 @@ impl Revolution {
|
||||
return Err(NOT_ANALYTIC);
|
||||
}
|
||||
let v = [v[0].max(-FRAC_PI_2), v[1].min(FRAC_PI_2)];
|
||||
return Ok((b::Surface::Sphere { frame: frame(foot), radius }, [angle, v], V::Angle { center, x, z: self.z }));
|
||||
return Ok((b::Surface::Sphere { frame: frame(foot), radius }, [angle, v]));
|
||||
}
|
||||
let major = off.dot(x);
|
||||
if !(major > radius) {
|
||||
return Err(NOT_ANALYTIC);
|
||||
}
|
||||
let torus = b::Surface::Torus { frame: frame(foot), major_radius: major, minor_radius: radius };
|
||||
Ok((torus, [angle, sorted(first, last)], V::Angle { center, x, z: self.z }))
|
||||
}
|
||||
|
||||
/// The analytic (u, v) of the record's (u, v): the angle is the revolution's own, v that of
|
||||
/// the profile's point (at angle 0), unwrapped to within half a turn of `near`.
|
||||
fn map(&self, v: &V, uv: [f64; 2], start: f64, near: f64) -> Option<[f64; 2]> {
|
||||
let (t, angle) = if self.forward { (uv[0], uv[1]) } else { (uv[1], uv[0]) };
|
||||
let p = self.profile.point(t)?;
|
||||
Some([angle - start, v.of(p, near)])
|
||||
Ok((torus, [angle, sorted(first, last)]))
|
||||
}
|
||||
}
|
||||
|
||||
/// From a revolution's (u, v) to its analytic surface's: the revolution, the angle the
|
||||
/// surface's u starts at, and how its v is found.
|
||||
struct Map<'a> {
|
||||
rev: &'a Revolution,
|
||||
start: f64,
|
||||
v: &'a V,
|
||||
}
|
||||
|
||||
/// How the analytic v of a profile point is found.
|
||||
enum V {
|
||||
/// Height along `z` above `foot` (cylinder, cone).
|
||||
Height { foot: Vec3, z: Vec3 },
|
||||
/// The angle about `center` from `x` towards `z` (sphere latitude, torus meridian).
|
||||
Angle { center: Vec3, x: Vec3, z: Vec3 },
|
||||
}
|
||||
|
||||
impl V {
|
||||
fn of(&self, p: Vec3, near: f64) -> f64 {
|
||||
match self {
|
||||
V::Height { foot, z } => (p - *foot).dot(*z),
|
||||
V::Angle { center, x, z } => {
|
||||
let q = p - *center;
|
||||
let mut a = q.dot(*z).atan2(q.dot(*x));
|
||||
while a - near > std::f64::consts::PI {
|
||||
a -= TAU;
|
||||
}
|
||||
while a - near < -std::f64::consts::PI {
|
||||
a += TAU;
|
||||
}
|
||||
a
|
||||
}
|
||||
/// The (u, v) of point `p` on an analytic surface, angles unwrapped to within half a turn of
|
||||
/// `near`.
|
||||
fn analytic_uv(s: &b::Surface, p: Vec3, near: [f64; 2]) -> Option<[f64; 2]> {
|
||||
let unwrap = |mut a: f64, n: f64| {
|
||||
while a - n > std::f64::consts::PI {
|
||||
a -= TAU;
|
||||
}
|
||||
}
|
||||
while a - n < -std::f64::consts::PI {
|
||||
a += TAU;
|
||||
}
|
||||
a
|
||||
};
|
||||
let (frame, round) = match s {
|
||||
b::Surface::Cylinder { frame, .. } | b::Surface::Cone { frame, .. } | b::Surface::Sphere { frame, .. } => (frame, 0.0),
|
||||
b::Surface::Torus { frame, major_radius, .. } => (frame, *major_radius),
|
||||
_ => return None,
|
||||
};
|
||||
let (x, y) = (v3(frame.x_axis), v3(frame.y_axis));
|
||||
let z = x.cross(y);
|
||||
let q = p - v3(frame.origin);
|
||||
let (qx, qy, qz) = (q.dot(x), q.dot(y), q.dot(z));
|
||||
let u = unwrap(qy.atan2(qx), near[0]);
|
||||
let r = qx.hypot(qy);
|
||||
Some(match s {
|
||||
b::Surface::Sphere { .. } => [u, qz.atan2(r)],
|
||||
b::Surface::Torus { .. } => [u, unwrap(qz.atan2(r - round), near[1])],
|
||||
_ => [u, qz],
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks the analytic surface against the revolution over the face's range, 5 × 5 points
|
||||
/// within `tol`, and says whether their natural normals are opposite (the face must be turned
|
||||
/// over), read at the range's middle.
|
||||
fn agree(record: (&b::Surface, [[f64; 2]; 2]), analytic: (&b::Surface, [[f64; 2]; 2]), map: Map, tol: f64) -> R<bool> {
|
||||
let ((record, domain), (analytic, range), Map { rev, start, v }) = (record, analytic, map);
|
||||
let bad = Unsupported::Invalid("a revolution its analytic surface does not match");
|
||||
/// Checks the analytic surface against the record's over the face's domain, 5 × 5 points within
|
||||
/// `tol`, and says whether their natural normals are opposite (the face must be turned over),
|
||||
/// read at the domain's middle.
|
||||
fn agree((record, domain): (&b::Surface, [[f64; 2]; 2]), (analytic, range): (&b::Surface, [[f64; 2]; 2]), tol: f64) -> R<bool> {
|
||||
let bad = Unsupported::Invalid("a surface its analytic form does not match");
|
||||
let s = SurfaceEval::new(record).map_err(|_| bad.clone())?;
|
||||
let a = SurfaceEval::new(analytic).map_err(|_| bad.clone())?;
|
||||
let [[u0, u1], [v0, v1]] = domain;
|
||||
let near = 0.5 * (range[1][0] + range[1][1]);
|
||||
let near = [0.5 * (range[0][0] + range[0][1]), 0.5 * (range[1][0] + range[1][1])];
|
||||
let normals = |u: f64, w: f64| -> R<f64> {
|
||||
let (p, du, dv) = s.derivs(u, w).ok_or(bad.clone())?;
|
||||
let uv = rev.map(v, [u, w], start, near).ok_or(bad.clone())?;
|
||||
let uv = analytic_uv(analytic, p, near).ok_or(bad.clone())?;
|
||||
let (q, au, av) = a.derivs(uv[0], uv[1]).ok_or(bad.clone())?;
|
||||
if !((p - q).len() <= tol) {
|
||||
return Err(bad.clone());
|
||||
@@ -195,7 +195,7 @@ fn revolutions_become_the_same_surface_as_a_rational_spline() {
|
||||
let (angles, prof) = ([0.4, 0.4 + 1.9 * std::f64::consts::PI], [0.0, 1.0]);
|
||||
let domain = if forward { [prof, angles] } else { [angles, prof] };
|
||||
let mut a = Arrays::default();
|
||||
surface(&mut a, &rev, domain).unwrap();
|
||||
surface(&mut a, &rev, domain, true).unwrap();
|
||||
let (nurbs, range) = first_nurbs(&a);
|
||||
let (re, ne) = (SurfaceEval::new(&rev).unwrap(), SurfaceEval::new(&nurbs).unwrap());
|
||||
let det = {
|
||||
|
||||
Reference in New Issue
Block a user