mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 03:51:08 +00:00
kernel: SMLib's round faces decode as truck's own revolutions are laid out
An SMLib cylinder, cone, sphere or torus decoded as a truck revolution with its parameters swapped (angle, profile), which truck's own revolutions never have. The tessellator then put a cone's apex at the wrong angle: a drill point through SMLib and back meshed 41 % too large and oracle cases 18, 55 and 56 measured wrong areas and volumes. The profile now runs against the record's v, unswapped, which keeps the record's natural normal and truck's (profile, angle). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
@@ -70,9 +70,10 @@ fn analytic_records_decode_as_their_mathematical_loci_and_normals() {
|
||||
(b::Surface::Torus { frame, major_radius: 4., minor_radius: 1. }, [0., TAU], 0., [6., 2., 3.], [1., 0., 0.]),
|
||||
];
|
||||
for (record, height, parameter, expected, normal) in cases {
|
||||
// Truck's revolution as truck makes its own: (u, v) = (the profile's parameter, the angle).
|
||||
let surface = load_surface(record, [[0., TAU], height]).unwrap();
|
||||
assert!(surface.subs(0., parameter).distance(p(expected)) < 1e-10);
|
||||
assert!(surface.normal(0., parameter).normalize().dot(v(normal).normalize()) > 1. - 1e-10);
|
||||
assert!(surface.subs(parameter, 0.).distance(p(expected)) < 1e-10);
|
||||
assert!(surface.normal(parameter, 0.).normalize().dot(v(normal).normalize()) > 1. - 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@ pub(super) fn revolved(profile: mt::Curve, origin: mt::Point3, axis: mt::Vector3
|
||||
|
||||
/// An unbounded analytic surface as a truck revolution, its profile over `height` (the face's
|
||||
/// v range) for a cylinder or cone, and the whole meridian for a sphere or torus. Truck's
|
||||
/// revolution has its own (u, v); the record's loops, not its parameters, bound the face.
|
||||
/// revolution has its own (u, v); the record's loops, not its parameters, bound the face. The
|
||||
/// profile runs against the record's v so the revolution's (profile, angle) has the record's
|
||||
/// natural normal unswapped, as truck's own revolutions are: the tessellator meets a cone's
|
||||
/// apex or a sphere's pole at the profile's end, where it places it right (swapped, a drilled
|
||||
/// hole's tip came out 41 % too large).
|
||||
pub(super) fn load_analytic(surface: b::Surface, height: [f64; 2]) -> Result<mt::Surface> {
|
||||
use std::f64::consts::{FRAC_PI_2, TAU};
|
||||
let axis = |f: &b::Frame| v(f.x_axis).cross(v(f.y_axis));
|
||||
@@ -38,22 +42,22 @@ pub(super) fn load_analytic(surface: b::Surface, height: [f64; 2]) -> Result<mt:
|
||||
b::Surface::Cylinder { frame, radius } => {
|
||||
let z = axis(&frame);
|
||||
let at = |h| p(frame.origin) + v(frame.x_axis) * radius + z * h;
|
||||
revolved(mt::Curve::Line(mt::Line(at(height[0]), at(height[1]))), p(frame.origin), z, true)
|
||||
revolved(mt::Curve::Line(mt::Line(at(height[1]), at(height[0]))), p(frame.origin), z, false)
|
||||
}
|
||||
b::Surface::Cone { frame, radius_at_origin, half_angle } => {
|
||||
let z = axis(&frame);
|
||||
let at = |h| p(frame.origin) + v(frame.x_axis) * (radius_at_origin + h * half_angle.tan()) + z * h;
|
||||
revolved(mt::Curve::Line(mt::Line(at(height[0]), at(height[1]))), p(frame.origin), z, true)
|
||||
revolved(mt::Curve::Line(mt::Line(at(height[1]), at(height[0]))), p(frame.origin), z, false)
|
||||
}
|
||||
b::Surface::Sphere { frame, radius } => {
|
||||
let z = axis(&frame);
|
||||
let meridian = b::Frame { origin: frame.origin, x_axis: frame.x_axis, y_axis: vector(z) };
|
||||
revolved(arc(&meridian, [radius; 2], [-FRAC_PI_2, FRAC_PI_2])?, p(frame.origin), z, true)
|
||||
revolved(arc(&meridian, [radius; 2], [-FRAC_PI_2, FRAC_PI_2])?.inverse(), p(frame.origin), z, false)
|
||||
}
|
||||
b::Surface::Torus { frame, major_radius, minor_radius } => {
|
||||
let z = axis(&frame);
|
||||
let profile = b::Frame { origin: point(p(frame.origin) + v(frame.x_axis) * major_radius), x_axis: frame.x_axis, y_axis: vector(z) };
|
||||
revolved(arc(&profile, [minor_radius; 2], [0., TAU])?, p(frame.origin), z, true)
|
||||
revolved(arc(&profile, [minor_radius; 2], [0., TAU])?.inverse(), p(frame.origin), z, false)
|
||||
}
|
||||
_ => return Err(invalid("expected analytic surface")),
|
||||
})
|
||||
|
||||
@@ -110,3 +110,18 @@ fn a_revolved_annulus_goes_with_its_flat_faces() {
|
||||
let got = s.volume(1e-7).unwrap();
|
||||
assert!((got - exact).abs() <= 1e-6 * exact, "SMLib volume {got}, exact {exact}");
|
||||
}
|
||||
/// A drill point (a cone ending in its apex) through SMLib and back measures as it went in.
|
||||
#[test]
|
||||
fn a_cone_apex_comes_back_measuring_as_it_went() {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
|
||||
let r = 3.4;
|
||||
let h = r / (59f64.to_radians()).tan();
|
||||
let profile =
|
||||
Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 3.0 - h), Vec2::new(r, 3.0), Vec2::new(r, 16.0), Vec2::new(0.0, 16.0)]), holes: vec![] };
|
||||
let plane = Plane { origin: v(20.0, 15.0, 0.0), x: v(1., 0., 0.), y: v(0., 0., 1.) };
|
||||
let tool = crate::revolve(&plane, &[profile], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU).unwrap().pop().unwrap();
|
||||
let back = super::body_from_smlib(&super::smlib_body(&tool).unwrap()).unwrap();
|
||||
let (m0, m1) = (crate::measure(&tool).unwrap(), crate::measure(&back).unwrap());
|
||||
assert!((m1.area - m0.area).abs() < 1e-5 * m0.area, "area {} -> {}", m0.area, m1.area);
|
||||
assert!((m1.volume - m0.volume).abs() < 1e-5 * m0.volume, "volume {} -> {}", m0.volume, m1.volume);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user