mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #1448 from brett19/port/is-nurb-surface-cylinder-torus
port: SmCylinder::IsNurbSurfaceCylinder and SmTorus::IsNurbSurfaceTorus, with fork fixes as opt-in flags (fixes #1433)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Runner for unit `surface.cone_p2`: `SmCone::CreateOffsetSurface`,
|
||||
//! `SmCone::IsNurbSurfaceCone`, `SmCone::RebuildSTEPFromNURBParameters` and the
|
||||
//! file-static `sm_GetEllipseTrimIntervals`.
|
||||
//! `SmCone::IsNurbSurfaceCone`, `SmCylinder::IsNurbSurfaceCylinder`,
|
||||
//! `SmCone::RebuildSTEPFromNURBParameters` and the file-static
|
||||
//! `sm_GetEllipseTrimIntervals`.
|
||||
//!
|
||||
//! C++: `solidcore_ffi::units::surface_cone_p2` (upstream `source/SMLib/src/SmCone.cpp`).
|
||||
//! Rust: `solidcore::surface::cone::p2`.
|
||||
@@ -181,6 +182,12 @@ enum OpIn {
|
||||
Rebuild {
|
||||
edit: EditIn,
|
||||
},
|
||||
/// `SmCylinder::IsNurbSurfaceCylinder` (fixtures only: the generator stream is
|
||||
/// unchanged).
|
||||
RecognizeCylinder {
|
||||
edit: EditIn,
|
||||
scale: f64,
|
||||
},
|
||||
Trim {
|
||||
origin: [f64; 3],
|
||||
x: [f64; 3],
|
||||
@@ -358,6 +365,10 @@ fn cpp(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
OpIn::Rebuild { edit } => ffi::Op::Rebuild {
|
||||
edit: ffi_edit(edit),
|
||||
},
|
||||
OpIn::RecognizeCylinder { edit, scale } => ffi::Op::RecognizeCylinder {
|
||||
edit: ffi_edit(edit),
|
||||
scale: *scale,
|
||||
},
|
||||
OpIn::Trim {
|
||||
origin,
|
||||
x,
|
||||
@@ -781,6 +792,22 @@ fn rust_op(k: &Kernel, s: &mut Cone, op: &OpIn, o: &mut Vec<f64>) -> solidcore::
|
||||
Err(e) => o.extend([status(&e), 0.0]),
|
||||
}
|
||||
}
|
||||
OpIn::RecognizeCylinder { edit, scale } => {
|
||||
let swap = s.rev().swap_uv();
|
||||
let mut test = s.rev().base().clone();
|
||||
edit_nurb(test.get_or_create_gw_nurb()?, &ffi_edit(edit), swap)?;
|
||||
match Cone::is_nurb_surface_cylinder(k, &test, *scale) {
|
||||
Ok(found) => {
|
||||
o.push(flag(found.is_some()));
|
||||
o.push(flag(found.is_some()));
|
||||
if let Some(found) = found {
|
||||
put_state(k, o, &found)?;
|
||||
}
|
||||
}
|
||||
Err(e) if e.is_unported() => return Err(e),
|
||||
Err(e) => o.extend([status(&e), 0.0]),
|
||||
}
|
||||
}
|
||||
OpIn::Rebuild { edit } => {
|
||||
let swap = s.rev().swap_uv();
|
||||
edit_nurb(s.base_mut().get_or_create_gw_nurb()?, &ffi_edit(edit), swap)?;
|
||||
@@ -1285,6 +1312,7 @@ mod tests {
|
||||
OpIn::Offset { .. } => "offset",
|
||||
OpIn::Recognize { .. } => "recognize",
|
||||
OpIn::Rebuild { .. } => "rebuild",
|
||||
OpIn::RecognizeCylinder { .. } => "recognize_cylinder",
|
||||
OpIn::Trim { .. } => "trim",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//! `operator==`, `Copy`, `CreateOffsetSurface`, the STEP/NURB conversions, STEP
|
||||
//! evaluation, iso curves, `RebuildSTEPFromNURBParameters`, `Transform`, and the
|
||||
//! inherited `SmSurfOfRevolution::TrimWithDomain`, `IntersectWithPlane` and
|
||||
//! `GlobalSurfaceIntersect` with a plane).
|
||||
//! `GlobalSurfaceIntersect` with a plane; `IsNurbSurfaceTorus` on its NURB, fixtures
|
||||
//! only).
|
||||
//!
|
||||
//! C++: `solidcore_ffi::units::surface_torus` (upstream `source/SMLib/src/SmTorus.cpp`).
|
||||
//! Rust: `solidcore::surface::torus`.
|
||||
@@ -28,7 +29,7 @@ use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::vector2d::{Point2, Vec2};
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_surface::{IsoCurveOptions, SurfParamType};
|
||||
use solidcore::nurbs::b_spline_surface::{BSplineSurface, IsoCurveOptions, SurfParamType};
|
||||
use solidcore::nurbs::crv_basic::Curve as NurbCurve;
|
||||
use solidcore::surface::surface::p2::{
|
||||
B_SPLINE_SURFACE_TYPE, SURF_OF_REVOLUTION_TYPE, TORUS_TYPE, TYPE,
|
||||
@@ -167,6 +168,27 @@ enum OpIn {
|
||||
TrimDomain {
|
||||
dom: [f64; 4],
|
||||
},
|
||||
/// `IsNurbSurfaceTorus` on the torus's NURB, scaled per axis and with control point
|
||||
/// changes (fixtures only: the generator stream is unchanged).
|
||||
IsNurb {
|
||||
scale: f64,
|
||||
#[serde(default = "unit_scale")]
|
||||
net_scale: [f64; 3],
|
||||
#[serde(default)]
|
||||
perturb: Vec<PerturbIn>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A control point change `(i, j) += d`.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
struct PerturbIn {
|
||||
i: usize,
|
||||
j: usize,
|
||||
d: [f64; 3],
|
||||
}
|
||||
|
||||
const fn unit_scale() -> [f64; 3] {
|
||||
[1.0, 1.0, 1.0]
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -281,6 +303,22 @@ fn cpp(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
args: ffi_args(args),
|
||||
},
|
||||
OpIn::TrimDomain { dom } => ffi::Op::TrimDomain { dom: *dom },
|
||||
OpIn::IsNurb {
|
||||
scale,
|
||||
net_scale,
|
||||
perturb,
|
||||
} => ffi::Op::IsNurb {
|
||||
scale: *scale,
|
||||
net_scale: *net_scale,
|
||||
perturb: perturb
|
||||
.iter()
|
||||
.map(|p| ffi::Perturb {
|
||||
i: p.i,
|
||||
j: p.j,
|
||||
d: p.d,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
let flat = ffi::torus_op(&ffi_torus(&i.torus), &op).map_err(|e| harness(e.to_string()))?;
|
||||
to_output(i, &flat)
|
||||
@@ -712,10 +750,67 @@ fn rust_op(
|
||||
}
|
||||
}
|
||||
}
|
||||
OpIn::IsNurb {
|
||||
scale,
|
||||
net_scale,
|
||||
perturb,
|
||||
} => match is_nurb(k, t, *scale, *net_scale, perturb) {
|
||||
Err(e) if e.is_unported() => Err(e),
|
||||
// upstream returns the status of a failed SER call as a truthy result
|
||||
Err(e) => {
|
||||
o.extend([status(&e).max(1.0), 0.0]);
|
||||
Ok(())
|
||||
}
|
||||
Ok(None) => {
|
||||
o.extend([0.0, 0.0]);
|
||||
Ok(())
|
||||
}
|
||||
Ok(Some(found)) => {
|
||||
o.extend([1.0, 1.0]);
|
||||
put_state(o, &found)
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The shim's `OP_IS_NURB`: a copy of the torus's NURB, scaled and changed, as a plain
|
||||
/// B-spline surface owning it, given to `IsNurbSurfaceTorus`.
|
||||
fn is_nurb(
|
||||
k: &Kernel,
|
||||
t: &Torus,
|
||||
scale: f64,
|
||||
net_scale: [f64; 3],
|
||||
perturb: &[PerturbIn],
|
||||
) -> solidcore::Result<Option<Torus>> {
|
||||
let mut nurb = t
|
||||
.base()
|
||||
.base()
|
||||
.gw_nurb()
|
||||
.cloned()
|
||||
.ok_or_else(|| SolidError::null_pointer("difftest::surface_torus::is_nurb"))?;
|
||||
let ix = nurb.indices();
|
||||
for i in 0..=ix.n {
|
||||
for j in 0..=ix.m {
|
||||
let p = nurb.cpt_mut(i, j)?;
|
||||
p.x *= net_scale[0];
|
||||
p.y *= net_scale[1];
|
||||
p.z *= net_scale[2];
|
||||
}
|
||||
}
|
||||
for pt in perturb {
|
||||
if pt.i > ix.n || pt.j > ix.m {
|
||||
continue;
|
||||
}
|
||||
let p = nurb.cpt_mut(pt.i, pt.j)?;
|
||||
p.x += pt.d[0];
|
||||
p.y += pt.d[1];
|
||||
p.z += pt.d[2];
|
||||
}
|
||||
let test = BSplineSurface::from_nurb(nurb, false);
|
||||
Torus::is_nurb_surface_torus(k, &test, scale)
|
||||
}
|
||||
|
||||
fn rust(i: &Inputs) -> Result<Output, HarnessError> {
|
||||
let k = Kernel::default();
|
||||
let mut o = Vec::new();
|
||||
@@ -1254,6 +1349,7 @@ mod tests {
|
||||
OpIn::IsectPlane { .. } => "isect_plane",
|
||||
OpIn::Global { .. } => "global",
|
||||
OpIn::TrimDomain { .. } => "trim_domain",
|
||||
OpIn::IsNurb { .. } => "is_nurb",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//! * `Recognize`: `result has [state]`: the [`Edit`] is applied to the cone's NURB, the
|
||||
//! `SmBSplineSurface` copy of it is given to `IsNurbSurfaceCone(ctx, .., scale)`
|
||||
//! (`result` is its `SmBoolean`, or the error status it returns through `SER`);
|
||||
//! * `RecognizeCylinder`: as `Recognize`, with `SmCylinder::IsNurbSurfaceCylinder`;
|
||||
//! * `Rebuild`: `status state`: the [`Edit`] is applied to the cone's NURB, then
|
||||
//! `RebuildSTEPFromNURBParameters`; the state is the cone's afterwards;
|
||||
//! * `Trim`: `1000 n [min max]*n`, the intervals of `sm_GetEllipseTrimIntervals`.
|
||||
@@ -125,6 +126,13 @@ pub enum Op {
|
||||
/// Tolerance scale.
|
||||
scale: f64,
|
||||
},
|
||||
/// `SmCylinder::IsNurbSurfaceCylinder(ctx, copy of the edited NURB, .., scale)`.
|
||||
RecognizeCylinder {
|
||||
/// NURB edit.
|
||||
edit: Edit,
|
||||
/// Tolerance scale.
|
||||
scale: f64,
|
||||
},
|
||||
/// `RebuildSTEPFromNURBParameters` after an edit of the NURB.
|
||||
Rebuild {
|
||||
/// NURB edit.
|
||||
@@ -162,6 +170,7 @@ impl Op {
|
||||
Op::Recognize { .. } => 2,
|
||||
Op::Rebuild { .. } => 3,
|
||||
Op::Trim { .. } => 4,
|
||||
Op::RecognizeCylinder { .. } => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,7 +343,7 @@ fn push_op(o: &mut Vec<f64>, op: &Op) -> Result<(), FfiError> {
|
||||
finite(&[*distance, *tol], "offset")?;
|
||||
o.extend([*distance, *tol]);
|
||||
}
|
||||
Op::Recognize { edit, scale } => {
|
||||
Op::Recognize { edit, scale } | Op::RecognizeCylinder { edit, scale } => {
|
||||
push_edit(o, edit)?;
|
||||
finite(&[*scale], "recognize scale")?;
|
||||
o.push(*scale);
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
//! `state copy == this`; `TrimDomain`: the domain after the call (`TrimWithDomain`
|
||||
//! writes it back), then the state (also after a failure); `IsectPlane`, `Global`:
|
||||
//! status, then on success `more ncurves curves ntypes types ndevs devs nuv1 nuv2
|
||||
//! nnull1 nnull2` (`more` is `0` for `Global`). The plane and the intersection
|
||||
//! nnull1 nnull2` (`more` is `0` for `Global`); `IsNurb`: `found has [state]`
|
||||
//! (`found` is the `SmBoolean`, or the status a failed `SER` returns, in the status
|
||||
//! slot). The plane and the intersection
|
||||
//! arguments are those of [`super::surface_sphere`] ([`Plane`], [`IsectArgs`]).
|
||||
//!
|
||||
//! The wrappers accept only what upstream handles without undefined behaviour: finite
|
||||
@@ -31,7 +33,7 @@
|
||||
//! inside `[-360, 360]` at most 360 long, generator circles with independent axes and a
|
||||
//! positive radius, derivative orders up to 3.
|
||||
|
||||
pub use super::surface_sphere::{Dom, IsectArgs, Plane};
|
||||
pub use super::surface_sphere::{Dom, IsectArgs, Perturb, Plane};
|
||||
use super::surface_sphere::{push_args, push_plane};
|
||||
use crate::validate::bad;
|
||||
use crate::{call_with_buffer, FfiError};
|
||||
@@ -177,6 +179,16 @@ pub enum Op {
|
||||
/// The new NURB domain `[u0, v0, u1, v1]`.
|
||||
dom: [f64; 4],
|
||||
},
|
||||
/// `IsNurbSurfaceTorus` on a copy of the torus's NURB, edited, as a plain B-spline
|
||||
/// surface (`SmBSplineSurface(g, FALSE, NULL)`).
|
||||
IsNurb {
|
||||
/// Tolerance scale.
|
||||
scale: f64,
|
||||
/// Scale of the homogeneous `x`, `y`, `z`.
|
||||
net_scale: [f64; 3],
|
||||
/// Control point changes.
|
||||
perturb: Vec<Perturb>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Op {
|
||||
@@ -195,6 +207,7 @@ impl Op {
|
||||
Op::TrimDomain { .. } => 10,
|
||||
Op::IsectPlane { .. } => 11,
|
||||
Op::Global { .. } => 12,
|
||||
Op::IsNurb { .. } => 13,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,6 +371,28 @@ fn push_op(o: &mut Vec<f64>, op: &Op) -> Result<(), FfiError> {
|
||||
domain(*dom, "trim domain")?;
|
||||
o.extend_from_slice(dom);
|
||||
}
|
||||
Op::IsNurb {
|
||||
scale,
|
||||
net_scale,
|
||||
perturb,
|
||||
} => {
|
||||
finite(&[*scale], "scale")?;
|
||||
if *scale <= 0.0 {
|
||||
return Err(bad("scale"));
|
||||
}
|
||||
finite(net_scale, "net scale")?;
|
||||
if perturb.len() > 8 {
|
||||
return Err(bad("too many control point changes"));
|
||||
}
|
||||
o.push(*scale);
|
||||
o.extend_from_slice(net_scale);
|
||||
o.push(perturb.len() as f64);
|
||||
for p in perturb {
|
||||
finite(&p.d, "control point change")?;
|
||||
o.extend([p.i as f64, p.j as f64]);
|
||||
o.extend_from_slice(&p.d);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -259,6 +259,8 @@ pub struct UpstreamFixes {
|
||||
pub new_edge_vertex_tolerance_covers_gap: bool,
|
||||
/// A torus rebuilt from its NURB measures its generating interval signed (WR-408, #1112).
|
||||
pub torus_signed_gen_curve_interval: bool,
|
||||
/// `SmTorus::IsNurbSurfaceTorus` and `SmSphere::IsNurbSurfaceSphere` keep the NURB they tested (rebuilding the analytic data from it, as `SetFromGwNurb` does), not the one made from the fitted circles (~1e-11 off), and the sphere test takes a half meridian within `180 + 360 * SM_EFF_ZERO` degrees, not `180 + SM_EFF_ZERO` (WR-493, #1433; tangency-labs/smlib@7ea6a96).
|
||||
pub found_torus_sphere_keep_tested_nurb: bool,
|
||||
/// A sheet sweep's open-profile wires are glued where the curves meet (WR-409, #1113).
|
||||
pub sheet_sweep_joins_profile_wires: bool,
|
||||
/// `FindPolygonGuess` keeps a suspicious control-net triangle guess as a candidate against the nearest control point's, and returns the nearer one on the surface, so a rational surface whose control points are farther apart than its neighbouring parts does not lose a good guess (WR-444, #1164).
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
//! `SmCone` part 2: the offset cone, recognition of a cone NURB, the rebuild of the
|
||||
//! analytic data from an edited NURB, and the ellipse intervals inside a trimmed cone.
|
||||
//!
|
||||
//! Upstream: `source/SMLib/src/SmCone.cpp`, `source/SMLib/inc/SmCone.h`.
|
||||
//! Upstream: `source/SMLib/src/SmCone.cpp`, `source/SMLib/inc/SmCone.h`;
|
||||
//! `SmCylinder::IsNurbSurfaceCylinder` from `source/SMLib/src/SmCylinder.cpp`,
|
||||
//! `source/SMLib/inc/SmCylinder.h` (ported here, next to the cone recogniser whose iso
|
||||
//! arc test it shares).
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |----------------------------------------------|--------------------------------------------------|
|
||||
//! | [`Cone::create_offset_surface`] | `SmCone::CreateOffsetSurface` |
|
||||
//! | [`Cone::is_nurb_surface_cone`] | `SmCone::IsNurbSurfaceCone` |
|
||||
//! | [`Cone::is_nurb_surface_cylinder`] | `SmCylinder::IsNurbSurfaceCylinder` (`SmCylinder.cpp`) |
|
||||
//! | [`Cone::rebuild_step_from_nurb_parameters`] | `SmCone::RebuildSTEPFromNURBParameters` |
|
||||
//! | [`ellipse_trim_intervals`] | `sm_GetEllipseTrimIntervals` (file static) |
|
||||
//! | `is_cone_point_in_trim_domain` (private) | `sm_IsConePointInTrimDomain` (file static) |
|
||||
@@ -59,6 +63,11 @@
|
||||
//! their mean becomes its radius. A larger taper returns `SM_ERR` before editing
|
||||
//! the analytic state. The pinned C++ instead assigns a nonzero semi-angle to
|
||||
//! cylinders. Ordinary `SmCone` objects retain upstream's radius/angle rules.
|
||||
//! - **Cylinder recogniser.** [`Cone::is_nurb_surface_cylinder`] returns an error
|
||||
//! where a `SER` call returns its status (a truthy result without cylinder) and
|
||||
//! `Some` where upstream returns `TRUE` with a cylinder; its NURB is a copy of the
|
||||
//! tested one, as upstream. A tested surface without NURB is `SM_ERR_NULL_POINTER`
|
||||
//! (upstream copies a null NURB).
|
||||
//! - **Recursion.** None of these functions recurses.
|
||||
//! - **Debug code.** The `SM_DEBUG_CODE` checks and drawing are omitted.
|
||||
|
||||
@@ -84,8 +93,10 @@ use crate::nurbs::b_spline_surface::{
|
||||
BSplineSurface, IsoCurveOptions, LocationType, SurfParamType,
|
||||
};
|
||||
use crate::nurbs::nurbs_crv::to_euclid;
|
||||
use crate::nurbs::nurbs_srf::copy_nurb_surface;
|
||||
|
||||
const OP_OFFSET: &str = "surface::cone::p2::Cone::create_offset_surface";
|
||||
const OP_NURB_CYLINDER: &str = "surface::cone::p2::Cone::is_nurb_surface_cylinder";
|
||||
const OP_REBUILD: &str = "surface::cone::p2::Cone::rebuild_step_from_nurb_parameters";
|
||||
const OP_REPARAM: &str = "surface::cone::p2::Cone::reparameterize";
|
||||
const OP_TRIM: &str = "surface::cone::p2::ellipse_trim_intervals";
|
||||
@@ -732,6 +743,157 @@ impl Cone {
|
||||
Ok(Some(cone))
|
||||
}
|
||||
|
||||
// ---- SmCylinder::IsNurbSurfaceCylinder ------------------------------------------
|
||||
|
||||
/// The cylinder `test_surface` is (`SmCylinder::IsNurbSurfaceCylinder`), or `None`:
|
||||
/// the surface must be rational, of degree 1 with at most two control points in one
|
||||
/// direction and degree 2 in the other; its iso curves at both ends of the linear
|
||||
/// direction are non-degenerate circular arcs over the same angles, with the same
|
||||
/// X axis, of equal radii, on parallel planes perpendicular to the line through
|
||||
/// their centres (at least `SM_EFF_ZERO` apart). Tolerances are `SM_EFF_ZERO_SQRT`
|
||||
/// for angles and `tolerance_scale * SM_EFF_ZERO * 100 * (1 + |min corner| + |max
|
||||
/// corner|)` for distances and radii (upstream default scale `1`). The new bounded
|
||||
/// cylinder ([`Cone::is_cylinder`]) keeps a copy of the tested NURB as its own, its
|
||||
/// polar converter rebuilt from it and its generator reparameterised to the
|
||||
/// linear direction's NURB interval.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Upstream's `SER` statuses (returned as a `TRUE` result without cylinder): those
|
||||
/// of the iso curves; those of the degeneracy and arc tests;
|
||||
/// `SM_ERR_NULL_POINTER` for a surface without NURB (upstream copies the null NURB).
|
||||
pub fn is_nurb_surface_cylinder(
|
||||
k: &Kernel,
|
||||
test_surface: &BSplineSurface,
|
||||
tolerance_scale: f64,
|
||||
) -> Result<Option<Self>> {
|
||||
let deg_u = test_surface.degree(SurfParamType::U);
|
||||
let deg_v = test_surface.degree(SurfParamType::V);
|
||||
if deg_u + deg_v != 3
|
||||
|| !test_surface.is_rational()
|
||||
|| (deg_u == 1 && test_surface.number_control_points(SurfParamType::U) > 2)
|
||||
|| (deg_v == 1 && test_surface.number_control_points(SurfParamType::V) > 2)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// (evaluation statuses ignored upstream)
|
||||
let uv_domain = test_surface.natural_uv_domain();
|
||||
let min_pnt = ignored(test_surface.evaluate_point(uv_domain.min()))?.unwrap_or(Vec3::UNDEF);
|
||||
let max_pnt = ignored(test_surface.evaluate_point(uv_domain.max()))?.unwrap_or(Vec3::UNDEF);
|
||||
|
||||
let pt_tol = tolerance_scale
|
||||
* EFF_ZERO
|
||||
* ANALYTIC_TOL_SCALE
|
||||
* (1.0 + min_pnt.max_dimension() + max_pnt.max_dimension());
|
||||
|
||||
let ang_tol = EFF_ZERO_SQRT;
|
||||
let (swap_uv, surf_param, bot_knot, top_knot) = if deg_u == 1 {
|
||||
// Need to swap UV when mapping parameters from analytic domain to nurbs domain
|
||||
(true, SurfParamType::U, uv_domain.min().x, uv_domain.max().x)
|
||||
} else {
|
||||
(
|
||||
false,
|
||||
SurfParamType::V,
|
||||
uv_domain.min().y,
|
||||
uv_domain.max().y,
|
||||
)
|
||||
};
|
||||
|
||||
let top_iso = test_surface
|
||||
.create_iso_parametric_curve(k, surf_param, top_knot, IsoCurveOptions::default())?
|
||||
.curve;
|
||||
if top_iso.is_degenerate(k, pt_tol, None)? {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(top) = iso_arc(k, &top_iso, pt_tol)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let bot_iso = test_surface
|
||||
.create_iso_parametric_curve(k, surf_param, bot_knot, IsoCurveOptions::default())?
|
||||
.curve;
|
||||
if bot_iso.is_degenerate(k, pt_tol, None)? {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(bot) = iso_arc(k, &bot_iso, pt_tol)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if (top.start_ang - bot.start_ang).abs() > ang_tol
|
||||
|| (top.end_ang - bot.end_ang).abs() > ang_tol
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let center_top = top.frame.origin();
|
||||
let x_axis = top.frame.x_axis();
|
||||
// (status ignored upstream, which then reads the uninitialised angle; the arcs'
|
||||
// unit X axes never fail it: 0 here)
|
||||
let ang = ignored(x_axis.angle_between(bot.frame.x_axis()))?.unwrap_or(0.0);
|
||||
if ang.abs() > ang_tol {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let center_bot = bot.frame.origin();
|
||||
let mut z_axis = center_top - center_bot;
|
||||
let height = z_axis.length();
|
||||
|
||||
// If you don't do this, Unitize will fail
|
||||
if height < EFF_ZERO {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
z_axis = z_axis.unitize_or_z();
|
||||
if !z_axis.is_parallel_to(bot.frame.z_axis(), ang_tol) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let inside_out = z_axis.dot(bot.frame.z_axis()) < 0.0;
|
||||
|
||||
let y_axis = z_axis.cross(x_axis);
|
||||
// Try to eliminate some numerical inaccurracies (the top radius is not used
|
||||
// further)
|
||||
let equal_radii = (bot.radius - top.radius).abs() < pt_tol;
|
||||
if !equal_radii {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut cylinder = Self::new_cylinder_bounded(
|
||||
center_bot,
|
||||
x_axis,
|
||||
y_axis,
|
||||
bot.radius,
|
||||
top.start_ang,
|
||||
top.end_ang,
|
||||
height,
|
||||
swap_uv,
|
||||
inside_out,
|
||||
false,
|
||||
);
|
||||
// (error recovery: the constructor always makes its generator)
|
||||
|
||||
// the cylinder's NURB is a copy of the tested one
|
||||
let nurb = test_surface
|
||||
.gw_nurb()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_NURB_CYLINDER))?;
|
||||
cylinder.rev.base.nurb = Some(copy_nurb_surface(nurb)).into();
|
||||
cylinder.rev.base.nurb_is_borrowed = false;
|
||||
|
||||
// (Notify(SM_NO_COPY) copies the test surface's attributes: not modelled)
|
||||
|
||||
// (status ignored upstream)
|
||||
ignore_status(cylinder.rebuild_cone_polar_converter(k))?;
|
||||
|
||||
// (status ignored upstream)
|
||||
let gen_ivl = Extent1d::from_min_max(bot_knot, top_knot);
|
||||
if let Some(gen) = cylinder.rev.gen_curve.as_mut() {
|
||||
ignore_status(gen.edit_parameterization(k, &gen_ivl))?;
|
||||
}
|
||||
|
||||
Ok(Some(cylinder))
|
||||
}
|
||||
|
||||
// ---- RebuildSTEPFromNURBParameters ---------------------------------------------
|
||||
|
||||
/// Rebuilds the analytic data from the NURB after an edit
|
||||
@@ -1671,4 +1833,87 @@ mod tests {
|
||||
"{trimmed}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The control points and knots of two NURBs agree bit for bit.
|
||||
fn same_nurb(a: &BSplineSurface, b: &BSplineSurface) -> bool {
|
||||
match (a.gw_nurb(), b.gw_nurb()) {
|
||||
(Some(a), Some(b)) => {
|
||||
let rows = |s: &crate::nurbs::srf_basic::Surface| {
|
||||
s.rows()
|
||||
.flat_map(|r| r.iter().map(|p| [p.x, p.y, p.z, p.w]))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
rows(a) == rows(b) && a.u_knots() == b.u_knots() && a.v_knots() == b.v_knots()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// tangency-labs/solidcore#1433: the cylinder test finds the radius 5 cylinder
|
||||
/// about +Z from its NURB form and keeps exactly that NURB.
|
||||
#[test]
|
||||
fn issue_1433_cylinder_found_in_its_nurb_keeps_it() {
|
||||
let k = kernel();
|
||||
let cyl = Cone::create_canonical_cylinder(&k, &Axis2Placement::default(), 5.0).unwrap();
|
||||
let nurbs = cyl.rev().base().clone();
|
||||
let found = Cone::is_nurb_surface_cylinder(&k, &nurbs, 1.0)
|
||||
.unwrap()
|
||||
.expect("a cylinder");
|
||||
assert!(found.is_cylinder());
|
||||
assert_eq!(found.semi_angle_deg(), 0.0);
|
||||
assert!(
|
||||
(found.base_radius() - 5.0).abs() < 1e-12,
|
||||
"{}",
|
||||
found.base_radius()
|
||||
);
|
||||
let z = found
|
||||
.rev()
|
||||
.position()
|
||||
.x_axis()
|
||||
.cross(found.rev().position().y_axis());
|
||||
assert!(z.is_parallel_to(Vec3::new(0.0, 0.0, 1.0), 1e-9), "{z:?}");
|
||||
assert!(found.rev().position().origin().x.abs() < 1e-12);
|
||||
assert!(found.rev().position().origin().y.abs() < 1e-12);
|
||||
assert!(same_nurb(found.rev().base(), &nurbs));
|
||||
}
|
||||
|
||||
/// A bounded partial cylinder (either UV order, inside out) is found with its
|
||||
/// radius, angles and height; a cone and a tapered cylinder NURB are not cylinders.
|
||||
#[test]
|
||||
fn cylinder_recognition_partial_and_non_matching() {
|
||||
let k = kernel();
|
||||
for (swap, inside) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let frame = Axis2Placement::new(
|
||||
Vec3::new(1.0, -2.0, 3.0),
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
);
|
||||
let mut cyl = Cone::new_cylinder_bounded(
|
||||
frame.origin(),
|
||||
frame.x_axis(),
|
||||
frame.y_axis(),
|
||||
2.5,
|
||||
30.0,
|
||||
200.0,
|
||||
4.0,
|
||||
swap,
|
||||
inside,
|
||||
false,
|
||||
);
|
||||
cyl.make_nurb(&k).unwrap();
|
||||
let nurbs = cyl.rev().base().clone();
|
||||
let found = Cone::is_nurb_surface_cylinder(&k, &nurbs, 1.0)
|
||||
.unwrap()
|
||||
.expect("a cylinder");
|
||||
assert!((found.base_radius() - 2.5).abs() < 1e-12);
|
||||
assert!(same_nurb(found.rev().base(), &nurbs));
|
||||
let d = found.rev().step_uv_domain();
|
||||
assert!((d.max().x - d.min().x - 170.0).abs() < 1e-9, "{d:?}");
|
||||
assert!((d.max().y - d.min().y - 4.0).abs() < 1e-9, "{d:?}");
|
||||
}
|
||||
let cone = canonical(&k, 2.0, 30.0, 3.0, false, false);
|
||||
assert!(Cone::is_nurb_surface_cylinder(&k, cone.rev().base(), 1.0)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
//! point only when the intersection circle's own radius is within the tolerance, not
|
||||
//! when the gap to tangency is, which collapses circles of radius up to about
|
||||
//! `sqrt(2 R tol)`. Off by default (upstream behaviour). No input-driven recursion.
|
||||
//! - **Opt-in fix** ([`UpstreamFixes::found_torus_sphere_keep_tested_nurb`](crate::kernel::UpstreamFixes::found_torus_sphere_keep_tested_nurb),
|
||||
//! WR-493, the SolidCraft fork's tangency-labs/smlib@7ea6a96): `IsNurbSurfaceSphere`
|
||||
//! tests the half meridian within `180 + 360 * SM_EFF_ZERO` degrees and the sphere it
|
||||
//! finds keeps the tested NURB (`SetFromGwNurb`). Off by default (upstream).
|
||||
//! - **Parent calls.** `Transform` finishes in `SmSurfOfRevolution::Transform` (status
|
||||
//! ignored), its polar-converter rebuild given the sphere's iso circles as upstream's
|
||||
//! virtual call does; `ReadFromDB` reads the parent record with
|
||||
@@ -125,6 +129,7 @@ use crate::nurbs::b_spline_surface::{
|
||||
BSplineSurface, BoundingBoxRequest, IsoCurveOptions, LocationType, SurfParamType,
|
||||
};
|
||||
use crate::nurbs::nurbs_crv::to_euclid;
|
||||
use crate::nurbs::nurbs_srf::copy_nurb_surface;
|
||||
use crate::surface::cone::Cone;
|
||||
use crate::surface::plane::p2::PlaneOwner;
|
||||
use crate::surface::plane::Plane;
|
||||
@@ -1887,6 +1892,13 @@ impl Sphere {
|
||||
/// domain of `test` (through [`Self::reparameterize`]). `None` when `test` is not
|
||||
/// a sphere.
|
||||
///
|
||||
/// With the opt-in
|
||||
/// [`UpstreamFixes::found_torus_sphere_keep_tested_nurb`](crate::kernel::UpstreamFixes::found_torus_sphere_keep_tested_nurb)
|
||||
/// (WR-493) the half turn is tested within `180 + 360 * SM_EFF_ZERO` degrees and the
|
||||
/// sphere then keeps a copy of `test`'s NURB, its analytic data rebuilt from it
|
||||
/// (`SetFromGwNurb`); by default `180 + SM_EFF_ZERO` and the NURB made from the
|
||||
/// fitted circles, as upstream.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Upstream's `SER` statuses (returned as a `TRUE` result without sphere): those of
|
||||
@@ -1937,14 +1949,21 @@ impl Sphere {
|
||||
// see if we have a U or a V oriented sphere
|
||||
let angles_u = iso_u.angles.unwrap_or(Extent1d::new());
|
||||
let angles_v = iso_v.angles.unwrap_or(Extent1d::new());
|
||||
// not upstream (WR-493, opt-in; tangency-labs/smlib@7ea6a96): the half-meridian
|
||||
// tolerance compensates for degrees, as RebuildSTEPFromNURBParameters [B279]
|
||||
let half_meridian_tol = if k.fixes.found_torus_sphere_keep_tested_nurb {
|
||||
360.0 * EFF_ZERO
|
||||
} else {
|
||||
EFF_ZERO
|
||||
};
|
||||
let is_sphere_with_swap_uv = iso_u.parallel_planes
|
||||
&& !iso_u.same_plane // [ 080625 ]
|
||||
&& iso_v.coaxial_centers
|
||||
&& angles_v.length() <= 180.0 + EFF_ZERO;
|
||||
&& angles_v.length() <= 180.0 + half_meridian_tol;
|
||||
let is_sphere_no_swap_uv = iso_v.parallel_planes
|
||||
&& !iso_v.same_plane // [ 080625 ]
|
||||
&& iso_u.coaxial_centers
|
||||
&& angles_u.length() <= 180.0 + EFF_ZERO;
|
||||
&& angles_u.length() <= 180.0 + half_meridian_tol;
|
||||
|
||||
// quit when not a properly oriented sphere
|
||||
if !is_sphere_with_swap_uv && !is_sphere_no_swap_uv {
|
||||
@@ -2086,6 +2105,17 @@ impl Sphere {
|
||||
Ok(()) => {}
|
||||
}
|
||||
|
||||
// not upstream (WR-493, opt-in; tangency-labs/smlib@7ea6a96): the sphere keeps
|
||||
// the NURB tested (SetFromGwNurb, whose rebuild's status is ignored)
|
||||
if k.fixes.found_torus_sphere_keep_tested_nurb {
|
||||
let nurb = test
|
||||
.gw_nurb()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_NURB_SPHERE))?;
|
||||
sphere.base.base.nurb = Some(copy_nurb_surface(nurb)).into();
|
||||
sphere.base.base.nurb_is_borrowed = false;
|
||||
ignore_status(sphere.rebuild_step_from_nurb_parameters(k))?;
|
||||
}
|
||||
|
||||
// (Notify(SM_NO_COPY) copies the test surface's attributes: not modelled)
|
||||
Ok(Some(sphere))
|
||||
}
|
||||
@@ -2950,4 +2980,36 @@ mod tests {
|
||||
let r = Sphere::read_from_db(&Kernel::default(), &mut db, 0);
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
/// The control points and knots of two NURBs agree bit for bit.
|
||||
fn same_nurb(a: &BSplineSurface, b: &BSplineSurface) -> bool {
|
||||
match (a.gw_nurb(), b.gw_nurb()) {
|
||||
(Some(a), Some(b)) => {
|
||||
let rows = |s: &crate::nurbs::srf_basic::Surface| {
|
||||
s.rows()
|
||||
.flat_map(|r| r.iter().map(|p| [p.x, p.y, p.z, p.w]))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
rows(a) == rows(b) && a.u_knots() == b.u_knots() && a.v_knots() == b.v_knots()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// tangency-labs/smlib@7ea6a96 (WR-493): with `found_torus_sphere_keep_tested_nurb`
|
||||
/// the sphere found in a NURB keeps exactly that NURB.
|
||||
#[test]
|
||||
fn wr493_sphere_found_in_its_nurb_keeps_it() {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.found_torus_sphere_keep_tested_nurb = true;
|
||||
for radius in [2.0, -2.0, 1e-3, 500.0] {
|
||||
let s = canonical(&k, radius);
|
||||
let nurbs = BSplineSurface::from_gw_nurb(s.base().base().gw_nurb().unwrap());
|
||||
let found = Sphere::is_nurb_surface_sphere(&k, &nurbs, 1.0)
|
||||
.unwrap()
|
||||
.expect("a sphere");
|
||||
assert!(same_nurb(found.base().base(), &nurbs));
|
||||
assert!((found.radius() - radius.abs()).abs() < 1e-9 * radius.abs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
//! | [`Torus::convert_uv_from_nurbs_to_step`] | `ConvertUVFromNURBSToSTEP` |
|
||||
//! | [`Torus::evaluate_step`], [`Torus::evaluate_step_point`] | `EvaluateSTEP`, `EvaluateSTEPPoint` |
|
||||
//! | [`Torus::create_iso_parametric_curve`] | `CreateIsoParametricCurve` |
|
||||
//! | [`Torus::is_nurb_surface_torus`] | `IsNurbSurfaceTorus` |
|
||||
//! | [`Torus::rebuild_step_from_nurb_parameters`] | `RebuildSTEPFromNURBParameters` |
|
||||
//! | [`Torus::transform`] | `Transform` |
|
||||
//! | [`Torus::read_from_db`] | `ReadFromDB` |
|
||||
@@ -66,6 +67,15 @@
|
||||
//! an ellipse, which upstream casts to `SmCircle`, is `InvalidInput`.
|
||||
//! `RebuildSTEPFromNURBParameters` returns `FALSE` (`SM_SUCCESS`) when the
|
||||
//! constant-u iso curves are not circles; that is `Ok(())` with nothing changed.
|
||||
//! - **Recognition.** [`Torus::is_nurb_surface_torus`] returns `Some` where upstream
|
||||
//! returns `TRUE` with a torus and an error where a `SER` call returns its status
|
||||
//! (a truthy result without torus). `Notify(SM_NO_COPY)` (attribute copying) has no
|
||||
//! equivalent. Two opt-in fork fixes: the torus keeps the tested NURB
|
||||
//! (`Kernel::fixes.found_torus_sphere_keep_tested_nurb`, WR-493; upstream keeps the
|
||||
//! NURB made from the fitted circles, ~1e-11 off the tested one), and the generator
|
||||
//! interval is measured signed (`Kernel::fixes.torus_signed_gen_curve_interval`,
|
||||
//! WR-408; upstream's unsigned `AngleBetween` mirrors an interval below the equator).
|
||||
//! Both are off by default.
|
||||
//! - **Parent calls.** `Transform` finishes in `SmSurfOfRevolution::Transform`
|
||||
//! (status ignored), its polar-converter rebuild given the torus's iso circles as
|
||||
//! upstream's virtual call does; `ReadFromDB` reads the parent record with
|
||||
@@ -85,8 +95,11 @@ use crate::geom::vector2d::Point2;
|
||||
use crate::geom::vector3d::{Point3, Vec3};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::{max2, min2, EFF_ZERO, EFF_ZERO_DEG, EFF_ZERO_SQRT, PI, UNDEF_DOUBLE};
|
||||
use crate::nurbs::b_spline_surface::{BSplineSurfaceForm, IsoCurveOptions, SurfParamType};
|
||||
use crate::nurbs::b_spline_surface::{
|
||||
BSplineSurface, BSplineSurfaceForm, IsoCurveOptions, SurfParamType,
|
||||
};
|
||||
use crate::nurbs::nurbs_crv::to_euclid;
|
||||
use crate::nurbs::nurbs_srf::copy_nurb_surface;
|
||||
use crate::surface::plane::p2::PlaneOwner;
|
||||
use crate::surface::plane::Plane;
|
||||
use crate::surface::surf_of_revolution::{
|
||||
@@ -105,6 +118,7 @@ const OP_CONVERT: &str = "surface::torus::Torus::convert_uv";
|
||||
const OP_EVAL_STEP: &str = "surface::torus::Torus::evaluate_step";
|
||||
const OP_REBUILD: &str = "surface::torus::Torus::rebuild_step_from_nurb_parameters";
|
||||
const OP_TRANSFORM: &str = "surface::torus::Torus::transform";
|
||||
const OP_NURB_TORUS: &str = "surface::torus::Torus::is_nurb_surface_torus";
|
||||
|
||||
/// `ANALYTIC_TOL_SCALE` (`SmConfig.h`).
|
||||
const ANALYTIC_TOL_SCALE: f64 = 100.0;
|
||||
@@ -875,6 +889,246 @@ impl Torus {
|
||||
)
|
||||
}
|
||||
|
||||
// ---- recognition ----------------------------------------------------------------
|
||||
|
||||
/// The torus with the parameterisation of `test` when `test` is one
|
||||
/// (`IsNurbSurfaceTorus`): a rational surface of degree 2 both ways whose
|
||||
/// constant-u and constant-v iso curves are circles ([`test_for_iso_circles`] at
|
||||
/// `tolerance_scale * ANALYTIC_TOL_SCALE * SM_EFF_ZERO * (1 + |corner points|)` and
|
||||
/// the angle tolerance `SM_EFF_ZERO_SQRT * tolerance_scale`), those in one direction
|
||||
/// on parallel planes (not one plane) and those in the other on planes through a
|
||||
/// common line (the axis); both radii above `SM_EFF_ZERO`. The torus is built from
|
||||
/// the circles found, its generator the minor circle over the NURB's first iso
|
||||
/// curve, and takes the NURB domain of `test` (through [`Self::reparameterize`]).
|
||||
/// `None` when `test` is not a torus (upstream default scale `1`).
|
||||
///
|
||||
/// With the opt-in
|
||||
/// [`UpstreamFixes::found_torus_sphere_keep_tested_nurb`](crate::kernel::UpstreamFixes::found_torus_sphere_keep_tested_nurb)
|
||||
/// (WR-493) the torus then keeps a copy of `test`'s NURB and rebuilds its analytic
|
||||
/// data from it (`SetFromGwNurb`); by default it keeps the NURB made from the
|
||||
/// fitted circles, as upstream. With
|
||||
/// [`UpstreamFixes::torus_signed_gen_curve_interval`](crate::kernel::UpstreamFixes::torus_signed_gen_curve_interval)
|
||||
/// (WR-408) the generator's interval is measured signed.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Upstream's `SER` statuses (returned as a `TRUE` result without torus): those of
|
||||
/// the evaluations, the iso-circle tests, the iso curve and the circle, and of the
|
||||
/// constructor; `InvalidInput` when an iso-circle output upstream would read is
|
||||
/// missing.
|
||||
pub fn is_nurb_surface_torus(
|
||||
k: &Kernel,
|
||||
test: &BSplineSurface,
|
||||
tolerance_scale: f64,
|
||||
) -> Result<Option<Self>> {
|
||||
// no work - not a degree 2 rational surface in both directions
|
||||
if test.degree(SurfParamType::U) != 2
|
||||
|| test.degree(SurfParamType::V) != 2
|
||||
|| !test.is_rational()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// evaluate Nurb min/max domain points
|
||||
let nurb_uv_domain = test.natural_uv_domain();
|
||||
let nurb_min_point = test.evaluate_point(nurb_uv_domain.min())?;
|
||||
let nurb_max_point = test.evaluate_point(nurb_uv_domain.max())?;
|
||||
|
||||
// scale zero value
|
||||
let scaled_zero = tolerance_scale
|
||||
* ANALYTIC_TOL_SCALE
|
||||
* EFF_ZERO
|
||||
* (1.0 + nurb_min_point.max_dimension() + nurb_max_point.max_dimension());
|
||||
let ang_tol = EFF_ZERO_SQRT * tolerance_scale;
|
||||
|
||||
// test sequence of V Direction (U Constant) isoCurves for circularity and
|
||||
// parallelism
|
||||
let iso_u = test_for_iso_circles(k, test, SurfParamType::U, scaled_zero, ang_tol)?;
|
||||
// quit when not a torus
|
||||
if !iso_u.found_circles {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// test sequence of U Direction (V Constant) isoCurves for circularity and
|
||||
// parallelism
|
||||
let iso_v = test_for_iso_circles(k, test, SurfParamType::V, scaled_zero, ang_tol)?;
|
||||
// quit when not a torus
|
||||
if !iso_v.found_circles {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// see if we have a U or a V oriented torus
|
||||
let is_torus_with_swap_uv = iso_u.parallel_planes && !iso_u.same_plane && iso_v.common_axis;
|
||||
let is_torus_no_swap_uv = iso_v.parallel_planes && !iso_v.same_plane && iso_u.common_axis;
|
||||
|
||||
// quit when not a properly oriented torus
|
||||
if !is_torus_with_swap_uv && !is_torus_no_swap_uv {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// locals
|
||||
let min_uv = nurb_uv_domain.evaluate(0.0, 0.0);
|
||||
let missing = || SolidError::invalid_input(OP_NURB_TORUS, "iso circle output not written");
|
||||
|
||||
// Get torus position and parameters: the frame where all circles are parallel,
|
||||
// and the one where all circle planes intersect on a common line
|
||||
let (swap_uv, ref_iso, other_iso) = if is_torus_no_swap_uv {
|
||||
(false, &iso_v, &iso_u)
|
||||
} else {
|
||||
(true, &iso_u, &iso_v)
|
||||
};
|
||||
let ref_frame = ref_iso.reference_frame.ok_or_else(missing)?;
|
||||
let other_frame = other_iso.reference_frame.ok_or_else(missing)?;
|
||||
let min_rad = other_iso.radius.ok_or_else(missing)?;
|
||||
let sweep_ivl = ref_iso.angles.ok_or_else(missing)?;
|
||||
let common_point = other_iso.common_line_point.ok_or_else(missing)?;
|
||||
let common_vec = other_iso.common_line_vec.ok_or_else(missing)?;
|
||||
|
||||
// get torus orientation and size
|
||||
let major_vec = other_frame.origin() - ref_frame.origin();
|
||||
let maj_rad = major_vec.length();
|
||||
let torus_x_axis = ref_frame.x_axis(); // the frame where all circles are parallel
|
||||
let torus_y_axis = ref_frame.y_axis();
|
||||
let torus_z_axis = ref_frame.z_axis();
|
||||
|
||||
// torus must have positive radii
|
||||
if min_rad <= EFF_ZERO || maj_rad <= EFF_ZERO {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// get torus origin = MinorCircleCenter projected onto common line
|
||||
let torus_origin =
|
||||
common_point + common_vec.dot(other_frame.origin() - common_point) * common_vec;
|
||||
|
||||
// Check GenCurve minor circle rotation direction, its y axis is either +/-
|
||||
// sweepCircle z
|
||||
let minor_orientation = other_frame.z_axis().cross(major_vec);
|
||||
let inside_out = torus_z_axis.dot(minor_orientation) < 0.0;
|
||||
|
||||
// project Nurb Min/Max Point to Torus XY plane to get vec to Center of each
|
||||
// GenCurve
|
||||
let min_plane_point = point_project_to_plane(nurb_min_point, torus_origin, torus_z_axis);
|
||||
let max_plane_point = point_project_to_plane(nurb_max_point, torus_origin, torus_z_axis);
|
||||
let start_for_min_angle = (min_plane_point - torus_origin).unitize_or_z();
|
||||
let start_for_max_angle = (max_plane_point - torus_origin).unitize_or_z();
|
||||
let min_origin = torus_origin + maj_rad * start_for_min_angle;
|
||||
let max_origin = torus_origin + maj_rad * start_for_max_angle;
|
||||
|
||||
// Get Vec from Nurb Min/Max points to each genCurve origin
|
||||
let vec_min = nurb_min_point - min_origin;
|
||||
let vec_max = nurb_max_point - max_origin;
|
||||
|
||||
// get GenCurve min and max angles measured from each GenCurve's origin and start
|
||||
// vector
|
||||
let (gen_angle_min, gen_angle_max) = if k.fixes.torus_signed_gen_curve_interval {
|
||||
// not upstream (WR-408, tangency-labs/solidcore#1112, opt-in): signed
|
||||
// angles; a failed angle is not a torus, as the fork
|
||||
match torus_gen_curve_interval(
|
||||
[start_for_min_angle, vec_min],
|
||||
[start_for_max_angle, vec_max],
|
||||
torus_z_axis,
|
||||
inside_out,
|
||||
EFF_ZERO,
|
||||
) {
|
||||
Ok(ivl) => ivl,
|
||||
Err(e) if e.is_unported() => return Err(e),
|
||||
Err(_) => return Ok(None),
|
||||
}
|
||||
} else {
|
||||
// (statuses ignored upstream: 0 on failure)
|
||||
let mut gen_angle_min =
|
||||
ignored(start_for_min_angle.angle_between(vec_min))?.unwrap_or(0.0);
|
||||
let mut gen_angle_max =
|
||||
ignored(start_for_max_angle.angle_between(vec_max))?.unwrap_or(0.0);
|
||||
gen_angle_min = 180.0 * gen_angle_min / PI;
|
||||
gen_angle_max = 180.0 * gen_angle_max / PI;
|
||||
|
||||
// snap to ends for small tolerances
|
||||
if gen_angle_min.abs() < EFF_ZERO {
|
||||
gen_angle_min = 0.0;
|
||||
}
|
||||
if gen_angle_max.abs() < EFF_ZERO || (gen_angle_max - 360.0).abs() < EFF_ZERO {
|
||||
gen_angle_max = 360.0;
|
||||
}
|
||||
if gen_angle_max < gen_angle_min {
|
||||
gen_angle_max += 360.0;
|
||||
}
|
||||
(gen_angle_min, gen_angle_max)
|
||||
};
|
||||
|
||||
// define Torus domain
|
||||
let anal_uv_domain = Extent2d::from_bounds(
|
||||
sweep_ivl.min(),
|
||||
gen_angle_min,
|
||||
sweep_ivl.max(),
|
||||
gen_angle_max,
|
||||
);
|
||||
|
||||
// Get GenCurve Isoparameter Curve
|
||||
let gen_curve_iso = test.create_iso_parametric_curve(
|
||||
k,
|
||||
if swap_uv {
|
||||
SurfParamType::V
|
||||
} else {
|
||||
SurfParamType::U
|
||||
},
|
||||
if swap_uv { min_uv.y } else { min_uv.x },
|
||||
IsoCurveOptions::default(),
|
||||
)?;
|
||||
let circle = Ellipse::new_circle(
|
||||
k,
|
||||
min_origin,
|
||||
start_for_min_angle,
|
||||
torus_z_axis,
|
||||
anal_uv_domain.v_interval(),
|
||||
min_rad,
|
||||
&EllipseOptions {
|
||||
dim: 3,
|
||||
nurb: Some(&gen_curve_iso.curve),
|
||||
inside_out,
|
||||
},
|
||||
)?;
|
||||
|
||||
// build a new torus with given GenCurve to force parameterization compatibility
|
||||
// constant U Curve orientations,
|
||||
// constant V Curve centers and radius,
|
||||
// and computed domain
|
||||
let mut torus = Self::new(
|
||||
k,
|
||||
torus_origin,
|
||||
torus_x_axis,
|
||||
torus_y_axis,
|
||||
anal_uv_domain,
|
||||
maj_rad,
|
||||
min_rad,
|
||||
TorusOptions {
|
||||
swap_uv,
|
||||
inside_out,
|
||||
circle_nurb: Some(Curve::from(circle)),
|
||||
},
|
||||
)?;
|
||||
// (error recovery: the constructor always stores the given generator)
|
||||
|
||||
// reparameterize the NURB so that AnalyticSurface->NurbDomain ==
|
||||
// InputSurface->NurbDomain (status ignored)
|
||||
ignore_status(torus.reparameterize(k, &test.natural_uv_domain()))?;
|
||||
|
||||
// not upstream (WR-493, opt-in; the SolidCraft fork's tangency-labs/smlib@7ea6a96):
|
||||
// the torus keeps the NURB tested (SetFromGwNurb, whose rebuild's status is
|
||||
// ignored)
|
||||
if k.fixes.found_torus_sphere_keep_tested_nurb {
|
||||
let nurb = test
|
||||
.gw_nurb()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_NURB_TORUS))?;
|
||||
torus.base.base.nurb = Some(copy_nurb_surface(nurb)).into();
|
||||
torus.base.base.nurb_is_borrowed = false;
|
||||
ignore_status(torus.rebuild_step_from_nurb_parameters(k))?;
|
||||
}
|
||||
|
||||
// (Notify(SM_NO_COPY) copies the test surface's attributes: not modelled)
|
||||
Ok(Some(torus))
|
||||
}
|
||||
|
||||
// ---- analytic data from the NURB ------------------------------------------------
|
||||
|
||||
/// Rebuilds the generator, radii, inside-out flag, placement, swap flag, STEP
|
||||
@@ -991,6 +1245,7 @@ impl Torus {
|
||||
[start_for_max_angle, vec_max],
|
||||
torus_z_axis,
|
||||
inside_out,
|
||||
EFF_ZERO_DEG,
|
||||
)?
|
||||
} else {
|
||||
// get GenCurve min and max angles (statuses ignored upstream: 0 on failure)
|
||||
@@ -1371,8 +1626,9 @@ fn convert_uv_from_nurbs_to_step(
|
||||
/// to the point. Each angle is measured counter-clockwise from X toward the torus's
|
||||
/// `z_axis` (about `x * z_axis`), as `EvaluateSTEP` measures V, over a whole turn; an
|
||||
/// inside-out torus runs its NURB the other way round, so its last point starts the
|
||||
/// interval. Angles within `EFF_ZERO_DEG` of 0 snap to it, and the end is lifted a turn
|
||||
/// when it is not above the start. Upstream measured both with the unsigned
|
||||
/// interval. Angles within `zero_deg` of 0 snap to it, and the end is lifted a turn
|
||||
/// when it is not above the start by `zero_deg` (`SM_EFF_ZERO_DEG` in the rebuild,
|
||||
/// `SM_EFF_ZERO` in [`Torus::is_nurb_surface_torus`], as the fork). Upstream measured both with the unsigned
|
||||
/// `AngleBetween`, which mirrors an interval below the equator or past 180 degrees
|
||||
/// (-90..90 became 90..450, the inner half of the tube).
|
||||
///
|
||||
@@ -1384,6 +1640,7 @@ fn torus_gen_curve_interval(
|
||||
last: [Vec3; 2],
|
||||
z_axis: Vec3,
|
||||
inside_out: bool,
|
||||
zero_deg: f64,
|
||||
) -> Result<(f64, f64)> {
|
||||
let [x_first, vec_first] = first;
|
||||
let [x_last, vec_last] = last;
|
||||
@@ -1398,13 +1655,13 @@ fn torus_gen_curve_interval(
|
||||
} else {
|
||||
(first_deg, last_deg)
|
||||
};
|
||||
if low.abs() < EFF_ZERO_DEG {
|
||||
if low.abs() < zero_deg {
|
||||
low = 0.0;
|
||||
}
|
||||
if high.abs() < EFF_ZERO_DEG {
|
||||
if high.abs() < zero_deg {
|
||||
high = 0.0;
|
||||
}
|
||||
if high < low + EFF_ZERO_DEG {
|
||||
if high < low + zero_deg {
|
||||
high += 360.0;
|
||||
}
|
||||
Ok((low, high))
|
||||
@@ -1703,4 +1960,122 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The control points and knots of two NURBs agree bit for bit.
|
||||
fn same_nurb(a: &BSplineSurface, b: &BSplineSurface) -> bool {
|
||||
match (a.gw_nurb(), b.gw_nurb()) {
|
||||
(Some(a), Some(b)) => {
|
||||
let rows = |s: &crate::nurbs::srf_basic::Surface| {
|
||||
s.rows()
|
||||
.flat_map(|r| r.iter().map(|p| [p.x, p.y, p.z, p.w]))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
rows(a) == rows(b) && a.u_knots() == b.u_knots() && a.v_knots() == b.v_knots()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn plain(t: &Torus) -> BSplineSurface {
|
||||
BSplineSurface::from_gw_nurb(t.base().base().gw_nurb().expect("nurb"))
|
||||
}
|
||||
|
||||
/// tangency-labs/solidcore#1433: the torus R 10, r 3 is found in its NURB form;
|
||||
/// with `found_torus_sphere_keep_tested_nurb` it keeps exactly that NURB.
|
||||
#[test]
|
||||
fn issue_1433_torus_found_in_its_nurb_keeps_it() {
|
||||
let mut k = Kernel::default();
|
||||
let t = Torus::create_canonical(
|
||||
&k,
|
||||
&Axis2Placement::default(),
|
||||
10.0,
|
||||
3.0,
|
||||
TorusOptions::default(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let nurbs = plain(&t);
|
||||
|
||||
// upstream: found, with the NURB made from the fitted circles
|
||||
let found = Torus::is_nurb_surface_torus(&k, &nurbs, 1.0)
|
||||
.unwrap()
|
||||
.expect("a torus");
|
||||
assert!((found.major_radius() - 10.0).abs() < 1e-9);
|
||||
assert!((found.minor_radius() - 3.0).abs() < 1e-9);
|
||||
|
||||
k.fixes.found_torus_sphere_keep_tested_nurb = true;
|
||||
let found = Torus::is_nurb_surface_torus(&k, &nurbs, 1.0)
|
||||
.unwrap()
|
||||
.expect("a torus");
|
||||
assert!(same_nurb(found.base().base(), &nurbs));
|
||||
assert!((found.major_radius() - 10.0).abs() < 1e-9);
|
||||
assert!((found.minor_radius() - 3.0).abs() < 1e-9);
|
||||
let z = found.base().position().z_axis();
|
||||
assert!(z.is_parallel_to(Vec3::new(0.0, 0.0, 1.0), 1e-9), "{z:?}");
|
||||
let o = found.base().position().origin();
|
||||
assert!(o.length() < 1e-9, "{o:?}");
|
||||
}
|
||||
|
||||
/// Partial tori (swapped, inside out, below the equator, scaled, tiny) are found
|
||||
/// with the flags on and keep their NURB; a perturbed NURB is not a torus.
|
||||
#[test]
|
||||
fn torus_recognition_partial_scaled_and_non_matching() {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.found_torus_sphere_keep_tested_nurb = true;
|
||||
k.fixes.torus_signed_gen_curve_interval = true;
|
||||
let frame = Axis2Placement::new(
|
||||
Vec3::new(1.0, 2.0, 0.5),
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
);
|
||||
for (scale, dom) in [
|
||||
(1.0, Extent2d::from_bounds(10.0, 0.0, 100.0, 90.0)),
|
||||
(1.0, Extent2d::from_bounds(0.0, -90.0, 360.0, 90.0)),
|
||||
(1e3, Extent2d::from_bounds(0.0, 30.0, 270.0, 300.0)),
|
||||
(1e-3, Extent2d::from_bounds(45.0, 0.0, 135.0, 360.0)),
|
||||
] {
|
||||
for (swap, inside) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let t = Torus::create_canonical(
|
||||
&k,
|
||||
&frame,
|
||||
10.0 * scale,
|
||||
3.0 * scale,
|
||||
TorusOptions {
|
||||
swap_uv: swap,
|
||||
inside_out: inside,
|
||||
circle_nurb: None,
|
||||
},
|
||||
Some(&dom),
|
||||
)
|
||||
.unwrap();
|
||||
let nurbs = plain(&t);
|
||||
let found = Torus::is_nurb_surface_torus(&k, &nurbs, 1.0)
|
||||
.unwrap()
|
||||
.expect("a torus");
|
||||
assert!(same_nurb(found.base().base(), &nurbs));
|
||||
assert!((found.major_radius() - 10.0 * scale).abs() < 1e-9 * scale);
|
||||
assert!((found.minor_radius() - 3.0 * scale).abs() < 1e-9 * scale);
|
||||
// the found torus is the tested surface
|
||||
let d = nurbs.natural_uv_domain();
|
||||
for (a, b) in [(0.25, 0.5), (0.5, 0.75), (0.9, 0.1)] {
|
||||
let uv = d.evaluate(a, b);
|
||||
let p = nurbs.evaluate_point(uv).unwrap();
|
||||
let step = found.convert_uv_from_nurbs_to_step(&k, uv).unwrap();
|
||||
let q = found.evaluate_step_point(step).unwrap();
|
||||
assert!((p - q).length() < 1e-9 * scale, "{p:?} {q:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (a sphere's NURB passes upstream's torus test too, its meridians' planes
|
||||
// through the axis: the C++ comparison covers it in the surface.torus difftest)
|
||||
|
||||
let t = canonical(&k, false, false);
|
||||
let mut bent = plain(&t);
|
||||
let p = bent.get_or_create_gw_nurb().unwrap().cpt_mut(2, 3).unwrap();
|
||||
p.z += 0.05;
|
||||
assert!(Torus::is_nurb_surface_torus(&k, &bent, 1.0)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// difftest shim for unit `surface.cone_p2`.
|
||||
// Wraps the SmCone members owned by the unit (source/SMLib/src/SmCone.cpp):
|
||||
// CreateOffsetSurface, IsNurbSurfaceCone and RebuildSTEPFromNURBParameters, plus a verbatim
|
||||
// CreateOffsetSurface, IsNurbSurfaceCone and RebuildSTEPFromNURBParameters (and
|
||||
// SmCylinder::IsNurbSurfaceCylinder from SmCylinder.cpp), plus a verbatim
|
||||
// copy of the file-statics sm_IsConePointInTrimDomain and sm_GetEllipseTrimIntervals.
|
||||
//
|
||||
// One entry point, sc_surface_cone_p2_eval(op, in, n_in, out, cap, out_len). Layouts are
|
||||
@@ -441,6 +442,7 @@ enum : int32_t
|
||||
OP_RECOGNIZE = 2,
|
||||
OP_REBUILD = 3,
|
||||
OP_TRIM = 4,
|
||||
OP_RECOGNIZE_CYLINDER = 5,
|
||||
};
|
||||
|
||||
// Construction modes (the surface.cone_p1 shim's).
|
||||
@@ -871,6 +873,23 @@ void cone_op(int32_t op, Rd& rd, const SmContext& ctx, SmCone* s, std::vector<do
|
||||
delete test;
|
||||
break;
|
||||
}
|
||||
case OP_RECOGNIZE_CYLINDER:
|
||||
{
|
||||
edit_nurb(rd, s->GetGwNurbPointer(), s->GetSwapUV() != FALSE);
|
||||
double scale = rd.num();
|
||||
SmBSplineSurface* test = new (ctx) SmBSplineSurface(*(SmBSplineSurface*)s);
|
||||
SmCylinder* found = NULL;
|
||||
SmBoolean res = SmCylinder::IsNurbSurfaceCylinder(ctx, test, found, scale);
|
||||
o.push_back((double)res);
|
||||
o.push_back(flg(found != NULL));
|
||||
if (found != NULL)
|
||||
{
|
||||
put_state(o, found);
|
||||
delete found;
|
||||
}
|
||||
delete test;
|
||||
break;
|
||||
}
|
||||
case OP_REBUILD:
|
||||
{
|
||||
edit_nurb(rd, s->GetGwNurbPointer(), s->GetSwapUV() != FALSE);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// (the constructor and MakeNurb), operator==, Copy, CreateOffsetSurface,
|
||||
// ConvertUVFromSTEPToNURBS, ConvertUVFromNURBSToSTEP, EvaluateSTEP, EvaluateSTEPPoint,
|
||||
// CreateIsoParametricCurve, RebuildSTEPFromNURBParameters, Transform, GetMaxAnalyticDomain,
|
||||
// GetInsideOut and IsKindOf; and the inherited SmSurfOfRevolution::TrimWithDomain,
|
||||
// GetInsideOut, IsKindOf and IsNurbSurfaceTorus; and the inherited SmSurfOfRevolution::TrimWithDomain,
|
||||
// IntersectWithPlane and GlobalSurfaceIntersect (with a plane).
|
||||
//
|
||||
// One entry point, sc_surface_torus_eval(op, in, n_in, out, cap, out_len). Layouts are
|
||||
@@ -60,6 +60,7 @@ enum : int32_t
|
||||
OP_TRIM_DOMAIN = 10,
|
||||
OP_ISECT_PLANE = 11,
|
||||
OP_GLOBAL = 12,
|
||||
OP_IS_NURB = 13,
|
||||
OP_CONST_MAKE = 20, // the constructor with no MakeNurb, then sc::put_const_make
|
||||
};
|
||||
|
||||
@@ -409,6 +410,48 @@ void torus_op(int32_t op, Rd& rd, const SmContext& ctx, SmTorus* s, std::vector<
|
||||
delete off;
|
||||
break;
|
||||
}
|
||||
case OP_IS_NURB:
|
||||
{
|
||||
double scale = rd.num();
|
||||
double sx = rd.num();
|
||||
double sy = rd.num();
|
||||
double sz = rd.num();
|
||||
int64_t npert = rd.idx();
|
||||
const gw_SURFACE* src = s->GetGwNurbPointer();
|
||||
gw_SURFACE* g = sm_AllocateAndCopyNurbSurface((gw_SURFACE*)src);
|
||||
for (gw_INDEX i = 0; i <= g->net->n; i++)
|
||||
for (gw_INDEX j = 0; j <= g->net->m; j++)
|
||||
{
|
||||
g->net->Pw[i][j].x *= sx;
|
||||
g->net->Pw[i][j].y *= sy;
|
||||
g->net->Pw[i][j].z *= sz;
|
||||
}
|
||||
for (int64_t k = 0; k < npert; k++)
|
||||
{
|
||||
int64_t i = rd.idx();
|
||||
int64_t j = rd.idx();
|
||||
double dx = rd.num();
|
||||
double dy = rd.num();
|
||||
double dz = rd.num();
|
||||
if (i < 0 || j < 0 || i > g->net->n || j > g->net->m)
|
||||
continue;
|
||||
g->net->Pw[i][j].x += dx;
|
||||
g->net->Pw[i][j].y += dy;
|
||||
g->net->Pw[i][j].z += dz;
|
||||
}
|
||||
SmBSplineSurface* test = new (ctx) SmBSplineSurface(g, FALSE, NULL);
|
||||
SmTorus* r = NULL;
|
||||
// SmBoolean is an int: a failed `SER(..)` returns its status as a truthy "found" with
|
||||
// no torus, so the value is recorded as is.
|
||||
SmBoolean found = SmTorus::IsNurbSurfaceTorus(ctx, test, r, scale);
|
||||
o.push_back((double)found);
|
||||
o.push_back(flg(r != NULL));
|
||||
if (r)
|
||||
put_state(o, r);
|
||||
delete r;
|
||||
delete test;
|
||||
break;
|
||||
}
|
||||
case OP_REBUILD:
|
||||
{
|
||||
SmStatus stt = s->RebuildSTEPFromNURBParameters();
|
||||
|
||||
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test:
|
||||
status = "green"
|
||||
claimed_by = ""
|
||||
claimed_at = ""
|
||||
notes = "main CI e33985411 (2026-10-23): PASS surface.cone_p1 cases=20702 passed=20702 failed=0 deferred=0. GREEN (PR #226 / issue #207): 20702/20702 Linux differential passes, bit-identical, no deferrals or absolute floors. GetHeight now retains initialized point sentinels when native evaluation statuses are ignored; part-1 state adapters compare this height without requiring a polar curve. Eight new build fixtures cover tiny axes and negative radii with every swap/inside-out combination. Earlier evidence: GREEN: after surface.surf_of_revolution_p2 merged (PR #51): 20694 cases, 20694 pass bit-identical, 0 deferred (the AdjustSTEPUVDomain / STEPInversion back-edges into surface.surf_of_revolution_p2 are wired; Cone::rev_step_inversion calls SurfOfRevolution::step_inversion_with_outputs). Before: PARTIAL: after surface.surf_of_revolution_p3 merged (PR #29): 20694 cases, 20238 pass, 456 deferred (surface.surf_of_revolution_p2 456); Transform (now takes &Kernel) and ReadFromDB wired; Surface::read_from_db cannot hold a cone yet (DISPATCH GAP, no SurfaceKind variant). Before: PARTIAL: difftest 20694 cases (252 fixture: TestSmConeSTEPCrossDerivative, TestSmGeneral cylinder/cone, smFilletTest/smTopoTest cylinders incl. every swap/inside-out, my_test_analytic_surfaces cones and cylinder, edge cases; + 3167 review adversarial fixtures (review.json: cone/cylinder equality across classes, on-surface inversions with/without guesses or oob, iso curves with reversed and zero trim domains, derivative orders 0-8, identity and near-identity transforms, extra coincidence configurations); + 3875 second-review adversarial fixtures (review2.json: semi-angles -30/89/89.9/90/120 deg, bounded cones with a zero radius incl. inside-out and swap, make_nurb_gen generators, near-cylinders, negative and 1e-6 heights, far/tiny/rotated/skewed/non-unit placements, seam/apex/past-apex inversions, iso circles 1e-7 off the surface, derivative orders to 4, reversed/partial/ellipse coincidence pairs); + 4100 third-review adversarial fixtures (review3.json: every derivative order pair incl. out of range, NURB eval at knot boundaries with all side/upper/non-zero flags, to_nurbs at u=720/-725/1e6, iso with reversed/zero-size/over-range trims, inversion and iso_circle with tol 0..10*scale and seam guesses, adjust at radius-boundary heights, near-identity and negative-scale transforms, cross-mode equality, partial/reversed circle coincidence at 1e-5..1e5 radii, near-coincident lines; std/rotated/far placements with and without oob); + 7800 fourth-review adversarial fixtures (review4.json: tiny/negative semi-angles with every swap/inside-out, zero/over-long/negative sweeps, zero heights, apex-crossing cones, negative and SM_USE_DEFAULT coincidence tolerances, sub-domain and closed-vs-arc circle and line coincidences, iso circles on 5e-11 deg cones); + 1500 generated incl. degenerate placements/heights/semi-angles/sweeps, 1e-6..1e5 scales), 20173 pass bit-identical (max_rel_err 0), 0 failed, 521 deferred (surface.surf_of_revolution_p2 456: AdjustSTEPUVDomain within the apex; surface.surf_of_revolution_p3 65: non-identity Transform); perturbation 20173/20173 detected; 20k-seed run clean. SmSurfOfRevolution MakeNurb/RebuildPolarConverter take the virtual iso-curve hook (cone override). sm_TotalCoincidenceChecker compared through a verbatim shim copy. SmSurface::ReadFromDB now dispatches cone/cylinder to Cone::read_from_db (then defers on surface.surf_of_revolution_p3). State compares GetTypeString (SmCone_TYPE/SmCylinder_TYPE) and GetClassString (SmCone/SmCylinder). Upstream issues UB-88, UB-87, HANG-11, WR-63, WR-64. 50 fns; largest: SmCone::CreateIsoParametricCurve, SmCone::CreateIsoCircleFromPoint, sm_TotalCoincidenceChecker, SmCone::ConvertUVFromSTEPToNURBS; cycle back-edges (call into later units; stub or co-port): surface.surf_of_revolution_p2(4), surface.surf_of_revolution_p3(2); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit"
|
||||
notes = "#1433: SmCylinder::IsNurbSurfaceCylinder is ported in surface::cone::p2 (Cone::is_nurb_surface_cylinder) and compared by the surface.cone_p2 runner. main CI e33985411 (2026-10-23): PASS surface.cone_p1 cases=20702 passed=20702 failed=0 deferred=0. GREEN (PR #226 / issue #207): 20702/20702 Linux differential passes, bit-identical, no deferrals or absolute floors. GetHeight now retains initialized point sentinels when native evaluation statuses are ignored; part-1 state adapters compare this height without requiring a polar curve. Eight new build fixtures cover tiny axes and negative radii with every swap/inside-out combination. Earlier evidence: GREEN: after surface.surf_of_revolution_p2 merged (PR #51): 20694 cases, 20694 pass bit-identical, 0 deferred (the AdjustSTEPUVDomain / STEPInversion back-edges into surface.surf_of_revolution_p2 are wired; Cone::rev_step_inversion calls SurfOfRevolution::step_inversion_with_outputs). Before: PARTIAL: after surface.surf_of_revolution_p3 merged (PR #29): 20694 cases, 20238 pass, 456 deferred (surface.surf_of_revolution_p2 456); Transform (now takes &Kernel) and ReadFromDB wired; Surface::read_from_db cannot hold a cone yet (DISPATCH GAP, no SurfaceKind variant). Before: PARTIAL: difftest 20694 cases (252 fixture: TestSmConeSTEPCrossDerivative, TestSmGeneral cylinder/cone, smFilletTest/smTopoTest cylinders incl. every swap/inside-out, my_test_analytic_surfaces cones and cylinder, edge cases; + 3167 review adversarial fixtures (review.json: cone/cylinder equality across classes, on-surface inversions with/without guesses or oob, iso curves with reversed and zero trim domains, derivative orders 0-8, identity and near-identity transforms, extra coincidence configurations); + 3875 second-review adversarial fixtures (review2.json: semi-angles -30/89/89.9/90/120 deg, bounded cones with a zero radius incl. inside-out and swap, make_nurb_gen generators, near-cylinders, negative and 1e-6 heights, far/tiny/rotated/skewed/non-unit placements, seam/apex/past-apex inversions, iso circles 1e-7 off the surface, derivative orders to 4, reversed/partial/ellipse coincidence pairs); + 4100 third-review adversarial fixtures (review3.json: every derivative order pair incl. out of range, NURB eval at knot boundaries with all side/upper/non-zero flags, to_nurbs at u=720/-725/1e6, iso with reversed/zero-size/over-range trims, inversion and iso_circle with tol 0..10*scale and seam guesses, adjust at radius-boundary heights, near-identity and negative-scale transforms, cross-mode equality, partial/reversed circle coincidence at 1e-5..1e5 radii, near-coincident lines; std/rotated/far placements with and without oob); + 7800 fourth-review adversarial fixtures (review4.json: tiny/negative semi-angles with every swap/inside-out, zero/over-long/negative sweeps, zero heights, apex-crossing cones, negative and SM_USE_DEFAULT coincidence tolerances, sub-domain and closed-vs-arc circle and line coincidences, iso circles on 5e-11 deg cones); + 1500 generated incl. degenerate placements/heights/semi-angles/sweeps, 1e-6..1e5 scales), 20173 pass bit-identical (max_rel_err 0), 0 failed, 521 deferred (surface.surf_of_revolution_p2 456: AdjustSTEPUVDomain within the apex; surface.surf_of_revolution_p3 65: non-identity Transform); perturbation 20173/20173 detected; 20k-seed run clean. SmSurfOfRevolution MakeNurb/RebuildPolarConverter take the virtual iso-curve hook (cone override). sm_TotalCoincidenceChecker compared through a verbatim shim copy. SmSurface::ReadFromDB now dispatches cone/cylinder to Cone::read_from_db (then defers on surface.surf_of_revolution_p3). State compares GetTypeString (SmCone_TYPE/SmCylinder_TYPE) and GetClassString (SmCone/SmCylinder). Upstream issues UB-88, UB-87, HANG-11, WR-63, WR-64. 50 fns; largest: SmCone::CreateIsoParametricCurve, SmCone::CreateIsoCircleFromPoint, sm_TotalCoincidenceChecker, SmCone::ConvertUVFromSTEPToNURBS; cycle back-edges (call into later units; stub or co-port): surface.surf_of_revolution_p2(4), surface.surf_of_revolution_p3(2); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit"
|
||||
|
||||
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin
|
||||
status = "green"
|
||||
claimed_by = ""
|
||||
claimed_at = ""
|
||||
notes = "#724: completes the existing opt-in Kernel::fixes.cylinder_rebuild_mean_radius (WR-385) by clearing stale angles; #713 was closed by #1077. Official 1.0.1 (3fd5b95): 1596/1596 pass, max_rel_err=0, 0 failed/deferred, 1596/1596 perturbations detected; all 1572 exact-base case records retained, 24 added. Rebuild source is identical at the old and current official pins; no additional rebuild port required. See plan/findings/surface.cone_p2--cylinder-nurb-rebuild-issue713--addendum-2026-10-27-v101.md. main CI e33985411 (2026-10-23): PASS surface.cone_p2 cases=1570 passed=1570 failed=0 deferred=0. GREEN (PR #153): difftest 1550 cases (42 fixture + 308 review adversarial fixture + 1200 generated), 1550 pass bit-identical, 0 failed, 0 deferred, max_rel_err 0, no absolute floors; --perturb SENSITIVE 1550/1550. Real SmCone/SmCylinder inputs (all surface.cone_p1 constructions), offsets on all three branches (no surface, apex truncation, moved copy incl. apex control points), recognition and rebuild after NURB edits (moves, twists, homogeneous rescale, flattening), trims of section circles/ellipses (sub-domains, arcs, off-cone). Back-edges wired: sm_GetEllipseTrimIntervals in surface::cone::p3 (surface.cone_p3 deferred 121 -> 30), SmCone::IsNurbSurfaceCone in BSplineSurface::copy_and_add_analytics (now takes the kernel; a recognised cone returns Unported at a DISPATCH GAP (no SurfaceKind::Cone yet, issue #76 / PR #133), the rest defers on surface.sphere). The prior closed attempt (PR #71) was not reused. 4 fns; largest: SmCone::CreateOffsetSurface, SmCone::IsNurbSurfaceCone, sm_GetEllipseTrimIntervals, SmCone::RebuildSTEPFromNURBParameters; cycle back-edges (call into later units; stub or co-port): surface.surface_p1(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit"
|
||||
notes = "#1433: SmCylinder::IsNurbSurfaceCylinder (SmCylinder.cpp, surface.cone_p1's file; ported in surface::cone::p2 as Cone::is_nurb_surface_cylinder next to IsNurbSurfaceCone, whose iso-arc helper it shares) with 77 fixture cases (fixtures/surface.cone_p2/issue1433_cylinder.json: 59 find a cylinder, 18 non-matching), all pass at max_rel_err=0. #724: completes the existing opt-in Kernel::fixes.cylinder_rebuild_mean_radius (WR-385) by clearing stale angles; #713 was closed by #1077. Official 1.0.1 (3fd5b95): 1596/1596 pass, max_rel_err=0, 0 failed/deferred, 1596/1596 perturbations detected; all 1572 exact-base case records retained, 24 added. Rebuild source is identical at the old and current official pins; no additional rebuild port required. See plan/findings/surface.cone_p2--cylinder-nurb-rebuild-issue713--addendum-2026-10-27-v101.md. main CI e33985411 (2026-10-23): PASS surface.cone_p2 cases=1570 passed=1570 failed=0 deferred=0. GREEN (PR #153): difftest 1550 cases (42 fixture + 308 review adversarial fixture + 1200 generated), 1550 pass bit-identical, 0 failed, 0 deferred, max_rel_err 0, no absolute floors; --perturb SENSITIVE 1550/1550. Real SmCone/SmCylinder inputs (all surface.cone_p1 constructions), offsets on all three branches (no surface, apex truncation, moved copy incl. apex control points), recognition and rebuild after NURB edits (moves, twists, homogeneous rescale, flattening), trims of section circles/ellipses (sub-domains, arcs, off-cone). Back-edges wired: sm_GetEllipseTrimIntervals in surface::cone::p3 (surface.cone_p3 deferred 121 -> 30), SmCone::IsNurbSurfaceCone in BSplineSurface::copy_and_add_analytics (now takes the kernel; a recognised cone returns Unported at a DISPATCH GAP (no SurfaceKind::Cone yet, issue #76 / PR #133), the rest defers on surface.sphere). The prior closed attempt (PR #71) was not reused. 4 fns; largest: SmCone::CreateOffsetSurface, SmCone::IsNurbSurfaceCone, sm_GetEllipseTrimIntervals, SmCone::RebuildSTEPFromNURBParameters; cycle back-edges (call into later units; stub or co-port): surface.surface_p1(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit"
|
||||
|
||||
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test:
|
||||
status = "green"
|
||||
claimed_by = ""
|
||||
claimed_at = ""
|
||||
notes = "main CI e33985411 (2026-10-23): PASS surface.sphere cases=816 passed=816 failed=0 deferred=0. GREEN (PR #100): difftest 800 generated cases, 800 pass bit-identical, 0 deferred, --perturb 800/800 (every op: CreateCanonical and the constructor with MakeNurb incl. swap/inside-out/partial-domain/explicit-generator, operator==, Copy, CreateOffsetSurface (+rebuild), CreateIsoParametricCurve, ConvertUVFromSTEPToNURBS/NURBSToSTEP, EvaluateSTEP(Point), EvaluateNormal, RebuildSTEPFromNURBParameters, Transform, IsClosed/U/V, STEPInversion, CreateTrimmedCircleSegments, IntersectWithPlane/Sphere/Cone, GlobalSurfaceIntersect with a plane, IsNurbSurfaceSphere). DISPATCH GAP: the sphere is not a SurfaceKind variant (as the torus): SmSphere::GlobalSurfaceIntersect falls back to SmSurface::GlobalSurfaceIntersect with a sphere `this` as Unported (names surface.sphere), Surface::read_from_db reads the record with Sphere::read_from_db and then reports the gap, and the ssi engine defers SurfaceSpec::Sphere on surface.surface_p2. Sphere implements Revolved so SurfOfRevolution::intersect_with_sphere takes it. No UNPORTED(surface.sphere) stubs remain. Not ported (not in the unit's function list): GetCanonical, GlobalPointSolveSTEP, RotationAboutAxisX/Y/Z, AssertValid, WriteToDB, Dump. 34 fns; largest: SmSphere::CreateTrimmedCircleSegments, SmSphere::IntersectWithSphere, SmSphere::IsNurbSurfaceSphere, SmSphere::IntersectWithCone; unit-SCC #22 (220 units); 319 upstream test entry points reach this unit"
|
||||
notes = "#1433: opt-in Kernel::fixes.found_torus_sphere_keep_tested_nurb (WR-493, smlib@7ea6a96) in is_nurb_surface_sphere (half-meridian tol 360*SM_EFF_ZERO, keeps the tested NURB); default unchanged. main CI e33985411 (2026-10-23): PASS surface.sphere cases=816 passed=816 failed=0 deferred=0. GREEN (PR #100): difftest 800 generated cases, 800 pass bit-identical, 0 deferred, --perturb 800/800 (every op: CreateCanonical and the constructor with MakeNurb incl. swap/inside-out/partial-domain/explicit-generator, operator==, Copy, CreateOffsetSurface (+rebuild), CreateIsoParametricCurve, ConvertUVFromSTEPToNURBS/NURBSToSTEP, EvaluateSTEP(Point), EvaluateNormal, RebuildSTEPFromNURBParameters, Transform, IsClosed/U/V, STEPInversion, CreateTrimmedCircleSegments, IntersectWithPlane/Sphere/Cone, GlobalSurfaceIntersect with a plane, IsNurbSurfaceSphere). DISPATCH GAP: the sphere is not a SurfaceKind variant (as the torus): SmSphere::GlobalSurfaceIntersect falls back to SmSurface::GlobalSurfaceIntersect with a sphere `this` as Unported (names surface.sphere), Surface::read_from_db reads the record with Sphere::read_from_db and then reports the gap, and the ssi engine defers SurfaceSpec::Sphere on surface.surface_p2. Sphere implements Revolved so SurfOfRevolution::intersect_with_sphere takes it. No UNPORTED(surface.sphere) stubs remain. Not ported (not in the unit's function list): GetCanonical, GlobalPointSolveSTEP, RotationAboutAxisX/Y/Z, AssertValid, WriteToDB, Dump. 34 fns; largest: SmSphere::CreateTrimmedCircleSegments, SmSphere::IntersectWithSphere, SmSphere::IsNurbSurfaceSphere, SmSphere::IntersectWithCone; unit-SCC #22 (220 units); 319 upstream test entry points reach this unit"
|
||||
|
||||
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test:
|
||||
status = "green"
|
||||
claimed_by = ""
|
||||
claimed_at = ""
|
||||
notes = "main CI e33985411 (2026-10-23): PASS surface.torus cases=596 passed=596 failed=0 deferred=0. GREEN (merge of PR #29): 563 cases, 563 pass bit-identical, 0 deferred (SmSurfOfRevolution::Transform / ::ReadFromDB wired to surface.surf_of_revolution_p3; Surface::read_from_db still cannot hold a torus: DISPATCH GAP, no SurfaceKind variant). Was PARTIAL (PR #23): difftest 563 cases (63 fixture: TestSmGeneral torus STEP derivatives, smSurfaceTest analytic torus, smTopoTest torus, SmApiCreateTorus radii, swap/inside-out/partial-domain/explicit-generator edges; + 500 generated) over CreateCanonical, EvaluateSTEP(Point), ConvertUVFromSTEPToNURBS/NURBSToSTEP, CreateIsoParametricCurve, CreateOffsetSurface (+rebuild), RebuildSTEPFromNURBParameters, Transform, operator==, Copy; 545 pass bit-identical, 18 deferred (surface.surf_of_revolution_p3: SmSurfOfRevolution::Transform). ReadFromDB is ported up to SmSurfOfRevolution::ReadFromDB (unported; unit test only). SurfOfRevolution::make_nurb_with_sweep_params_using / rebuild_polar_converter take the iso-curve maker so the torus override is used (upstream virtual CreateIsoParametricCurve). 21 fns; cycle back-edges: surface.surf_of_revolution_p3(2), surface.surf_of_revolution_p2(1)"
|
||||
notes = "#1433: SmTorus::IsNurbSurfaceTorus ported (Torus::is_nurb_surface_torus) with 126 fixture cases (fixtures/surface.torus/issue1433_is_nurb.json: 103 find a torus, 23 non-matching), all pass; opt-in fixes found_torus_sphere_keep_tested_nurb (WR-493) and torus_signed_gen_curve_interval (WR-408) unit-tested. main CI e33985411 (2026-10-23): PASS surface.torus cases=596 passed=596 failed=0 deferred=0. GREEN (merge of PR #29): 563 cases, 563 pass bit-identical, 0 deferred (SmSurfOfRevolution::Transform / ::ReadFromDB wired to surface.surf_of_revolution_p3; Surface::read_from_db still cannot hold a torus: DISPATCH GAP, no SurfaceKind variant). Was PARTIAL (PR #23): difftest 563 cases (63 fixture: TestSmGeneral torus STEP derivatives, smSurfaceTest analytic torus, smTopoTest torus, SmApiCreateTorus radii, swap/inside-out/partial-domain/explicit-generator edges; + 500 generated) over CreateCanonical, EvaluateSTEP(Point), ConvertUVFromSTEPToNURBS/NURBSToSTEP, CreateIsoParametricCurve, CreateOffsetSurface (+rebuild), RebuildSTEPFromNURBParameters, Transform, operator==, Copy; 545 pass bit-identical, 18 deferred (surface.surf_of_revolution_p3: SmSurfOfRevolution::Transform). ReadFromDB is ported up to SmSurfOfRevolution::ReadFromDB (unported; unit test only). SurfOfRevolution::make_nurb_with_sweep_params_using / rebuild_polar_converter take the iso-curve maker so the torus override is used (upstream virtual CreateIsoParametricCurve). 21 fns; cycle back-edges: surface.surf_of_revolution_p3(2), surface.surf_of_revolution_p2(1)"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Updates: WR-408
|
||||
|
||||
`SmTorus::IsNurbSurfaceTorus` is now ported (`Torus::is_nurb_surface_torus`,
|
||||
`crates/solidcore/src/surface/torus.rs`, tangency-labs/solidcore#1433). Its generator
|
||||
interval has the same unsigned `AngleBetween` defect; the existing opt-in
|
||||
`Kernel::fixes.torus_signed_gen_curve_interval` now covers it too, through the same
|
||||
`torus_gen_curve_interval`, with the fork's (tangency-labs/smlib@d9f20b0) zero tolerance
|
||||
`SM_EFF_ZERO` there (`SM_EFF_ZERO_DEG` in the rebuild), and a failed signed angle makes
|
||||
the test answer "not a torus", as the fork. No new flag. Default off: the `surface.torus`
|
||||
`issue1433_torus_*` fixtures (including intervals below the equator) compare upstream's
|
||||
unsigned angles. Test with the flag on: `torus_recognition_partial_scaled_and_non_matching`.
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#### WR-493. A torus or sphere found in a NURBS does not keep that NURBS; a half meridian measured to its rounding is refused
|
||||
- **Severity:** Wrong result
|
||||
- **Area:** Surfaces (tori and spheres)
|
||||
- **Location:** `source/SMLib/src/SmTorus.cpp`, `SmTorus::IsNurbSurfaceTorus` (after `Reparameterize`); `source/SMLib/src/SmSphere.cpp`, `SmSphere::IsNurbSurfaceSphere` (the `sAnglesU/V.GetLength() <= 180.0 + SM_EFF_ZERO` orientation tests, and after `Reparameterize`).
|
||||
- **What happens:** both tests build the analytic surface from the circles fitted to the tested NURBS and keep that surface's own NURB, whose weights and control points differ from the tested ones by ~1e-11 (a fillet torus R 2.85, r 0.2, 90 mm out). A face trimmed in the tested NURBS's parameters then sits on a slightly different surface, and an exact same-NURBS check (the C API's flat import, `FACE_NURBS_FORM`) refuses it. `SmCylinder::IsNurbSurfaceCylinder` already keeps a copy of the tested NURB. The sphere test also measures a pole-to-pole meridian in degrees (180.0000000000024 on a radius 2 lune 500 mm out) against `180 + SM_EFF_ZERO` and takes it for more than a half circle; `RebuildSTEPFromNURBParameters` already allows `360 * SM_EFF_ZERO` there ([B279]).
|
||||
- **Trigger:** tangency-labs/solidcore#1433: the NURBS form of a torus or sphere face given to the recognisers (e.g. `Torus::create_canonical` R 10, r 3, then `IsNurbSurfaceTorus` on its NURB).
|
||||
- **How found:** SolidCraft flat round-trip import (the fork's capi `flat_roundtrip` tests).
|
||||
- **Confirmed:** on the fork's C++ build (tangency-labs/smlib@7ea6a96). The pinned C++ keeps the fitted NURB, which the `surface.torus` `issue1433_torus_*` difftest fixtures compare with the fix off.
|
||||
- **Fix (fork):** after `Reparameterize`, `SetFromGwNurb(0, pTestSurface->GetGwNurbPointer())` (a copy of the tested NURB, then `RebuildSTEPFromNURBParameters`, status ignored); the sphere's half-meridian tolerance is `360 * SM_EFF_ZERO`.
|
||||
- **Rust port:** opt-in `Kernel::fixes.found_torus_sphere_keep_tested_nurb` (default `false`, upstream's behaviour) in `Torus::is_nurb_surface_torus` (`crates/solidcore/src/surface/torus.rs`) and `Sphere::is_nurb_surface_sphere` (`crates/solidcore/src/surface/sphere.rs`), both parts under the one flag. Tests: `issue_1433_torus_found_in_its_nurb_keeps_it`, `torus_recognition_partial_scaled_and_non_matching`, `wr493_sphere_found_in_its_nurb_keeps_it`.
|
||||
Reference in New Issue
Block a user