Compare commits

...

1 Commits

Author SHA1 Message Date
dudedagger42 4fe44c0ad5 fix(torus): a torus read back from its NURB takes its generator's V interval (#3204)
Under WR-493, rebuild_step_from_nurb_parameters snaps the generator's angle
interval to 0 or 360 within SM_EFF_ZERO_DEG for the torus's V interval, then
builds the generator circle on the NURB's iso curve, which measures its own
interval unsnapped. Where the two agree within SM_EFF_ZERO_DEG at both ends,
the torus now takes the circle's, so SmSurfOfRevolution test #2 and SmTorus
test #2 (1e-12) see the same interval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:28:30 -05:00
2 changed files with 91 additions and 0 deletions
+12
View File
@@ -1436,6 +1436,18 @@ impl Torus {
return Err(SolidError::sm_err(OP_REBUILD));
}
// not upstream (solidcore#3204, solidcraft#12757): the circle on the NURB's iso
// curve measures its own interval, unsnapped; where that is the snapped interval
// within SM_EFF_ZERO_DEG at both ends, the torus takes it, so its V interval is its
// generator's (`SmSurfOfRevolution` test #2 compares them within 1e-12)
let mut anal_uv_domain = anal_uv_domain;
let (gen_ivl, v_ivl) = (circle.step_interval(), anal_uv_domain.v_interval());
if (gen_ivl.min() - v_ivl.min()).abs() < EFF_ZERO_DEG
&& (gen_ivl.max() - v_ivl.max()).abs() < EFF_ZERO_DEG
{
anal_uv_domain.set_v_interval(&gen_ivl);
}
// set m_pGenCurve
self.base.gen_curve = Some(Curve::from(circle));
@@ -0,0 +1,79 @@
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
//! A torus read back from its NURB keeps its V interval its generator's (solidcore#3204,
//! solidcraft#12757).
//!
//! With `found_torus_sphere_keep_tested_nurb` (WR-493), `Torus::is_nurb_surface_torus`
//! rebuilds the analytic data from the NURB it tested: it snaps the generator's angle
//! interval to 0 or 360 within `SM_EFF_ZERO_DEG` for the torus's V interval, then builds the
//! generator circle on the NURB's iso curve, which measures its own interval, unsnapped. The
//! two differed by up to `SM_EFF_ZERO_DEG`, and the torus failed `SmSurfOfRevolution` test #2
//! and `SmTorus` test #2 (tolerance 1e-12).
use solidcore::geom::assert_array::{AssertArray, ObjectKey};
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::extent2d::Extent2d;
use solidcore::geom::vector2d::Point2;
use solidcore::kernel::{KernelOptions, UpstreamFixes};
use solidcore::math::{Point3, Vec3};
use solidcore::surface::surface::{Surface, SurfaceKind};
use solidcore::surface::torus::{Torus, TorusOptions};
use solidcore::Kernel;
/// The failed checks of the torus read back from the NURB of `at`'s torus over `v`.
fn failed_checks(k: &Kernel, at: &Axis2Placement, v: (f64, f64)) -> Vec<String> {
let t = Torus::create_canonical(k, at, 49.0, 0.2, TorusOptions::default(), None).unwrap();
let mut s = Surface::from(t);
let d = Extent2d::from_points(Point2::new(0.0, v.0), Point2::new(180.0, v.1));
s.adjust_step_uv_domain(k, &d).unwrap();
let SurfaceKind::Torus(t) = s.kind() else {
panic!("a torus")
};
let back = Torus::is_nurb_surface_torus(k, t.base().base(), 1.0)
.unwrap()
.expect("read back as a torus");
let mut list = AssertArray::new();
Surface::from(back)
.assert_valid(k, Some(&mut list), 2, ObjectKey(1))
.unwrap();
list.reports()
.iter()
.filter(|r| !r.ok)
.map(|r| format!("type {} test {} {}", r.reporting_type, r.test_index, r.name))
.collect()
}
#[test]
fn a_torus_read_back_from_its_nurb_passes_its_check() {
let mut k = Kernel::new(&KernelOptions::default());
k.fixes = UpstreamFixes::all();
// A quarter of the tube, axis +Y, as a round on a part's edge: its generator's
// measured interval ends a hair past 0.
let at = Axis2Placement::new(Point3::new(51.0, 44.0, 14.75), -Vec3::X, Vec3::Z);
assert_eq!(failed_checks(&k, &at, (-90.0, 0.0)), Vec::<String>::new());
// Frames a little off the axes, as an imported part has them, over a quarter, a quarter
// past a half turn, and a half.
for i in 0..20 {
let a = f64::from(i) * 0.731;
let z = Vec3::new(a.sin() * 1e-3, 1.0, a.cos() * 1e-4)
.unitize()
.unwrap();
let x = Vec3::new(-1.0, 0.0, 1.7e-4)
.cross(z)
.cross(z)
.unitize()
.unwrap();
let x = if x.dot(-Vec3::X) < 0.0 { -x } else { x };
let at = Axis2Placement::new(Point3::new(51.0, 44.0, 14.75), x, z.cross(x));
for v in [(-90.0, 0.0), (180.0, 270.0), (-90.0, 90.0)] {
assert_eq!(
failed_checks(&k, &at, v),
Vec::<String>::new(),
"frame {i}, V {v:?}"
);
}
}
}