Merge pull request #1578 from tangency-labs/port/assert-valid-level2-green

topology.assert_valid + topology.vertex green: #1478 (WP4a, @brett19) merged with main, plus extrusion AssertValid and fixes
This commit is contained in:
bherbruck
2026-10-29 17:05:24 -04:00
committed by GitHub
36 changed files with 39295 additions and 127 deletions
@@ -1,6 +1,6 @@
//! Runner for unit `surface.surface_assert_valid`: the virtual `SmSurface::AssertValid`
//! on real B-spline surfaces, planes, cylinders, cones, spheres, tori and surfaces of
//! revolution.
//! on real B-spline surfaces, planes, cylinders, cones, spheres, tori, surfaces of
//! revolution and surfaces of extrusion.
//!
//! C++: `solidcore_ffi::units::surface_surface_assert_valid` (shim
//! `harness/cpp/shim/surface_surface_assert_valid.cpp`, which documents the block).
@@ -14,7 +14,12 @@
//! and nearly collapsed boundaries, folded (non-monotonic) nets, flat corners,
//! coincident boundaries, degenerate surfaces, rational weights, negative and zero
//! scales and radii, apexes inside cone domains, spindle tori, mismatched and
//! degenerate generators, at huge and tiny scales.
//! degenerate generators, and extrusions of lines, planar curves (the extrusion along,
//! across and at an angle to their plane) and twisted ones with zero and degenerate
//! extrusion vectors and domains, origins off the generator, generators with a tangent
//! along the extrusion and stale cached planes and lines, at huge and tiny scales. The
//! named extrusion fixtures (`fixtures/surface.surface_assert_valid/extrusion.json`) cover
//! every generator orientation with and without a tangent along the extrusion.
// The generator indexes the nets and knot vectors it has just built (as other runners do).
#![allow(clippy::indexing_slicing)]
@@ -36,6 +41,7 @@ use solidcore::nurbs::srf_basic::Surface as NlSurface;
use solidcore::surface::cone::Cone;
use solidcore::surface::plane::Plane;
use solidcore::surface::sphere::{Sphere, SphereOptions};
use solidcore::surface::surf_of_extrusion::SurfOfExtrusion;
use solidcore::surface::surf_of_revolution::SurfOfRevolution;
use solidcore::surface::surface::{Surface, SurfaceKind};
use solidcore::surface::torus::{Torus, TorusOptions};
@@ -48,6 +54,9 @@ use crate::{Case, HarnessError, Output, Rng, UnitDiff};
/// Generated cases per run, whatever `--seeds` says (it can only raise this).
pub const MIN_GENERATED: usize = 1500;
/// Generated extrusion cases per run (`extNNNN`, their own seed stream).
pub const EXTRUSION_CASES: usize = 400;
/// `surface.surface_assert_valid` runner.
pub struct Runner;
@@ -112,7 +121,35 @@ impl Rd<'_> {
}
}
/// A B-spline generator: `p n rational pts[n*4] knots[n+p+1]`.
fn generator(rd: &mut Rd<'_>) -> Result<Curve, HarnessError> {
let (p, n) = (rd.int()?, rd.int()?);
let rational = rd.flag()?;
let pts = (0..n).map(|_| rd.point(rational)).collect::<Result<Vec<_>, _>>()?;
let knots = (0..=n + p).map(|_| rd.num()).collect::<Result<Vec<_>, _>>()?;
Ok(Curve::from(BSplineCurve::from_nurb(
NlCurve::new(pts, p, knots).map_err(herr)?,
3,
false,
)))
}
fn build(k: &Kernel, rd: &mut Rd<'_>, kind: usize) -> Result<Surface, HarnessError> {
if kind == 7 {
let origin = rd.vec()?;
let vec = rd.vec()?;
let d = rd.dom()?;
let swap = rd.flag()?;
let gen = generator(rd)?;
let stale = rd.flag()?;
let (cp, cv) = (rd.vec()?, rd.vec()?);
let mut e = SurfOfExtrusion::new(k, gen, origin, vec, d, swap).map_err(herr)?;
if stale {
e.set_curve_point(cp);
e.set_curve_vec(cv);
}
return Ok(Surface::from(e));
}
if kind == 0 {
let (p, q, nu, nv) = (rd.int()?, rd.int()?, rd.int()?, rd.int()?);
let rational = rd.flag()?;
@@ -173,15 +210,7 @@ fn build(k: &Kernel, rd: &mut Rd<'_>, kind: usize) -> Result<Surface, HarnessErr
_ => {
let d = rd.dom()?;
let swap = rd.flag()?;
let (p, n) = (rd.int()?, rd.int()?);
let rational = rd.flag()?;
let pts = (0..n).map(|_| rd.point(rational)).collect::<Result<Vec<_>, _>>()?;
let knots = (0..=n + p).map(|_| rd.num()).collect::<Result<Vec<_>, _>>()?;
let gen = Curve::from(BSplineCurve::from_nurb(
NlCurve::new(pts, p, knots).map_err(herr)?,
3,
false,
));
let gen = generator(rd)?;
Surface::from(SurfOfRevolution::new(k, gen, o, x, y, d, swap).map_err(herr)?)
}
})
@@ -211,9 +240,19 @@ fn rust(i: &Inputs) -> Result<Output, HarnessError> {
let kind = rd.int()?;
let level = rd.int()?;
let make = rd.flag()?;
let surface = build(&k, &mut rd, kind)?;
let mut surface = build(&k, &mut rd, kind)?;
if make {
// MakeNurb (status ignored)
if let SurfaceKind::SurfOfExtrusion(extrusion) = surface.kind_mut() {
// the shim's MakeNurb on the object itself: it trims the generator to the
// U interval of the analytic domain (AdjustSTEPInterval), which the lazy,
// const NURB of kind_with_nurb leaves on a copy
if let Err(e) = extrusion.make_nurb(&k) {
if let Some(o) = Output::unported(&e) {
return Ok(o);
}
}
}
match surface.kind() {
SurfaceKind::Plane(p) => {
let _ = p.base_with_nurb();
@@ -477,6 +516,99 @@ fn angles(rng: &mut Rng) -> (f64, f64) {
}
}
/// The generator of an extrusion case, its control points (`x y z w`, homogeneous when
/// rational) and its clamped knots, by shape: 0 a line, 1 a planar arc in z = 0,
/// 2 a planar arc in x = 0 whose start tangent is along z, 3 a twisted (non-planar)
/// curve, 4 a non-planar curve whose interior control points are lifted, 5 a nearly
/// straight curve.
fn extrusion_generator(rng: &mut Rng, shape: u64, s: f64) -> (usize, Vec<[f64; 4]>, Vec<f64>) {
let p = if shape == 0 { 1 + rng.below(2) as usize } else { 2 + rng.below(2) as usize };
let n = p + 1 + rng.below(3) as usize;
let knots = clamped(rng, n, p);
let rational = shape != 0 && rng.chance(0.2);
let pts = (0..n)
.map(|i| {
let t = i as f64 / (n - 1) as f64;
let a = t * 2.5;
let c = match shape {
0 => [t, 0.5 * t, -0.25 * t],
1 => [a.cos(), a.sin(), 0.0],
2 => [0.0, a.cos(), a.sin()],
3 => [a.cos(), a.sin(), 0.4 * t * t],
4 => [t, t * t, if i == 0 || i + 1 == n { 0.0 } else { 0.5 }],
_ => [t, 1e-9 * (i % 2) as f64, 0.0],
};
let w = if rational { rng.uniform(0.5, 2.0) } else { 1.0 };
[c[0] * s * w, c[1] * s * w, c[2] * s * w, w]
})
.collect();
(p, pts, knots)
}
/// An extrusion case after the `kind level make` head.
fn gen_extrusion(rng: &mut Rng, b: &mut Vec<f64>, s: f64) {
let shape = rng.below(6);
let (p, pts, knots) = extrusion_generator(rng, shape, s);
// the generator's start (its first control point: the knots are clamped), or a
// point off it
let mid = pts[pts.len() / 2];
let origin = if rng.chance(0.15) {
[mid[0] / mid[3] + s, mid[1] / mid[3] - s, mid[2] / mid[3] + 0.5 * s]
} else {
let first = pts[0];
[first[0] / first[3], first[1] / first[3], first[2] / first[3]]
};
let dir = match rng.below(8) {
0 => [0.0, 0.0, 0.0],
1 => [0.0, 0.0, 1e-13],
2 => [1.0, 0.0, 0.0],
3 => [0.0, 1.0, 0.0],
4 => [0.3, -0.4, 0.8],
_ => [0.0, 0.0, 1.0],
};
let length = rng.uniform(0.5, 3.0) * s;
let vec = dir.map(|c| c * length);
let (v0, v1) = match rng.below(8) {
0 => (0.5, 0.5),
1 => (-1.0, -1.0 + 1e-14),
_ => {
let v = rng.uniform(-2.0, 2.0);
(v, v + rng.uniform(0.1, 3.0))
}
};
let (u0, u1) = match rng.below(8) {
0 => (0.0, 0.0),
1 => (0.2, 0.8),
_ => (0.0, 1.0),
};
b.extend(origin);
b.extend(vec);
b.extend([u0, v0, u1, v1, flag(rng, 0.3)]);
let rational = pts.iter().any(|q| q[3] != 1.0);
b.extend([p as f64, pts.len() as f64, f64::from(u8::from(rational))]);
for q in &pts {
b.extend(q);
}
b.extend(knots);
// a stale cached plane or line point and vector
if rng.chance(0.15) {
b.push(1.0);
b.extend([rng.uniform(-2.0, 2.0) * s, rng.uniform(-2.0, 2.0) * s, s]);
b.extend([rng.uniform(-1.0, 1.0), rng.uniform(-1.0, 1.0), rng.uniform(0.0, 1.0)]);
} else {
b.extend([0.0; 7]);
}
}
/// An extrusion case (kind 7): its own seed stream, so the other generated cases are
/// unchanged.
fn gen_extrusion_case(rng: &mut Rng) -> Inputs {
let mut b = vec![7.0, level(rng), flag(rng, 0.5)];
let s = scale(rng);
gen_extrusion(rng, &mut b, s);
Inputs { block: b }
}
fn gen_case(rng: &mut Rng) -> Inputs {
let kind = rng.below(7) as usize;
let mut b = vec![kind as f64, level(rng)];
@@ -587,11 +719,25 @@ impl UnitDiff for Runner {
}
fn cases(&self, ctx: &CaseContext) -> Result<Vec<Case>, HarnessError> {
let ctx = CaseContext {
let main = CaseContext {
seeds: ctx.seeds.max(MIN_GENERATED),
..ctx.clone()
};
default_cases(self, &ctx)
let mut v = default_cases(self, &main)?;
// extrusions: their own seed stream (the other generated cases are unchanged);
// named `extNNNN`
let ext = CaseContext {
seeds: ctx.seeds.max(EXTRUSION_CASES),
..ctx.clone()
};
for mut c in ext.generated_cases("surface.surface_assert_valid:extrusion", |rng| {
serde_json::to_value(gen_extrusion_case(rng)).ok()
}) {
c.name = c.name.replacen("gen", "ext", 1);
c.unit = self.id().to_string();
v.push(c);
}
Ok(v)
}
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
@@ -51,6 +51,9 @@ struct Inputs {
/// Run the capi sequence instead of `AssertValid` alone.
#[serde(default)]
capi: bool,
/// Run the level-2 tests one by one (shim op 2) instead of `AssertValid`.
#[serde(default)]
tests: bool,
#[serde(default)]
mutation: u8,
#[serde(default)]
@@ -207,6 +210,16 @@ fn own_brep(k: &mut Kernel, src: &Src) -> Result<Result<Brep, Output>, HarnessEr
Built::Brep(b) => {
let mut b = *b;
b.set_naming(NamingScheme::Notify);
// the copy keeps each UV trim curve's owner from the B-rep it was built in;
// upstream's `SmEdgeuse::AssertValid` test 6 expects the edge use holding it
// (`GetOwner() == this`), here under the same identities as the walk's
let uses: Vec<_> = b.edgeuses().ids().collect();
for eu in uses {
let edgeuse = b.edgeuse_mut(eu).map_err(|e| herr(e.to_string()))?;
if let Some(trim) = edgeuse.uv_trim_curve.as_mut() {
let _ = trim.set_owner(Some(key(EntityRef::Edgeuse(eu))), None);
}
}
Ok(b)
}
Built::Failed(e) => Err(Output::unported(&e)
@@ -446,15 +459,86 @@ fn output(protocol: &[f64], capi: bool) -> Result<Output, HarnessError> {
}
Ok(o)
}
/// The level-2 test results (shim op 2): per face tests 32, 33, 38 (`-1` without
/// surface), per edge test 13 and its max gap, per vertex test 1, its max gap and the
/// face of that gap (index, `-1` for none).
fn tests_output(protocol: &[f64]) -> Result<Output, HarnessError> {
let mut rd = Reader {
data: protocol,
pos: 0,
};
if rd.num()? != 0.0 {
return Ok(Output::rejected("native fixture construction failed"));
}
let (nf, ne, nv) = (rd.count()?, rd.count()?, rd.count()?);
let faces: Vec<i64> = rd.slice(3 * nf)?.iter().map(|&x| x as i64).collect();
let edges = rd.slice(2 * ne)?.to_vec();
let vertices = rd.slice(3 * nv)?.to_vec();
if rd.pos != protocol.len() {
return Err(herr("trailing level-2 test output"));
}
let flags = |v: &[f64]| v.iter().step_by(2).map(|&x| x as i64).collect::<Vec<_>>();
let gaps = |v: &[f64]| v.iter().skip(1).step_by(2).copied().collect::<Vec<_>>();
Ok(Output::new()
.ints("face_tests_32_33_38", faces)
.ints("edge_test13", flags(&edges))
.values("edge_test13_max_gap", gaps(&edges))
.ints("vertex_test1", vertices.iter().step_by(3).map(|&x| x as i64).collect::<Vec<_>>())
.values("vertex_test1_max_gap_3d", vertices.iter().skip(1).step_by(3).copied().collect::<Vec<_>>())
.ints("vertex_test1_max_gap_face", vertices.iter().skip(2).step_by(3).map(|&x| x as i64).collect::<Vec<_>>()))
}
/// The Rust side of shim op 2, in its order (faces, then edges, then vertices: the UV
/// trim curves test 38 builds stay for the later tests on both sides).
fn level2_tests(k: &mut Kernel, b: &mut Brep) -> solidcore::Result<Vec<f64>> {
use solidcore::topology::face::assert_tests as fa;
let faces: Vec<FaceId> = b.faces().ids().collect();
let edges: Vec<EdgeId> = b.edges().ids().collect();
let vertices: Vec<VertexId> = b.vertices().ids().collect();
let mut out = vec![0.0, faces.len() as f64, edges.len() as f64, vertices.len() as f64];
let flag = |v: bool| if v { 1.0 } else { 0.0 };
for f in faces {
out.push(flag(fa::assert_test_face32(b, f)?));
out.push(flag(fa::assert_test_face33(k, b, f)?));
out.push(if b.face(f)?.surface.is_some() {
flag(fa::assert_test_face38(k, b, f)?)
} else {
-1.0
});
}
for e in edges {
let (ok, gap) = solidcore::topology::edge::edge_form::assert_test_edge13(k, b, e)?;
out.push(flag(ok));
out.push(gap);
}
for v in vertices {
let (ok, gap, face) = solidcore::topology::vertex::corner::assert_test_vertex1(k, b, v)?;
out.push(flag(ok));
out.push(gap);
out.push(face.map_or(-1.0, |f| f64::from(f.index())));
}
Ok(out)
}
/// Why op 2 refuses mutations 8-10 (null face surface, edge curve, edge use link): the
/// level-2 tests dereference them unguarded in C++.
const NULL_LINK_TESTS: &str = "the level-2 tests dereference the nulled link (crash in C++)";
impl UnitDiff for Runner {
fn id(&self) -> &'static str {
"topology.assert_valid"
}
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
let i: Inputs = case.parse()?;
if i.tests && i.mutation >= 8 {
return Ok(Output::rejected(NULL_LINK_TESTS));
}
let (path, block) = block(&i.brep)?;
let out = ffi::eval(u8::from(i.capi), &path, &block, args(&i))
.map_err(|e| herr(e.to_string()))?;
let op = if i.tests { 2 } else { u8::from(i.capi) };
let out = ffi::eval(op, &path, &block, args(&i)).map_err(|e| herr(e.to_string()))?;
if i.tests {
return tests_output(&out);
}
output(&out, i.capi)
}
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
@@ -465,6 +549,12 @@ impl UnitDiff for Runner {
Err(o) => return Ok(o),
};
mutate(&mut b, &i)?;
if i.tests {
return match level2_tests(&mut k, &mut b) {
Ok(out) => tests_output(&out),
Err(e) => Ok(Output::unported(&e).unwrap_or_else(|| Output::err(e.to_string()))),
};
}
let mut out = vec![0.0];
if i.capi {
let level = match i.level {
@@ -574,6 +664,7 @@ impl UnitDiff for Runner {
},
walk: rng.below(4) != 0,
capi,
tests: false,
mutation,
index: rng.below(100) as usize,
value,
+42 -1
View File
@@ -35,6 +35,7 @@ use solidcore::curve::curve::p2::{
};
use solidcore::curve::curve::Curve as SmCurve;
use solidcore::curve::line::Line;
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::attribute::{Attribute, AttributeStore, Behavior, ObjectKey};
use solidcore::geom::extent1d::Extent1d;
use solidcore::geom::extent2d::Extent2d;
@@ -1159,6 +1160,13 @@ enum OpIn {
a: f64,
b: f64,
},
/// `IsOnFaceEdgeForm(&dMaxGap3d)` (`SmEdge::AssertValid` test 13) after translating the
/// edge curve by `d` (`SmCurve::Transform`; no move for zero).
EdgeForm {
e: i64,
#[serde(default)]
d: [f64; 3],
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -1384,6 +1392,7 @@ fn resolve(op: &OpIn, c: [usize; 10], b: &Brep) -> Option<OpIn> {
n: n.clamp(28, ffi::MAX_ODE as u64),
},
OpIn::SetInterval { e, a, b } => OpIn::SetInterval { e: es(e), a, b },
OpIn::EdgeForm { e, d } => OpIn::EdgeForm { e: es(e), d },
})
}
@@ -1503,6 +1512,7 @@ fn op_args(op: &OpIn) -> Result<(Op, Vec<f64>), HarnessError> {
(Op::Ode, v)
}
OpIn::SetInterval { e, a, b } => (Op::SetInterval, vec![f(*e), *a, *b]),
OpIn::EdgeForm { e, d } => (Op::EdgeForm, vec![f(*e), d[0], d[1], d[2]]),
})
}
@@ -1533,7 +1543,8 @@ fn inexact(op: &OpIn, im: &Imported) -> bool {
| OpIn::Topological { e, .. }
| OpIn::Tangent { e, .. }
| OpIn::Radial { e, .. }
| OpIn::Blend { e } => faces_inexact(*e) || curve_inexact(*e),
| OpIn::Blend { e }
| OpIn::EdgeForm { e, .. } => faces_inexact(*e) || curve_inexact(*e),
_ => false,
}
}
@@ -2355,6 +2366,36 @@ fn run_rust_op(b: &mut Brep, op: &OpIn) -> Result<Output, Fail> {
let ivl = ed::interval(b, eid(*e))?;
out.extend([ivl.min(), ivl.max()]);
}
OpIn::EdgeForm { e, d } => {
let e = eid(*e);
let faces = ed::faces(b, e, None)?;
// the shim's guard: the gap calculation dereferences the curve and surfaces
let mut ok = faces.is_empty() || b.edge(e)?.curve.is_some();
for fc in &faces {
if b.face(*fc)?.surface.is_none() {
ok = false;
}
}
if !ok {
out.push(NULL_DEREF);
return Ok(Output::new().values("state", out));
}
let mut moved = 0.0;
if d.iter().any(|&x| x != 0.0) {
if let Some(curve) = b.edge_mut(e)?.curve.as_mut() {
let mut place = Axis2Placement::default();
place.translate(Vec3::new(d[0], d[1], d[2]));
match curve.transform(&k, &place, None) {
Ok(()) => {}
Err(err) if err.is_unported() => return Err(err.into()),
Err(_) => moved = 1.0,
}
}
}
out.push(moved);
let (ok, gap) = ed::edge_form::is_on_face_edge_form(&k, b, e)?;
out.extend([flag(ok), gap]);
}
}
Ok(Output::new().values("state", out))
}
+94 -3
View File
@@ -46,8 +46,9 @@ use solidcore::nurbs::srf_basic::Surface as NlSurface;
use solidcore::query::poly::PolyVertexId;
use solidcore::surface::surface::p2::DropCurveFail;
use solidcore::surface::surface::Surface;
use solidcore::topology::brep_cache::{arena_identity, CacheObject};
use solidcore::topology::topology::Topology;
use solidcore::topology::vertex::{self as vx, VertexGap};
use solidcore::topology::vertex::{self as vx, corner, VertexGap};
use solidcore::topology::{
Brep, Edge, EdgeId, Edgeuse, EdgeuseId, EdgeuseOwner, Face, FaceId, Faceuse, FaceuseId, Loop,
LoopId, Loopuse, LoopuseId, LoopuseStart, ObjectHandle, Region, RegionId, Shell, ShellId,
@@ -240,6 +241,14 @@ enum OpIn {
exact: bool,
poly: bool,
},
/// `sm_AssertTestVertex1` (as `SmVertex::AssertValid` calls it), then
/// `IsOnFaceCorner`, `HasParallelFaces` and a forced `IsWithinXSectTol3dOfFaces`,
/// after moving the vertex by `d` (`SetPoint`) when given.
Corner {
v: i64,
#[serde(default)]
d: Option<[f64; 3]>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -840,8 +849,14 @@ fn import(ex: &[f64]) -> Result<(Brep, Option<i64>), HarnessError> {
.collect();
b.add_shell_record(s).map_err(|e| harness(e.to_string()))?;
}
for (f, (surface, faceuse, uv_domain, tolerance)) in surfaces.into_iter().enumerate() {
for (f, (mut surface, faceuse, uv_domain, tolerance)) in surfaces.into_iter().enumerate() {
let shell = face_shells.get(f).copied().unwrap_or(ShellId::from_index(0));
// `SmFace::SetSurface` owns the surface (`GetOwner() == face`): its solvers read
// the face tolerance through it (`LocalPointSolve_1`'s loose tolerance, which
// decides a seam jump), under the arena identity of [`CONVENTIONS`]
if let Some(s) = surface.as_mut() {
s.set_owner(Some(arena_identity(CacheObject::Face(FaceId::from_index(f as u32)))));
}
// The rectangular-trim flag is not exported (`SmFace()` default); the UV domain and
// tolerance are (the vertex/face gaps solve against them).
b.add_face_record(Face {
@@ -1349,6 +1364,7 @@ fn resolve(op: &OpIn, c: [usize; 10], b: &Brep) -> Result<OpIn, Box<Output>> {
operation,
},
OpIn::SetPoint { v, p } => OpIn::SetPoint { v: vs(v), p },
OpIn::Corner { v, d } => OpIn::Corner { v: vs(v), d },
OpIn::SetTolerance {
v,
tol,
@@ -1411,6 +1427,13 @@ fn op_args(op: &OpIn) -> (Op, Vec<f64>) {
),
OpIn::RefreshTolerance { v, nested } => (Op::RefreshTolerance, vec![f(*v), flag(*nested)]),
OpIn::Exact { v, exact, poly } => (Op::Exact, vec![f(*v), flag(*exact), flag(*poly)]),
OpIn::Corner { v, d } => {
let m = d.unwrap_or([0.0; 3]);
(
Op::Corner,
vec![f(*v), flag(d.is_some()), m[0], m[1], m[2]],
)
}
}
}
@@ -1502,6 +1525,50 @@ fn put_flag(q: &mut Vec<i64>, r: Result<bool, SolidError>) -> Result<(), SolidEr
Ok(())
}
/// A face as the shim's `g.find(3, face)`.
fn face_index(f: Option<FaceId>) -> f64 {
f.map_or(-1.0, |f| f64::from(f.index()))
}
/// The corner outputs as the shim's `put_corner`.
fn put_corner(out: &mut Vec<f64>, on: bool, c: &corner::FaceCorner) {
out.extend([flag(on), flag(c.found_point), c.max_gap_3d]);
out.extend([c.best_point.x, c.best_point.y, c.best_point.z]);
}
/// The `Corner` op after the move, as the shim's `OP_CORNER`.
fn corner_op(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<Vec<f64>, SolidError> {
let mut out = vec![0.0];
// sm_AssertTestVertex1, spelled out as the shim does
let (within, max_gap, max_face) = corner::is_within_xsect_tol_3d_of_faces(k, b, v, false)?;
let mut passed = within;
out.extend([flag(within), max_gap, face_index(max_face)]);
if within {
let (on, c) = corner::is_on_face_corner(k, b, v)?;
passed &= on;
put_corner(&mut out, on, &c);
}
out.push(flag(passed));
let (on, c) = corner::is_on_face_corner(k, b, v)?;
put_corner(&mut out, on, &c);
if vx::faces(b, v)?.is_empty() {
out.push(-1.0);
} else {
let p = corner::has_parallel_faces(k, b, v, Some(solidcore::geom::tol::ANG_TOL_DEG))?;
out.extend([
flag(p.parallel),
p.min_dihedral_ang_deg,
p.max_parallel_face_gap,
face_index(p.face1),
face_index(p.face2),
]);
}
let (within, gap, face) = corner::is_within_xsect_tol_3d_of_faces(k, b, v, true)?;
out.extend([flag(within), gap, face_index(face)]);
gap_states(b, &mut out);
Ok(out)
}
/// The cached gap states of every vertex use, as the shim's `put_gap_states`.
fn gap_states(b: &Brep, out: &mut Vec<f64>) {
for (_, r) in b.vertexuses().iter() {
@@ -1699,7 +1766,10 @@ fn run_rust(i: &Inputs) -> Result<Output, HarnessError> {
// The gap, topological-vertex and pole queries solve against the face surfaces: a face
// surface the export holds only as its B-spline part (a cone, sphere, torus, revolution
// or extrusion subclass) is a stand-in whose solves differ from the analytic ones.
if let (Some(ty), OpIn::Gaps { .. } | OpIn::Topological { .. } | OpIn::OnPole { .. }) =
if let (
Some(ty),
OpIn::Gaps { .. } | OpIn::Topological { .. } | OpIn::OnPole { .. } | OpIn::Corner { .. },
) =
(inexact, &op)
{
let (unit, symbol) = match ty {
@@ -1915,6 +1985,27 @@ fn run_rust_op(b: &mut Brep, op: &OpIn) -> Result<Output, Fail> {
gap_states(b, &mut out);
out
}
OpIn::Corner { v, d } => {
let v = vid(*v);
if let Some(d) = d {
let mut env = NotifyEnv::new(b);
let p = vx::point(b, v)? + Vec3::new(d[0], d[1], d[2]);
vx::set_point(
b,
v,
p,
&mut env.records,
&mut env.attributes,
None,
env.this,
&vertexuse_ref,
)?;
}
match or_null(corner_op(&k, b, v))? {
Some(out) => out,
None => vec![NULL_DEREF as f64],
}
}
OpIn::SetPoint { v, p } => {
let mut env = NotifyEnv::new(b);
vx::set_point(
@@ -1,7 +1,7 @@
//! Safe wrapper for unit `surface.surface_assert_valid`: the pinned virtual
//! `SmSurface::AssertValid` on a real B-spline surface, plane, cylinder, cone, sphere,
//! torus or surface of revolution built from plain data (block layout in
//! `harness/cpp/shim/surface_surface_assert_valid.cpp`).
//! torus, surface of revolution or surface of extrusion built from plain data (block
//! layout in `harness/cpp/shim/surface_surface_assert_valid.cpp`).
use crate::{call_with_buffer, FfiError};
@@ -102,6 +102,35 @@ fn points(rd: &mut Rd<'_>, n: usize, rational: bool) -> Result<(), FfiError> {
Ok(())
}
/// Knots clamped at both ends (multiplicity `degree + 1`), as the extrusion's generator
/// needs: its constructor evaluates the generator and builds its STEP interval.
fn clamped(k: &[f64], degree: usize) -> Result<(), FfiError> {
let m = mults(k);
if m.first() != Some(&(degree as i64 + 1)) || m.last() != Some(&(degree as i64 + 1)) {
return Err(bad("generator knots not clamped"));
}
Ok(())
}
/// Control points spread over more than a point (the constructor's plane test of a
/// collapsed generator may fail and leave its result unset).
fn spread(block: &[f64], first: usize, n: usize, rational: bool) -> Result<(), FfiError> {
let at = |i: usize, c: usize| block.get(first + 4 * i + c).copied().unwrap_or(0.0);
// (homogeneous coordinates when rational)
let w = |i: usize| if rational { at(i, 3) } else { 1.0 };
let size = (1..n)
.map(|i| {
(0..3)
.map(|c| (at(i, c) / w(i) - at(0, c) / w(0)).abs())
.fold(0.0, f64::max)
})
.fold(0.0, f64::max);
if size.is_nan() || size <= 1e-9 {
return Err(bad("collapsed generator"));
}
Ok(())
}
/// Unique knots' multiplicities.
fn mults(k: &[f64]) -> Vec<i64> {
let mut m: Vec<i64> = Vec::new();
@@ -176,12 +205,12 @@ fn internal_pole_indices(
fn check(block: &[f64]) -> Result<(), FfiError> {
let mut rd = Rd { v: block, at: 0 };
let kind = rd.int(0.0, 6.0)?;
let kind = rd.int(0.0, 7.0)?;
let level = rd.int(0.0, 4.0)?;
if level == 3 {
return Err(bad("level"));
}
rd.flag()?;
let make = rd.flag()?;
if kind == 0 {
let p = rd.int(1.0, 5.0)?;
let q = rd.int(1.0, 5.0)?;
@@ -199,6 +228,34 @@ fn check(block: &[f64]) -> Result<(), FfiError> {
iso_extraction(&ku, p)?;
iso_extraction(&kv, q)?;
points(&mut rd, nu * nv, rational)?;
} else if kind == 7 {
rd.vec()?;
let v = rd.vec()?;
let d = rd.dom()?;
if d[0] > d[2] || d[1] > d[3] {
return Err(bad("extrusion domain not ordered"));
}
// MakeNurb of a collapsed extrusion (zero vector or domain) is not exercised:
// the check of its NURB is the B-spline unit's
if make && (len(v) <= 1e-9 || d[2] - d[0] <= 1e-9 || d[3] - d[1] <= 1e-9) {
return Err(bad("MakeNurb of a collapsed extrusion"));
}
rd.flag()?;
let p = rd.int(1.0, 5.0)?;
let n = rd.int(2.0, 32.0)?;
if n <= p {
return Err(bad("too few generator points"));
}
let rational = rd.flag()?;
let first = rd.at;
points(&mut rd, n, rational)?;
spread(block, first, n, rational)?;
let k: Vec<f64> = (0..=n + p).map(|_| rd.num()).collect::<Result<_, _>>()?;
knots(&k)?;
clamped(&k, p)?;
rd.flag()?;
rd.vec()?;
rd.vec()?;
} else {
rd.vec()?;
let x = rd.vec()?;
@@ -30,13 +30,18 @@ fn primitive_len(kind: f64) -> Option<usize> {
}
}
/// `AssertValid` (op 0) or the capi sequence (op 1) of the B-rep built from `block`
/// (a `sc_edgeuse_graph.h` source or a primitive), after mutation `args[2]` (kinds
/// 1-10 of `topology_brep_validation`). Arguments: level (0-4), walk, mutation kind,
/// index, value.
/// `AssertValid` (op 0), the capi sequence (op 1) or the level-2 tests one by one (op
/// 2: `sm_AssertTestFace32/33/38` per face, `sm_AssertTestEdge13` per edge,
/// `sm_AssertTestVertex1` per vertex) of the B-rep built from `block` (a
/// `sc_edgeuse_graph.h` source or a primitive), after mutation `args[2]` (kinds 1-10 of
/// `topology_brep_validation`). Arguments: level (0-4), walk, mutation kind, index,
/// value. Op 2 refuses the null-pointer mutations 8-10: the tests dereference the face
/// surface, edge curve and edge use links without the `ValidatePointers` guard
/// `AssertValid` runs first.
pub fn eval(op: u8, path: &str, block: &[f64], args: [f64; 5]) -> Result<Vec<f64>, FfiError> {
let bad = || FfiError::InvalidArgument("invalid AssertValid arguments".into());
if op > 1
if op > 2
|| (op == 2 && args[2] >= 8.0)
|| !args.iter().all(|x| x.is_finite())
|| args[0].fract() != 0.0
|| !(0.0..=4.0).contains(&args[0])
@@ -72,6 +72,7 @@
//! | `Blend` | `e` | `eu` |
//! | `Ode` | `e ox oy oz radius t n` | `status dydx*n` |
//! | `SetInterval` | `e a b` | `min max` |
//! | `EdgeForm` | `e dx dy dz` | `moved ok max_gap` (`moved`: 0 = translated or not moved, 1 = `Transform` failed) |
//!
//! [`eval`] accepts only what upstream handles without undefined behaviour: existing
//! `.smb` files, well-formed clamped B-splines, planes with a non-zero normal, indices
@@ -119,6 +120,7 @@ pub enum Op {
Blend = 23,
Ode = 24,
SetInterval = 25,
EdgeForm = 26,
}
impl Op {
@@ -553,6 +555,14 @@ fn brep_args(op: Op, rd: &mut Reader<'_>, c: [usize; 10]) -> Result<(), FfiError
rd.finite()?;
rd.finite()?;
}
Op::EdgeForm => {
for _ in 0..3 {
let d = rd.finite()?;
if d.abs() > MAX_COORD {
return Err(bad("bad translation"));
}
}
}
}
Ok(())
}
@@ -48,6 +48,7 @@
//! | `SetTolerance` | `v tol only_if_larger cascade` | `tolerance` |
//! | `RefreshTolerance` | `v nested` | `status tolerance edge_tolerances...` |
//! | `Exact` | `v exact poly` | `is_exact has_poly_vertex` |
//! | `Corner` | `v has_move dx dy dz` | `0 within max_gap max_gap_face [on found gap best[3]] passed`, `on found gap best[3]`, `parallel min_ang max_gap face1 face2` (or `-1` without faces), `within max_gap face` (forced), then the gap states; `-2` alone where upstream dereferences null |
//!
//! [`eval`] accepts only what upstream handles without undefined behaviour: existing
//! `.smb` files, well-formed clamped B-splines, planes with a non-zero normal, indices
@@ -86,6 +87,7 @@ pub enum Op {
SetTolerance = 14,
RefreshTolerance = 15,
Exact = 16,
Corner = 17,
}
impl Op {
@@ -433,6 +435,10 @@ fn brep_args(op: Op, rd: &mut Reader<'_>, c: [usize; 10]) -> Result<(), FfiError
rd.flag()?;
rd.flag()?;
}
Op::Corner => {
rd.flag()?;
rd.vec3()?;
}
}
Ok(())
}
@@ -1,12 +1,14 @@
//! The surface validity checks (`AssertValid`): the base class `SmSurface` and its
//! virtual dispatch over the B-spline, plane, cone, cylinder, sphere, torus and
//! surface-of-revolution overrides, with the predicates only they use.
//! virtual dispatch over the B-spline, plane, cone, cylinder, sphere, torus,
//! surface-of-revolution and surface-of-extrusion overrides, with the predicates only
//! they use.
//!
//! Upstream: `source/SMLib/src/SmSurface.cpp`, `source/SMLib/src/SmBSplineSurface.cpp`,
//! `source/SMLib/src/SmNurbsSrf.cpp`, `source/SMLib/src/SmPlane.cpp`,
//! `source/SMLib/src/SmCone.cpp`, `source/SMLib/src/SmCylinder.cpp`,
//! `source/SMLib/src/SmSphere.cpp`, `source/SMLib/src/SmTorus.cpp`,
//! `source/SMLib/src/SmSurfOfRevolution.cpp`, `source/SMLib/src/SmAxis2Placement.cpp`,
//! `source/SMLib/src/SmSurfOfRevolution.cpp`, `source/SMLib/src/SmSurfOfExtrusion.cpp`,
//! `source/SMLib/src/SmAxis2Placement.cpp`,
//! `source/SMLib/src/SmPointSet.cpp`, `source/SMLib/inc/SmAssertArray.h`.
//!
//! | Rust | Upstream |
@@ -21,6 +23,7 @@
//! | `sphere_assert_valid` (private) | `SmSphere::AssertValid` |
//! | `torus_assert_valid` (private) | `SmTorus::AssertValid` |
//! | `surf_of_revolution_assert_valid` (private) | `SmSurfOfRevolution::AssertValid` |
//! | `surf_of_extrusion_assert_valid` (private) | `SmSurfOfExtrusion::AssertValid` |
//! | [`axis2_placement_assert_valid`] | `SmAxis2Placement::AssertValid` |
//! | [`has_knot_multiplicity_greater_than_degree`] | `SmBSplineSurface::HasKnotMultiplicityGreaterThanDegree`, `sm_HasKnotMultiplicityGreaterThanDegree` |
//! | [`has_internal_pole`] | `SmBSplineSurface::HasInternalPole` |
@@ -40,45 +43,58 @@
//! class. The placement's reports (`m_vPosition.AssertValid`, its own object
//! upstream) also name `key`: their reporting type differs, so they never collide
//! with the surface's.
//! - **Context tests** (`SmSurfOfRevolution` 0, 1) always hold: one kernel is the
//! only context.
//! - **Generator owner** (`SmSurfOfRevolution` 14): the generator is held by value, so
//! it is owned by the surface unless its owner handle names another object.
//! Upstream dereferences a missing generator there; that is
//! [`SolidError::InvalidInput`].
//! - **Context tests** (`SmSurfOfRevolution` 0, 1, `SmSurfOfExtrusion` 27) always
//! hold: one kernel is the only context.
//! - **Generator owner** (`SmSurfOfRevolution` 14, `SmSurfOfExtrusion` 26): the
//! generator is held by value, so it is owned by the surface unless its owner handle
//! names another object. Upstream dereferences a missing generator there (and in
//! `SmSurfOfExtrusion` 7 and 8); that is [`SolidError::InvalidInput`].
//! - **Angles** (`SmSurfOfExtrusion` 10, 12, 14, 16, 18, 20): a failed `AngleBetween`
//! (a zero-length vector) is the angle `0` it writes, as upstream, which ignores its
//! status. The `GlobalPropertyAnalysis` of tests 21 and 23 fails for a zero-length
//! extrusion vector (its `LocalSolve` `SER`s the same `AngleBetween`); upstream
//! ignores that status too and reads the solutions it reset on entry, so it is no
//! solution found (`no_tangent_along`).
//! - **Warnings.** `SmBSplineSurface::AssertValid`'s repeated-end-control-point
//! check only issues a `WARN`, so `HasRepeatedEndControlPoints` is not run.
//! - **Undefined behaviour.** Index arithmetic of `HasInternalPole` that leaves the
//! control net (unclamped knot vectors), the first/last polar angle of an empty
//! table, missing iso boundaries and a continuity output `CalculateContinuities`
//! never wrote are [`SolidError::InvalidInput`]. A failed `CalculateContinuities`
//! (status ignored) answers the minimum continuity it had reached.
//! (status ignored) answers the minimum continuity it had reached. In
//! `SmSurfOfExtrusion::AssertValid` a `plane_point_distance` error is mapped to
//! [`SolidError::InvalidInput`]: upstream would read `dDist` uninitialised there, which
//! is unreachable, since `IsPlanar`'s normal is never longer than unit.
//! - **Statuses.** Upstream ignores the statuses of the evaluations and queries in
//! these tests; the port propagates their errors (as `validation.rs` does), and a
//! these tests; the port propagates their errors (as `validation.rs` does; except
//! the extrusion's `GlobalPropertyAnalysis`, see **Angles**), and a
//! failed `MakeNurb` is ignored, as upstream.
//! - **Unported overrides.** `SmCurveBoundedSurface`, `SmSrfInVolume`,
//! `SmOffsetSurface` and `SmSurfOfExtrusion` have their own `AssertValid`, not
//! ported here; they return [`SolidError::Unported`]. `SmSTEPSurface` has none and
//! - **Unported overrides.** `SmCurveBoundedSurface`, `SmSrfInVolume` and
//! `SmOffsetSurface` have their own `AssertValid`, not ported here; they return
//! [`SolidError::Unported`]. `SmSTEPSurface` has none and
//! runs the base check.
//! - No recursion.
use std::borrow::Cow;
use crate::curve::curve::p1::GlobalPointSolveOptions;
use crate::curve::curve::p2::TANGENT_FIELD_TYPE;
use crate::curve::curve::{Curve, CurveKind};
use crate::curve::curve::p7::PropertyAnalysisOptions;
use crate::curve::curve::{Curve, CurveKind, CurvePropertyType};
use crate::error::{ignore_status, Result, SolidError};
use crate::geom::a_object;
use crate::geom::assert_array::labels::{
AXIS2_PLACEMENT_TYPE, B_SPLINE_SURFACE_TYPE, CONE_TYPE, CRV_ON_SURF_TYPE, CYLINDER_TYPE,
FACE_TYPE, OFFSET_SURFACE_TYPE, PLANE_TYPE, SPHERE_TYPE, SRF_IN_VOLUME_TYPE, SURFACE_TYPE,
SURF_OF_REVOLUTION_TYPE, TORUS_TYPE,
SURF_OF_EXTRUSION_TYPE, SURF_OF_REVOLUTION_TYPE, TORUS_TYPE,
};
use crate::geom::assert_array::{AssertArray, AssertReport, ObjectKey, ReportSpec};
use crate::geom::axis2_placement::Axis2Placement;
use crate::geom::extent1d::Extent1d;
use crate::geom::extent3d::Extent3d;
use crate::geom::geom_utility::p1::line_point_distance;
use crate::geom::geom_utility::p1::{line_point_distance, plane_point_distance};
use crate::geom::geom_utility::p2::ContinuityType;
use crate::geom::global_solver::{SolutionRequested, SolverOperation};
use crate::geom::solution_array::{OperandObject, SolutionType};
use crate::geom::tol::{
approx_tol3d, approx_tol_3d, are_same, are_same_to_tol, is_degenerate_curve, is_zero,
@@ -88,8 +104,8 @@ use crate::geom::tol::{
use crate::geom::vector3d::acos as sm_acos;
use crate::kernel::Kernel;
use crate::math::{
max2, min2, rad_to_deg, Point2, Point3, Vec3, EFF_ZERO, EFF_ZERO_DEG, EFF_ZERO_SQ, PI,
UNDEF_DOUBLE,
max2, min2, rad_to_deg, Point2, Point3, Vec3, EFF_ZERO, EFF_ZERO_DEG, EFF_ZERO_RAD,
EFF_ZERO_SQ, EFF_ZERO_SQRT, PI, UNDEF_DOUBLE,
};
use crate::nurbs::b_spline_curve::BSplineCurve;
use crate::nurbs::b_spline_surface::{BSplineSurface, SurfParamType};
@@ -98,6 +114,7 @@ use crate::nurbs::knots::Side;
use crate::surface::cone::Cone;
use crate::surface::plane::Plane;
use crate::surface::sphere::Sphere;
use crate::surface::surf_of_extrusion::{CurveOrientType, SurfOfExtrusion};
use crate::surface::surf_of_revolution::SurfOfRevolution;
use crate::surface::torus::Torus;
@@ -198,7 +215,7 @@ impl Surface {
/// # Errors
///
/// [`SolidError::Unported`] for the surface kinds with their own unported check
/// (curve bounded, surface in volume, offset, extrusion) and from callees; the
/// (curve bounded, surface in volume, offset) and from callees; the
/// errors of the evaluations and queries the tests run (statuses upstream
/// ignores); [`SolidError::InvalidInput`] where upstream's behaviour is undefined
/// (see the module docs).
@@ -245,13 +262,7 @@ impl Surface {
"surface.offset_surface",
))
}
SurfaceKind::SurfOfExtrusion(_) => {
// UNPORTED(surface.surf_of_extrusion_p1): SmSurfOfExtrusion::AssertValid
Err(SolidError::unported(
"SmSurfOfExtrusion::AssertValid",
"surface.surf_of_extrusion_p1",
))
}
SurfaceKind::SurfOfExtrusion(_) => surf_of_extrusion_assert_valid(k, self, &mut r),
}
}
}
@@ -450,6 +461,7 @@ fn b_spline_part(s: &Surface) -> Option<&BSplineSurface> {
SurfaceKind::Sphere(sp) => Some(sp.base().base()),
SurfaceKind::Torus(t) => Some(t.base().base()),
SurfaceKind::SurfOfRevolution(sr) => Some(sr.base()),
SurfaceKind::SurfOfExtrusion(e) => Some(e.base()),
_ => None,
}
}
@@ -1495,6 +1507,303 @@ fn surf_of_revolution_assert_valid(k: &Kernel, s: &Surface, r: &mut Reporter<'_>
Ok(rtn)
}
/// `ANALYTIC_TOL_SCALE` (`SmConfig.h`).
const ANALYTIC_TOL_SCALE: f64 = 100.0;
fn extrusion_of(s: &Surface) -> Result<&SurfOfExtrusion> {
match s.kind() {
SurfaceKind::SurfOfExtrusion(e) => Ok(e),
_ => Err(invalid("not an SmSurfOfExtrusion")),
}
}
/// `SmVector3d::AngleBetween` with its status ignored: the angle it writes, `0` when
/// a vector has zero length.
fn angle_or_zero(a: Vec3, b: Vec3) -> f64 {
a.angle_between(b).unwrap_or(0.0)
}
/// Whether `angle_rad` is within `SM_EFF_ZERO_RAD` of `0` or `PI`, and its distance to
/// the nearer one (`smos_Min(smos_Fabs(a), smos_Fabs(a - SM_PI))`).
fn parallel_angle(angle_rad: f64) -> (bool, f64) {
(
is_zero_to_tol(angle_rad, EFF_ZERO_RAD) || is_zero_to_tol(angle_rad - PI, EFF_ZERO_RAD),
min2(angle_rad.abs(), (angle_rad - PI).abs()),
)
}
/// `GlobalPropertyAnalysis(SM_CP_PARALLEL_TO_VECTOR)` of the generator over its
/// natural interval with the extrusion vector: whether it found no solution. The
/// status is ignored, as upstream: a failed analysis leaves `sSolutions` empty (it is
/// reset on entry, and the failing check, `AngleBetween` of a zero-length extrusion
/// vector in `SmCurvePropertyGS::LocalSolve`, runs before a solution is kept), so it
/// counts as none found. [`SolidError::Unported`] still propagates.
fn no_tangent_along(k: &Kernel, gen: &Curve, extrusion_vec: Vec3, tol: f64) -> Result<bool> {
let vectors = [extrusion_vec];
let opts = PropertyAnalysisOptions::<OperandObject> {
property_value: Some(0.0),
opt_vectors: &vectors,
want_comb: false,
handle: None,
};
match gen.global_property_analysis(
k,
&gen.natural_interval(),
CurvePropertyType::ParallelToVector,
tol,
&opts,
) {
Ok(found) => Ok(found.solutions.is_empty()),
Err(e) if e.is_unported() => Err(e),
Err(_) => Ok(true),
}
}
/// `SmSurfOfExtrusion::AssertValid`.
#[allow(clippy::too_many_lines)]
fn surf_of_extrusion_assert_valid(k: &Kernel, s: &Surface, r: &mut Reporter<'_>) -> Result<bool> {
const CLASS: i64 = SURF_OF_EXTRUSION_TYPE;
let mut rtn = true;
// call the base class AssertValid
if has_nurb(s) {
rtn &= if r.bases() {
b_spline_surface_assert_valid(k, s, r)?
} else {
true
};
}
let ext = extrusion_of(s)?;
let extrusion_vec = ext.extrusion_vector();
let anal = ext.step_uv_domain();
// 0. m_vExtrusionVec is initialized and nonZero length
rtn &= r.report(CLASS, 0, 0, !extrusion_vec.is_undef(), UNDEF, UNDEF)?;
let length = extrusion_vec.length();
rtn &= r.report(CLASS, 1, 0, length > EFF_ZERO, EFF_ZERO, length)?;
// 1. AnalUVDomain Check - it has a nonZero area
rtn &= r.report(
CLASS,
2,
0,
anal.x_length() > EFF_ZERO,
EFF_ZERO,
anal.x_length(),
)?;
rtn &= r.report(
CLASS,
3,
0,
anal.y_length() > EFF_ZERO,
EFF_ZERO,
anal.y_length(),
)?;
// 2. Domain consistency
if let Some(gen) = ext.gen_curve() {
/* 24 */
// GenCurve InsideOut bit (if it has one) must be FALSE
rtn &= r.report(CLASS, 24, 0, !curve_inside_out(gen), UNDEF, UNDEF)?;
if has_nurb(s) {
/* 25 */
// GenCurve must have same parameterization as Surface in GenCurve direction
let nurb_dir = if ext.swap_uv() {
SurfParamType::V
} else {
SurfParamType::U
};
let same = has_same_parameterization_curve(k, s, SurfParamType::U, nurb_dir, gen)?;
rtn &= r.report(CLASS, 25, 0, same, EFF_ZERO, UNDEF)?;
}
/* 26 */
// GenCurve owner must be this SmSurfOfExtrusion object
let owner_ok = gen.owner().is_none_or(|o| o.key.0 == r.key.0);
rtn &= r.report(CLASS, 26, 0, owner_ok, UNDEF, UNDEF)?;
/* 27 */
// GenCurve context must be this SmSurfOfExtrusion context (one kernel: always)
rtn &= r.report(CLASS, 27, 0, true, UNDEF, UNDEF)?;
}
// check that STEP and Nurb representations are the same
let tol_u = EFF_ZERO * (1.0 + anal.u_interval().length());
let tol_v = EFF_ZERO * (1.0 + anal.v_interval().length());
if has_nurb(s) {
const COUNT: usize = 3;
for ii in 0..=COUNT {
for jj in 0..=COUNT {
// check UV conversions
let step_uv = anal.evaluate(ii as f64 / COUNT as f64, jj as f64 / COUNT as f64);
let nurb_uv = s.convert_uv_from_step_to_nurbs(k, step_uv)?;
let test_step_uv = s.convert_uv_from_nurbs_to_step(k, nurb_uv)?;
let good_u = is_zero_to_tol(step_uv.x - test_step_uv.x, tol_u);
let good_v = is_zero_to_tol(step_uv.y - test_step_uv.y, tol_v);
if !good_u || !good_v {
rtn &= r.report(CLASS, 5, 0, good_u, tol_u, step_uv.x - test_step_uv.x)?;
rtn &= r.report(CLASS, 5, 0, good_v, tol_v, step_uv.y - test_step_uv.y)?;
}
// check equivalent evaluations
let step_point = s.evaluate_step_point(k, step_uv)?;
let nurb_point = s.evaluate_point(k, nurb_uv)?;
let dist = step_point.distance(nurb_point);
rtn &= r.report(
CLASS,
6,
0,
is_zero_to_tol(dist, EFF_ZERO_SQRT),
EFF_ZERO_SQRT,
dist,
)?;
}
}
}
// (upstream dereferences the generator from here on)
let ext = extrusion_of(s)?;
let gen = ext
.gen_curve()
.ok_or_else(|| invalid("surface of extrusion without generator"))?;
// 3. check that m_vAnalUVDomain->UInterval is defined by the genCurve analytic interval
let gen_step = curve_step_interval(gen)?;
rtn &= r.report(
CLASS,
7,
0,
anal.u_interval().are_equal(&gen_step, tol_u),
tol_u,
UNDEF,
)?;
// 4. check that m_vOrigin is a point on m_pGenCurve
let gen_ivl = gen.natural_interval();
let solutions = gen.global_point_solve(
k,
&gen_ivl,
SolverOperation::Intersect,
ext.origin(),
EFF_ZERO,
SolutionRequested::All,
&GlobalPointSolveOptions::<OperandObject>::default(),
)?;
rtn &= r.report(CLASS, 8, 0, !solutions.is_empty(), UNDEF, UNDEF)?;
// 5. Check cached m_vCurvePoint/m_vCurveVec information
let pnt = gen.evaluate_point(k, gen_ivl.evaluate(0.45678))?;
let tol =
ANALYTIC_TOL_SCALE * EFF_ZERO * (1.0 + ext.origin().max_dimension() + pnt.max_dimension());
let lin = gen.is_linear(k, tol, false)?;
let planar = gen.is_planar(k, tol)?;
let curve_point = ext.curve_point();
let curve_vec = ext.curve_vec();
if lin.linear {
// m_vCurvePoint = Point on line, m_vCurveVec = Line Tangent
let (dist, _) = line_point_distance(lin.line_point, lin.line_tangent, curve_point);
let (parallel, off) = parallel_angle(angle_or_zero(lin.line_tangent, curve_vec));
rtn &= r.report(CLASS, 11, 0, is_zero_to_tol(dist, EFF_ZERO), EFF_ZERO, dist)?;
rtn &= r.report(CLASS, 12, 0, parallel, EFF_ZERO_RAD, off)?;
} else if planar.is_planar {
// m_vCurvePoint = Point on Plane, m_vCurveVec = Plane Normal
let dist = plane_point_distance(planar.point, planar.normal, curve_point)
.map_err(|_| invalid("plane distance not computed (uninitialised upstream)"))?;
let (parallel, off) = parallel_angle(angle_or_zero(planar.normal, curve_vec));
rtn &= r.report(CLASS, 9, 0, is_zero_to_tol(dist, EFF_ZERO), EFF_ZERO, dist)?;
rtn &= r.report(CLASS, 10, 0, parallel, EFF_ZERO_RAD, off)?;
}
// 6. check that every surface point is unique (no generator tangent along the sweep)
match ext.orientation() {
CurveOrientType::Linear => {
let (parallel, off) = parallel_angle(angle_or_zero(lin.line_tangent, extrusion_vec));
// geometry description check
rtn &= r.report(
CLASS,
13,
0,
lin.linear && planar.is_planar,
EFF_ZERO,
UNDEF,
)?;
// unique sweep check
rtn &= r.report(CLASS, 14, 0, !parallel, EFF_ZERO_RAD, off)?;
}
CurveOrientType::Perpendicular => {
let (parallel, off) = parallel_angle(angle_or_zero(extrusion_vec, planar.normal));
rtn &= r.report(
CLASS,
15,
0,
!lin.linear && planar.is_planar,
EFF_ZERO,
UNDEF,
)?;
// orthogonal sweep check
rtn &= r.report(CLASS, 16, 0, parallel, EFF_ZERO_RAD, off)?;
}
CurveOrientType::Independent => {
let off = angle_or_zero(extrusion_vec, planar.normal) - PI / 2.0;
rtn &= r.report(
CLASS,
17,
0,
!lin.linear && planar.is_planar,
EFF_ZERO,
UNDEF,
)?;
// nonPlanar sweep check
let nonplanar = !is_zero_to_tol(off, EFF_ZERO_RAD);
rtn &= r.report(CLASS, 18, 0, nonplanar, EFF_ZERO_RAD, off)?;
}
CurveOrientType::Coplanar => {
let off = angle_or_zero(extrusion_vec, planar.normal) - PI / 2.0;
rtn &= r.report(
CLASS,
19,
0,
!lin.linear && planar.is_planar,
EFF_ZERO,
UNDEF,
)?;
// CoPlanar check
rtn &= r.report(
CLASS,
20,
0,
is_zero_to_tol(off, EFF_ZERO_RAD),
EFF_ZERO_RAD,
off,
)?;
// look for points of tangency on GenCurve with the extrusion Vec direction
let none = no_tangent_along(k, gen, extrusion_vec, tol)?;
rtn &= r.report(CLASS, 21, 0, none, UNDEF, UNDEF)?;
}
CurveOrientType::NotPlanar => {
rtn &= r.report(
CLASS,
22,
0,
!lin.linear && !planar.is_planar,
EFF_ZERO,
UNDEF,
)?;
// look for points of tangency on GenCurve with the extrusion Vec direction
let none = no_tangent_along(k, gen, extrusion_vec, tol)?;
rtn &= r.report(CLASS, 23, 0, none, UNDEF, UNDEF)?;
}
CurveOrientType::Undefined => {}
}
Ok(rtn)
}
/// `SmCone::AssertValid`.
fn cone_assert_valid(k: &Kernel, s: &Surface, r: &mut Reporter<'_>) -> Result<bool> {
const CLASS: i64 = CONE_TYPE;
@@ -2134,4 +2443,62 @@ mod tests {
)],
);
}
/// A surface of extrusion of the quadratic arc through (0, 0, 0), (1, 1, 0),
/// (2, 0, 0) along `vec`, over the generator's interval by `v`, with the origin
/// `origin` (the generator point at `0.3` when `None`).
fn extrusion(vec: Vec3, v: (f64, f64), origin: Option<Point3>) -> Surface {
let k = Kernel::default();
let pts = vec![
CPoint::new(0.0, 0.0, 0.0, NOW),
CPoint::new(1.0, 1.0, 0.0, NOW),
CPoint::new(2.0, 0.0, 0.0, NOW),
];
let arc = NlCurve::new(pts, 2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap();
let gen = Curve::from(BSplineCurve::from_nurb(arc, 3, false));
let origin = origin.unwrap_or_else(|| gen.evaluate_point(&k, 0.3).unwrap());
let d = Extent2d::from_bounds(0.0, v.0, 1.0, v.1);
Surface::from(SurfOfExtrusion::new(&k, gen, origin, vec, d, false).unwrap())
}
#[test]
fn valid_extrusion_passes_at_every_level() {
let s = extrusion(Vec3::new(0.0, 0.0, 1.0), (0.0, 2.0), None);
assert_clean(&s);
assert_clean(&with_nurb(s));
}
#[test]
fn extrusion_without_extent() {
let s = extrusion(Vec3::new(0.0, 0.0, 1.0), (1.0, 1.0), None);
expect(
&s,
0,
&[(
SURF_OF_EXTRUSION_TYPE,
3,
"Analytic Y",
"m_vAnalUVDomain YLength must be non-zero",
)],
);
}
#[test]
fn extrusion_origin_off_the_generator() {
let s = extrusion(
Vec3::new(0.0, 0.0, 1.0),
(0.0, 2.0),
Some(Point3::new(5.0, 5.0, 5.0)),
);
expect(
&s,
0,
&[(
SURF_OF_EXTRUSION_TYPE,
8,
"Origin",
"m_vOrigin is a point on m_pGenCurve",
)],
);
}
}
@@ -273,20 +273,41 @@ fn tolerances_validate_only(k: &mut Kernel, b: &mut Brep, cx: &mut Cx<'_>) -> Re
return Ok(ERROR);
};
// get all edgeuse start/end vertex gaps
let start_edge_gap = tvu::vertex_edge_gap(k, b, start_vu, false)?;
let start_face_gap = tvu::vertex_face_gap(k, b, start_vu, false)?;
let end_edge_gap = tvu::vertex_edge_gap(k, b, end_vu, false)?;
let end_face_gap = tvu::vertex_face_gap(k, b, end_vu, false)?;
// get all edgeuse start/end vertex gaps: upstream keeps pointers into the
// vertex uses' caches and reads their lengths below, so a later refresh that
// resets a sibling's cache (`CalcVertexFaceGap` / `CalcVertexEdgeGap` clear
// every use of the vertex on the same face / edge, e.g. both ends of a
// closed edge) shows in an earlier gap
let start_edge_gap = tvu::vertex_edge_gap(k, b, start_vu, false)?.is_some();
let start_face_gap = tvu::vertex_face_gap(k, b, start_vu, false)?.is_some();
let end_edge_gap = tvu::vertex_edge_gap(k, b, end_vu, false)?.is_some();
let end_face_gap = tvu::vertex_face_gap(k, b, end_vu, false)?.is_some();
let start_tol = b.vertex(start_v)?.tolerance;
let end_tol = b.vertex(end_v)?.tolerance;
let (start, end) = (b.vertexuse(start_vu)?, b.vertexuse(end_vu)?);
// report bad edgeuse start and end vertex/edge and vertex/face gaps
let gaps = [
(0, start_edge_gap.map(|g| g.gap_3d), start_tol),
(1, start_face_gap.map(|g| g.gap_3d), start_tol),
(2, end_edge_gap.map(|g| g.gap_3d), end_tol),
(3, end_face_gap.map(|g| g.gap_3d), end_tol),
(
0,
start_edge_gap.then_some(start.vertex_edge_gap.gap_3d),
start_tol,
),
(
1,
start_face_gap.then_some(start.vertex_face_gap.gap_3d),
start_tol,
),
(
2,
end_edge_gap.then_some(end.vertex_edge_gap.gap_3d),
end_tol,
),
(
3,
end_face_gap.then_some(end.vertex_face_gap.gap_3d),
end_tol,
),
];
for (index, gap, tol) in gaps {
let ok = !need || gap.is_none_or(|g| g <= tol / 2.0);
@@ -193,37 +193,29 @@ pub(super) fn crv_in_volume_assert_valid(
/// `sm_AssertTestFace32(this)` (`== TRUE`): no coincident vertices
/// (`HasCoincidentVertices`). Face test 32, evaluated only at `SM_LEVEL_2` and above.
pub(super) fn assert_test_face32(_k: &mut Kernel, _b: &mut Brep, _f: FaceId) -> Result<bool> {
// UNPORTED(topology.face_p4): sm_AssertTestFace32
Err(unported("sm_AssertTestFace32", "topology.face_p4"))
pub(super) fn assert_test_face32(_k: &mut Kernel, b: &mut Brep, f: FaceId) -> Result<bool> {
crate::topology::face::assert_tests::assert_test_face32(b, f)
}
/// `sm_AssertTestFace33(this)` (`== TRUE`): no coincident edges (`HasCoincidentEdges`).
/// Face test 33, evaluated only at `SM_LEVEL_2` and above.
pub(super) fn assert_test_face33(_k: &mut Kernel, _b: &mut Brep, _f: FaceId) -> Result<bool> {
// UNPORTED(topology.face_p4): sm_AssertTestFace33
Err(unported("sm_AssertTestFace33", "topology.face_p4"))
pub(super) fn assert_test_face33(k: &mut Kernel, b: &mut Brep, f: FaceId) -> Result<bool> {
crate::topology::face::assert_tests::assert_test_face33(k, b, f)
}
/// `sm_AssertTestFace38(this)` (`== TRUE`): the face loops are disjoint in UV
/// (`sm_DoLoopsIntersectInUV`). Face test 38, evaluated only at `SM_LEVEL_2` and above
/// and only for a face with a surface.
pub(super) fn assert_test_face38(_k: &mut Kernel, _b: &mut Brep, _f: FaceId) -> Result<bool> {
// UNPORTED(topology.face_p4): sm_AssertTestFace38
Err(unported("sm_AssertTestFace38", "topology.face_p4"))
pub(super) fn assert_test_face38(k: &mut Kernel, b: &mut Brep, f: FaceId) -> Result<bool> {
crate::topology::face::assert_tests::assert_test_face38(k, b, f)
}
/// `sm_AssertTestEdge13(this, &dMaxGap)`: `(passed, max_gap)`, the edge curve within
/// `SmTol::GetXSectTol3d(this, this)` of every face surface (`IsOnFaceEdgeForm`).
/// Edge test 13, evaluated only at `SM_LEVEL_2` and above; `max_gap` is the report
/// value (upstream initialises it to `0.0`).
pub(super) fn assert_test_edge13(
_k: &mut Kernel,
_b: &mut Brep,
_e: EdgeId,
) -> Result<(bool, f64)> {
// UNPORTED(topology.edge): sm_AssertTestEdge13
Err(unported("sm_AssertTestEdge13", "topology.edge"))
pub(super) fn assert_test_edge13(k: &mut Kernel, b: &mut Brep, e: EdgeId) -> Result<(bool, f64)> {
crate::topology::edge::edge_form::assert_test_edge13(k, b, e)
}
/// `sm_AssertTestVertex1(this, &dMaxGap3d, &pMaxGapFace)`: `(passed, max_gap_3d)`, the
@@ -231,10 +223,10 @@ pub(super) fn assert_test_edge13(
/// (`IsWithinXSectTol3dOfFaces`, `IsOnFaceCorner`). Called at every level; its report
/// (Vertex test 1) is level 2.
pub(super) fn assert_test_vertex1(
_k: &mut Kernel,
_b: &mut Brep,
_v: VertexId,
k: &mut Kernel,
b: &mut Brep,
v: VertexId,
) -> Result<(bool, f64)> {
// UNPORTED(topology.vertex): sm_AssertTestVertex1
Err(unported("sm_AssertTestVertex1", "topology.vertex"))
let (passed, max_gap_3d, _) = crate::topology::vertex::corner::assert_test_vertex1(k, b, v)?;
Ok((passed, max_gap_3d))
}
@@ -30,7 +30,7 @@ pub(super) fn vertex(k: &mut Kernel, b: &mut Brep, cx: &mut Cx<'_>, v: VertexId)
let (located, max_gap) = seams::assert_test_vertex1(k, b, v)?;
if !located {
// SmTol::GetXSectTol3d(GetBrep())
let xsect_tol = 2.0 * b.tolerance();
let xsect_tol = crate::topology::vertex::corner::xsect_tol_3d_of_brep(k, b);
if max_gap > xsect_tol {
let ok = !cx.rep.need(2);
rtn &= cx
+3
View File
@@ -29,6 +29,7 @@
//! | [`vertices`], [`vertexuses`], [`edgeuses`], [`AttributeFilter`] | `SmEdge::GetVertices(SmTArray&, ULONG*)`, `GetVertexuses`, `GetEdgeuses` |
//! | [`max_gap_3d`], [`max_up_dim_gap_3d`], [`max_down_dim_gap_3d`], [`EdgeGap`], [`MaxGap`] | `SmEdge::GetMaxGap3d`, `GetMaxUpDimGap3d`, `GetMaxDownDimGap3d` |
//! | [`number_face_occurances`] | `SmEdge::GetNumberFaceOccurances` |
//! | [`edge_form`] (submodule) | `SmEdge::IsOnFaceEdgeForm`, `sm_AssertTestEdge13` |
//! | [`add_oriented_eu_pair`] | `SmEdge::AddOrientedEUPair` |
//! | [`vertex`], [`start_vertex`], [`end_vertex`], [`other_vertex`], [`start_end_vertices`] | `SmEdge::GetVertex`, `GetStartVertex`, `GetEndVertex`, `GetOtherVertex`, `GetVertices(SmVertex*&, SmVertex*&)` |
//! | [`insert_manifold_edge_end`] | `SmEdge::InsertManifoldEdgeEnd` |
@@ -108,6 +109,8 @@
//! two face surfaces are moved out of their faces for the call and back, so the
//! covering surface keeps any expansion, as upstream.
pub mod edge_form;
use crate::curve::curve::{Curve, CurveKind};
use crate::error::{ignore_status, EntityRef, Result, SolidError};
use crate::geom::a_object;
@@ -0,0 +1,138 @@
//! The edge "on face edge form" check: the edge curve within the cross-section
//! tolerance of every face it bounds (`SmEdge::AssertValid` test 13).
//!
//! Upstream: `source/SMLib/src/SmEdge.cpp`, `source/SMLib/inc/SmEdge.h`.
//!
//! | Rust | Upstream |
//! |----------------------------|-----------------------------|
//! | [`is_on_face_edge_form`] | `SmEdge::IsOnFaceEdgeForm` |
//! | [`assert_test_edge13`] | `sm_AssertTestEdge13` |
//!
//! Deviations: the optional `double*` max-gap output is the second tuple member,
//! always returned. `GetMaxEdgeFaceGap` fills the edge uses' gap cache, so the B-rep
//! is `&mut`. `SmTol::GetXSectTol3d(this, pFace)` is the sum of the two
//! `SmTopology::GetTolerance` zone tolerances (`SM_USE_OLDTOL`), the system zone
//! tolerance for a null face. `dMaxGap3d < *pMaxEdgeFaceGap` resolves to
//! `operator<(double, const SmGap&)` (`fabs(d) < gap`); `*pMaxEdgeFaceGap >
//! sXSectTol3d` to `operator>(double, const SmTol3d&)` on the gap's `m_dGap3d`. No
//! recursion; the face loop is bounded by the edge's edge uses.
use crate::error::Result;
use crate::geom::tol::{self, SmallTopoType};
use crate::kernel::Kernel;
use crate::topology::edgeuse::p1 as eu1;
use crate::topology::face::p2 as face2;
use crate::topology::{Brep, EdgeId};
/// `SmEdge::IsOnFaceEdgeForm(&dMaxGap3d)`: `(ok, max_gap_3d)`. `ok` is false when the
/// cached max edge/face gap of an edge use on any of the edge's faces exceeds
/// `SmTol::GetXSectTol3d(edge, face)`; `max_gap_3d` is the largest such gap (`0.0`
/// when there is none).
///
/// # Errors
///
/// [`crate::SolidError::NotFound`] for a dangling id; those of
/// [`eu1::max_edge_face_gap`] (the gap calculation's own status is ignored, as
/// upstream).
pub fn is_on_face_edge_form(k: &Kernel, b: &mut Brep, e: EdgeId) -> Result<(bool, f64)> {
// locals
let mut ok = true;
let mut max_gap_3d = 0.0_f64;
let faces = super::faces(b, e, None)?;
// for every face - check for Max Edge/Face gaps
for f in faces {
// When Edge is connected to Face through an Edgeuse
let Some(eu) = super::edgeuse_of_face(b, e, Some(f))? else {
continue;
};
let face = eu1::face(b, eu)?;
// gap free Edge/Face XSectTol3d
let edge_zone = tol::zone_tol3d(k, super::tolerance(b, e)?);
let face_zone = match face {
Some(face) => tol::zone_tol3d(k, face2::tolerance(b, face)?),
None => tol::zone_tol_3d(k, SmallTopoType::NormalSize),
};
let xsect_tol_3d = tol::xsect_tol_3d_from_zones(k, edge_zone, face_zone);
// Find the Max Edge/Face gap
if let Some(gap) = eu1::max_edge_face_gap(k, b, eu, false)? {
// Save the largest MaxEdgeFaceGap
if max_gap_3d.abs() < gap.gap_3d {
max_gap_3d = gap.gap_3d;
}
// update the output
if gap.gap_3d > xsect_tol_3d {
ok = false;
}
}
}
Ok((ok, max_gap_3d))
}
/// `sm_AssertTestEdge13(pEdge, &dMaxGap3d)`: `(ok, max_gap_3d)` of
/// [`is_on_face_edge_form`] (`SmEdge::AssertValid` test 13, level 2).
///
/// # Errors
///
/// Those of [`is_on_face_edge_form`].
pub fn assert_test_edge13(k: &Kernel, b: &mut Brep, e: EdgeId) -> Result<(bool, f64)> {
// bRtn == FALSE when Vertex Point is more than ScaledZero from best corner pos defined by connected faces
is_on_face_edge_form(k, b, e)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geom::axis2_placement::Axis2Placement;
use crate::math::Vec3;
use crate::primitive::api_primitives::create_box;
use crate::topology::vertex;
fn box_brep(k: &mut Kernel) -> Brep {
create_box(k, Vec3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0).unwrap()
}
// A box edge lies exactly on both of its planes: test 13 passes with zero gap.
#[test]
fn box_edges_are_on_their_face_edge_form() {
let mut k = Kernel::default();
let mut b = box_brep(&mut k);
let edges: Vec<EdgeId> = b.edges().ids().collect();
assert_eq!(edges.len(), 12);
for e in edges {
assert_eq!(assert_test_edge13(&k, &mut b, e).unwrap(), (true, 0.0));
}
}
// Move one edge curve 0.01 off one of its planes (the box's zone tolerance is
// far smaller): the gap exceeds XSectTol3d and test 13 fails, reporting the gap.
#[test]
fn offset_edge_curve_fails_test_13() {
let mut k = Kernel::default();
let mut b = box_brep(&mut k);
let e = b.edges().ids().next().unwrap();
let (v1, v2) = super::super::start_end_vertices(&b, e).unwrap();
let p1 = vertex::point(&b, v1.unwrap()).unwrap();
let p2 = vertex::point(&b, v2.unwrap()).unwrap();
let dir = p2 - p1;
// move 0.01 toward the box centre along the first axis across the edge: off
// one face's plane, still inside the other face
let toward = |c: f64| if c < 5.0 { 0.01 } else { -0.01 };
let shift = if dir.x == 0.0 {
Vec3::new(toward(p1.x), 0.0, 0.0)
} else {
Vec3::new(0.0, toward(p1.y), 0.0)
};
let mut place = Axis2Placement::default();
place.translate(shift);
let curve = b.edge_mut(e).unwrap().curve.as_mut().unwrap();
curve.transform(&k, &place, None).unwrap();
let tol = super::super::tolerance(&b, e).unwrap();
assert!(0.01 > 2.0 * tol);
let (ok, gap) = assert_test_edge13(&k, &mut b, e).unwrap();
assert!(!ok);
assert!((gap - 0.01).abs() <= 1e-15, "gap {gap}");
}
}
+1
View File
@@ -4,6 +4,7 @@
//!
//! Upstream: `source/SMLib/src/SmFace.cpp`, `source/SMLib/inc/SmFace.h`.
pub mod assert_tests;
pub mod p1;
pub mod p2;
pub mod p3;
@@ -0,0 +1,516 @@
//! The level-2 `SmFace::AssertValid` tests: coincident face vertices (test 32),
//! coincident face edges (test 33) and face loops meeting in UV (test 38).
//!
//! Upstream: `source/SMLib/src/SmFace.cpp`, `source/SMLib/inc/SmFace.h`.
//!
//! | Rust | Upstream |
//! |---------------------------------|----------------------------------|
//! | [`has_coincident_vertices`] | `SmFace::HasCoincidentVertices` |
//! | [`has_coincident_edges`] | `SmFace::HasCoincidentEdges` |
//! | `find_coincident_verts` (private) | `sm_FindCoincidentVerts` |
//! | [`assert_test_face32`] | `sm_AssertTestFace32` |
//! | [`assert_test_face33`] | `sm_AssertTestFace33` |
//! | [`assert_test_face38`] | `sm_AssertTestFace38` |
//! | `do_loops_intersect_in_uv` (private) | `sm_DoLoopsIntersectInUV` |
//!
//! Deviations:
//! - Out-params become return values: [`CoincidentVertices`] and [`CoincidentEdges`]
//! hold the "to survive" / "to delete" lists. `HasCoincidentVertices` quits after the
//! first pair only when the caller passes no survive list (upstream tests
//! `pOptToSurviveVerts == NULL` twice); [`has_coincident_vertices`] takes that as
//! `quit_early`. `HasCoincidentEdges` always passes its own lists to
//! `HasCoincidentVertices`, and quits early when no edge survive list is passed
//! (`quit_early` again). `sm_AssertTestFace32/33` pass none, so both quit early.
//! - `GlobalCoincidenceChecker`'s status is ignored by `HasCoincidentEdges`: a failed
//! check leaves `rbAdditionalWorkNeeded` at the `FALSE` it was set to on entry (every
//! `SER` return of the checker precedes its assignment) and the solutions empty, so
//! the edge pair counts as coincident. A failing [`Curve::global_coincidence_checker`]
//! is read the same way; [`SolidError::Unported`] propagates.
//! - Edge ends are `Option`s: upstream compares `NULL` vertex pointers like any other.
//! - `sm_AssertTestFace38` dereferences the face surface; a face without one is
//! [`SolidError::InvalidInput`] (`SmFace::AssertValid` only calls it with a surface).
//! The surface is copied before the UV trim curves are built (building them mutates
//! the B-rep, not the surface).
use crate::curve::curve::Curve;
use crate::error::{Result, SolidError};
use crate::geom::extent1d::Extent1d;
use crate::geom::solution_array::{ObjectKind, SolutionObject, SolutionType};
use crate::geom::tol::xsect_tol_3d_from_zones;
use crate::kernel::Kernel;
use crate::math::EFF_ZERO_SQ;
use crate::topology::edgeuse::p1 as eu1;
use crate::topology::edgeuse::p3 as eu3;
use crate::topology::{edge, loopuse, vertex};
use crate::topology::{Brep, EdgeId, FaceId, LoopId, VertexId};
const OP: &str = "topology::face::assert_tests";
fn null(reason: &'static str) -> SolidError {
SolidError::invalid_input(OP, reason)
}
/// The coincident vertex pairs [`has_coincident_vertices`] found
/// (`pOptToSurviveVerts`, `pOptToDeleteVerts`): `to_delete[i]` lies within
/// `SM_EFF_ZERO` of `to_survive[i]`.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CoincidentVertices {
/// Whether any pair was found (the return value).
pub found: bool,
/// The vertices with coincident partners, once per partner.
pub to_survive: Vec<VertexId>,
/// Each pair's partner.
pub to_delete: Vec<VertexId>,
}
/// The coincident edge pairs [`has_coincident_edges`] found (`pOptToSurviveEdges`,
/// `pOptToDeleteEdges`).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CoincidentEdges {
/// Whether any pair was found (the return value).
pub found: bool,
/// The edges with coincident partners, once per partner.
pub to_survive: Vec<EdgeId>,
/// Each pair's partner.
pub to_delete: Vec<EdgeId>,
}
/// `SmFace::HasCoincidentVertices`: the pairs of face vertices (`GetVertices` order)
/// closer than `SM_EFF_ZERO`. A vertex already listed as a partner is not paired
/// again. With `quit_early` the search stops before the next outer vertex once a pair
/// was found (upstream: no survive list passed).
///
/// # Errors
///
/// Those of [`crate::topology::face::p4::vertices`] and [`vertex::point`].
pub fn has_coincident_vertices(
b: &Brep,
f: FaceId,
quit_early: bool,
) -> Result<CoincidentVertices> {
let vertices = crate::topology::face::p4::vertices(b, f, None, false, None)?;
let mut out = CoincidentVertices::default();
let mut done = false;
// for every Vertex
for (ii, &to_survive) in vertices.iter().enumerate() {
if done {
break;
}
// skip Vertices already on the pToDeleteVerts list
if out.to_delete.contains(&to_survive) {
continue;
}
let survive_point = vertex::point(b, to_survive)?;
// for every other vertex - look for coincident partners
for &to_delete in vertices.iter().skip(ii + 1) {
let dist_sq = survive_point.distance_squared(vertex::point(b, to_delete)?);
if dist_sq < EFF_ZERO_SQ {
out.found = true;
done = quit_early;
out.to_survive.push(to_survive);
out.to_delete.push(to_delete);
}
}
}
Ok(out)
}
/// `sm_FindCoincidentVerts`: `vertex` and every vertex the coincident pairs make it
/// coincident with (its surviving partner when it is a partner, then that vertex's
/// partners).
fn find_coincident_verts(
vertex: Option<VertexId>,
pairs: &CoincidentVertices,
) -> Vec<Option<VertexId>> {
// init output
let mut out = vec![vertex];
let mut tgt = vertex;
// See if pVertex is in the pToDeleteVerts
if let Some(indx) = pairs.to_delete.iter().position(|&v| Some(v) == tgt) {
// Make the surviving partner the TgtVertex
tgt = pairs.to_survive.get(indx).copied();
out.push(tgt);
}
// for every pToSurviveVerts vertex
for (ii, &this_vertex) in pairs.to_survive.iter().enumerate() {
if Some(this_vertex) == tgt {
out.push(pairs.to_delete.get(ii).copied());
}
}
out
}
/// The curve objects of the coincidence solutions (upstream names the edge curves):
/// their natural intervals.
#[derive(Debug, Clone, Copy, PartialEq)]
struct CurveHandle(Extent1d);
impl SolutionObject for CurveHandle {
fn kind(&self) -> ObjectKind {
ObjectKind::Curve
}
fn natural_interval(&self) -> Option<Extent1d> {
Some(self.0)
}
}
/// The edge curve (`GetCurve()`), dereferenced only by the coincidence check.
fn edge_curve(b: &Brep, e: EdgeId) -> Result<Option<&Curve>> {
Ok(b.edge(e)?.curve.as_ref())
}
fn deref(c: Option<&Curve>) -> Result<&Curve> {
c.ok_or_else(|| null("HasCoincidentEdges: edge without curve"))
}
/// `SmFace::HasCoincidentEdges`: the pairs of face edges (`GetEdges` order) whose end
/// vertices are the same or coincident ([`has_coincident_vertices`]) in either
/// orientation and whose curves `GlobalCoincidenceChecker` (snapping to the ends)
/// finds coincident within the edges' cross-section tolerance: no more work needed,
/// or one or two range solutions. An edge already listed as a partner is not paired
/// again. With `quit_early` the search stops before the next outer edge once a pair
/// was found (upstream: no survive list passed).
///
/// # Errors
///
/// Those of the face, edge and vertex accessors; [`SolidError::InvalidInput`] for an
/// edge without curve; [`SolidError::Unported`] from the coincidence check.
pub fn has_coincident_edges(
k: &Kernel,
b: &Brep,
f: FaceId,
quit_early: bool,
) -> Result<CoincidentEdges> {
let edges = crate::topology::face::p4::edges(b, f, None, false, None)?;
let mut out = CoincidentEdges::default();
// look for coincident vertices
let verts = has_coincident_vertices(b, f, false)?;
let mut done = false;
// for every Edge
for (ii, &to_survive) in edges.iter().enumerate() {
if done {
break;
}
let survive_curve = edge_curve(b, to_survive)?;
let survive_ivl = b.edge(to_survive)?.interval;
let survive_zone_tol = edge::tolerance(b, to_survive)?;
// skip edges already on the pToDeleteEdges list
if out.to_delete.contains(&to_survive) {
continue;
}
// Get list of all vertices coincident with pToSurviveEdge Start and End Vertices
let start_verts = find_coincident_verts(edge::start_vertex(b, to_survive)?, &verts);
let end_verts = find_coincident_verts(edge::end_vertex(b, to_survive)?, &verts);
// for every other Edge - look for coincident partners
for &to_delete in edges.iter().skip(ii + 1) {
let delete_curve = edge_curve(b, to_delete)?;
let delete_ivl = b.edge(to_delete)?.interval;
let start_vertex = edge::start_vertex(b, to_delete)?;
let end_vertex = edge::end_vertex(b, to_delete)?;
let delete_zone_tol = edge::tolerance(b, to_delete)?;
// only check edges which share common endPoint vertices
if (start_verts.contains(&start_vertex) && end_verts.contains(&end_vertex))
|| (end_verts.contains(&start_vertex) && start_verts.contains(&end_vertex))
{
let xsect_tol = xsect_tol_3d_from_zones(k, survive_zone_tol, delete_zone_tol);
let survive_curve = deref(survive_curve)?;
let delete_curve = deref(delete_curve)?;
let found = survive_curve.global_coincidence_checker(
k,
&survive_ivl,
delete_curve,
&delete_ivl,
xsect_tol,
true,
[
Some(CurveHandle(survive_curve.natural_interval())),
Some(CurveHandle(delete_curve.natural_interval())),
],
);
// status ignored: a failed check leaves bAdditionalWorkNeeded FALSE
let (additional_work_needed, types) = match found {
Ok(c) => (
c.additional_work_needed,
c.solutions
.iter()
.map(|s| s.solution_type)
.collect::<Vec<_>>(),
),
Err(e) if e.is_unported() => return Err(e),
Err(_) => (false, Vec::new()),
};
// check for coincidence
let range = |i: usize| types.get(i) == Some(&SolutionType::RangeOfValues);
if !additional_work_needed
|| (types.len() == 1 && range(0))
|| (types.len() == 2 && range(0) && range(1))
{
out.found = true;
done = quit_early;
out.to_survive.push(to_survive);
out.to_delete.push(to_delete);
}
}
}
}
Ok(out)
}
/// `sm_AssertTestFace32` (no output lists): `true` when the face has no coincident
/// vertices.
///
/// # Errors
///
/// Those of [`has_coincident_vertices`].
pub fn assert_test_face32(b: &Brep, f: FaceId) -> Result<bool> {
Ok(!has_coincident_vertices(b, f, true)?.found)
}
/// `sm_AssertTestFace33` (no output lists): `true` when the face has no coincident
/// edges.
///
/// # Errors
///
/// Those of [`has_coincident_edges`].
pub fn assert_test_face33(k: &Kernel, b: &Brep, f: FaceId) -> Result<bool> {
Ok(!has_coincident_edges(k, b, f, true)?.found)
}
/// The UV trim curve `GetOrCreateUVTrimCurve(pUVCurve)` gives (built when missing);
/// `None` when the call fails or gives none (inconclusive).
fn uv_curve(k: &Kernel, b: &mut Brep, eu: crate::topology::EdgeuseId) -> Result<Option<Curve>> {
let fetch = eu3::get_or_create_uv_trim_curve(k, b, eu, false, false, true)?;
if fetch.status.is_err() {
return Ok(None);
}
Ok(eu1::uv_trim_curve(b, eu)?.0.cloned())
}
/// `sm_DoLoopsIntersectInUV`: whether any UV trim curve of `loop1` meets one of `loop2`
/// (`GlobalCurveIntersect` at the surface zone tolerance mapped to UV at the domain
/// middle gives a solution). Missing trim curves are built; edge uses whose curve does
/// not build, and pairs whose intersection fails, are skipped.
fn do_loops_intersect_in_uv(
k: &Kernel,
b: &mut Brep,
loop1: LoopId,
loop2: LoopId,
surface: &crate::surface::surface::Surface,
) -> Result<bool> {
let edgeuses1 = loopuse::loop_edgeuses(b, loop1)?;
let edgeuses2 = loopuse::loop_edgeuses(b, loop2)?;
// UVTrimCurves are measured in UVSpace, so the 3d ZoneTol has to be mapped there
let zone_tol_3d = crate::surface::surface::p1::zone_tol_3d(k, surface)?;
let mid = surface.natural_uv_domain_with_nurb(k)?.mid();
let uv_tol = crate::surface::surface::p1::map_to_2d(k, zone_tol_3d, mid, surface)?;
// for every Loop1 UVTrimCurve
for &eu1_id in &edgeuses1 {
let Some(uv_curve1) = uv_curve(k, b, eu1_id)? else {
continue; // inconclusive - nothing to intersect
};
let interval1 = uv_curve1.natural_interval();
// for every Loop2 UVTrimCurve
for &eu2_id in &edgeuses2 {
let Some(uv_curve2) = uv_curve(k, b, eu2_id)? else {
continue; // inconclusive - nothing to intersect
};
let interval2 = uv_curve2.natural_interval();
let solutions = match uv_curve1.global_curve_intersect::<CurveHandle>(
k,
&interval1,
&uv_curve2,
&interval2,
uv_tol,
&crate::curve::curve::p8::CurveIntersectOptions::default(),
) {
Ok(s) => s,
Err(e) if e.is_unported() => return Err(e),
Err(_) => continue, // inconclusive - the intersector gave up on this pair
};
// any solution at all places the two Loops on one another
if !solutions.is_empty() {
return Ok(true);
}
}
}
// arrive here when no intersection was found
Ok(false)
}
/// `sm_AssertTestFace38`: `true` when no two loops of the face meet in its UV space
/// (`sm_DoLoopsIntersectInUV` for every loop pair, `GetLoops` order). A face with
/// fewer than two loops passes.
///
/// # Errors
///
/// [`SolidError::InvalidInput`] for a face without surface (upstream dereferences
/// it); those of the loop, trim curve, tolerance and intersection computations
/// ([`SolidError::Unported`] included).
pub fn assert_test_face38(k: &Kernel, b: &mut Brep, f: FaceId) -> Result<bool> {
let loops = crate::topology::face::p2::loops(b, f, false, None)?;
let surface = crate::topology::face::p2::surface(b, f)?
.ok_or_else(|| null("sm_AssertTestFace38: face without surface"))?
.clone();
// no work - a Face with fewer than 2 Loops has no Loop pair to intersect
if loops.len() < 2 {
return Ok(true);
}
// for every pair of Face Loops
for (ii, &l1) in loops.iter().enumerate() {
for &l2 in loops.iter().skip(ii + 1) {
if do_loops_intersect_in_uv(k, b, l1, l2, &surface)? {
return Ok(false);
}
}
}
// arrive here when no two Face Loops intersect
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::boolean::api_brep::boolean_difference;
use crate::geom::axis2_placement::Axis2Placement;
use crate::math::{Point3, Vec3};
use crate::primitive::api_primitives::{create_box, create_cylinder};
use crate::topology::face::{p2, p4};
fn all_pass(k: &Kernel, b: &mut Brep) {
let faces = crate::topology::brep::p1::faces(b, None).unwrap();
for f in faces {
assert!(assert_test_face32(b, f).unwrap(), "{f:?} test 32");
assert!(assert_test_face33(k, b, f).unwrap(), "{f:?} test 33");
assert!(assert_test_face38(k, b, f).unwrap(), "{f:?} test 38");
}
}
fn edge_between(b: &Brep, f: FaceId, v1: VertexId, v2: VertexId) -> EdgeId {
p4::edges(b, f, None, false, None)
.unwrap()
.into_iter()
.find(|&e| {
let s = edge::start_vertex(b, e).unwrap();
let t = edge::end_vertex(b, e).unwrap();
(s, t) == (Some(v1), Some(v2)) || (s, t) == (Some(v2), Some(v1))
})
.unwrap()
}
/// A box passes tests 32, 33 and 38 on every face.
#[test]
fn box_faces_pass() {
let mut k = Kernel::default();
let mut b = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 2.0, 3.0, 4.0).unwrap();
all_pass(&k, &mut b);
}
/// A box face whose third corner is moved onto its first and whose second edge is
/// given the first edge's curve has one coincident vertex pair (test 32) and one
/// coincident edge pair (test 33).
#[test]
fn coincident_vertices_and_edges_fail() {
let mut k = Kernel::default();
let mut b = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 2.0, 3.0, 4.0).unwrap();
let f = b.faces().ids().next().unwrap();
let vs = p4::vertices(&b, f, None, false, None).unwrap();
let (v0, v1, v2) = (vs[0], vs[1], vs[2]);
let e01 = edge_between(&b, f, v0, v1);
let e12 = edge_between(&b, f, v1, v2);
// test 32 only: the corner moved, the curves untouched
let p0 = b.vertex(v0).unwrap().point;
b.vertex_mut(v2).unwrap().point = p0;
assert!(!assert_test_face32(&b, f).unwrap());
let pairs = has_coincident_vertices(&b, f, false).unwrap();
assert_eq!(
pairs,
CoincidentVertices {
found: true,
to_survive: vec![v0],
to_delete: vec![v2],
}
);
assert!(assert_test_face33(&k, &b, f).unwrap());
// test 33: the edge from v1 to v2 (now at v0) on the curve from v0 to v1
let (curve, interval) = {
let e = b.edge(e01).unwrap();
(e.curve.clone(), e.interval)
};
let e = b.edge_mut(e12).unwrap();
e.curve = curve;
e.interval = interval;
assert!(!assert_test_face33(&k, &b, f).unwrap());
let pairs = has_coincident_edges(&k, &b, f, false).unwrap();
assert!(pairs.found);
assert_eq!(pairs.to_survive.len(), 1);
let pair = [pairs.to_survive[0], pairs.to_delete[0]];
assert!(pair == [e01, e12] || pair == [e12, e01], "{pair:?}");
}
/// The top face of a box with a through hole has two disjoint loops (test 38
/// passes); moving the hole across the face boundary makes them cross (fails).
#[test]
fn loops_crossing_in_uv_fail() {
let mut k = Kernel::default();
let a = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0).unwrap();
let c = create_cylinder(&mut k, Point3::new(5.0, 5.0, -1.0), 2.0, 12.0).unwrap();
let mut b = boolean_difference(&mut k, a, c).unwrap();
all_pass(&k, &mut b);
let faces = crate::topology::brep::p1::faces(&b, None).unwrap();
let mut two_loop_faces = 0;
for f in faces {
let loops = p2::loops(&b, f, false, None).unwrap();
if loops.len() != 2 {
continue;
}
two_loop_faces += 1;
let mut b = b.clone();
// move the inner loop's edge (and its vertex) 4 along x, dropping its UV
// trims so they are rebuilt from the moved curve
let inner = loopuse::loop_edges(&b, loops[1]).unwrap();
let shift = Axis2Placement::new(
Point3::new(4.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
Vec3::new(1.0, 0.0, 0.0),
);
for e in inner.into_iter().flatten() {
let mut curve = b.edge(e).unwrap().curve.clone().unwrap();
curve.transform(&k, &shift, None).unwrap();
b.edge_mut(e).unwrap().curve = Some(curve);
for v in b.edge(e).unwrap().vertices {
let p = b.vertex(v).unwrap().point;
b.vertex_mut(v).unwrap().point = Point3::new(p.x + 4.0, p.y, p.z);
}
for eu in b.edge(e).unwrap().edgeuses.clone() {
b.edgeuse_mut(eu).unwrap().uv_trim_curve = None;
}
}
assert!(!assert_test_face38(&k, &mut b, f).unwrap(), "{f:?}");
}
assert_eq!(two_loop_faces, 2);
}
}
+5 -2
View File
@@ -30,6 +30,7 @@
//! | [`check_pointers`] | `SmVertex::CheckPointers` |
//! | [`edge_start_vertex`], [`edge_vertex_pair`], [`edge_vertices`], [`edge_end_tangent`] | `SmEdge::GetStartVertex`, `GetVertices(SmVertex*&, SmVertex*&)`, `GetVertices(SmTArray&)`, `GetEndTangent` (CO-PORTED, `topology.edge`) |
//! | [`loopuse_vertices`], [`loop_vertices`] | `SmLoopuse::GetVertices`, `SmLoop::GetVertices` (CO-PORTED, `topology.loopuse`) |
//! | [`corner`] | `SmVertex::IsWithinXSectTol3dOfFaces`, `HasParallelFaces`, `CalcCornerFromFaces`, `IsOnFaceCorner`, `sm_AssertTestVertex1` |
//!
//! Deviations:
//! - **Object model.** A vertex is a record in the [`Brep`]'s arenas; the methods are
@@ -78,14 +79,16 @@
//! loop use walk stops with [`SolidError::InvalidInput`] after visiting more edge uses
//! than the B-rep holds (upstream loops forever on a CCW chain that does not return to
//! its start). They have no attribute filter (callers here pass none).
//! - `CalcCornerFromFaces`, `IsZoneTol3dConsistent`, `GetLoopOfFace`,
//! - `IsZoneTol3dConsistent`, `GetLoopOfFace`,
//! `GetVertexuseOfEdge`, `GetVertexNumberInBrep`, the `Has*` / `IsEdgeVertex` /
//! `IsOnSeam` / `IsConnectedToEdge` / `IsConnectedToFace` predicates,
//! `IsWithinXSectTol3dOf*`, `IsOnFaceCorner`, `HasParallelFaces`,
//! `IsWithinXSectTol3dOfEdges`,
//! `IsComplexCornerMember`, `RefineGeometry`, `FindAndRemoveVertexuseByEdge`,
//! `GetMemoryUsed`, `AssertValid`, the graphics and the `Dump`s are not part of this
//! unit.
pub mod corner;
use crate::error::{EntityRef, Result, SolidError};
use crate::geom::attribute::{AttributeStore, ObjectKey};
use crate::geom::context::Context;
@@ -0,0 +1,667 @@
//! The vertex placement checks behind `SmVertex::AssertValid` test 1: the vertex/face
//! gaps against the B-rep's intersection tolerance and the vertex's distance from the
//! corner its faces define (surface/surface/surface intersection, surface/surface
//! refinement or drop to one surface).
//!
//! Upstream: `source/SMLib/src/SmVertex.cpp`, `source/SMLib/inc/SmVertex.h`.
//!
//! | Rust | Upstream |
//! |---------------------------------------|-------------------------------------------|
//! | [`is_within_xsect_tol_3d_of_faces`] | `SmVertex::IsWithinXSectTol3dOfFaces` |
//! | [`has_parallel_faces`], [`ParallelFaces`] | `SmVertex::HasParallelFaces` |
//! | [`calc_corner_from_faces`], [`FaceCorner`] | `SmVertex::CalcCornerFromFaces` |
//! | [`is_on_face_corner`] | `SmVertex::IsOnFaceCorner` |
//! | [`assert_test_vertex1`] | `sm_AssertTestVertex1` (`SmVertex.cpp`) |
//! | `vertex_scaled_zero` (private) | `SmTol::GetScaledZero(const SmVertex&)` |
//! | `brep_xsect_tol_3d` (private), [`xsect_tol_3d_of_brep`] | `SmTol::GetXSectTol3d(GetBrep())` (`SM_USE_OLDTOL`) |
//!
//! Deviations:
//! - **Out-params.** Optional outputs are always computed and returned in structs or
//! tuples. `sm_AssertTestVertex1` is ported for the call `SmVertex::AssertValid`
//! makes (`pOptMaxGap3d`, `pOptMaxGapFace` given, the corner outputs not), so the
//! corner test runs only when the gap test passed.
//! - **Tolerances.** `SM_USE_NEWTOL` is not defined in the reference build, so the
//! intersection tolerance is `2 * SmZoneTol3d(GetBrep()->GetTolerance())` (the zone
//! tolerance constructed first, then doubled, as `SmTol`). A vertex outside any
//! B-rep (`GetBrep()` null, which upstream dereferences) returns
//! [`SolidError::InvalidInput`]. `SmTol::GetApproxTol3d(GetContext())` reads the
//! zone tolerance of the vertex's context, the B-rep's: the kernel's context
//! (`context_approx_tol_3d(k, Some(&k.context), ..)`, as the B-rep constructor reads it).
//! - **Temporary point.** `SmTemporaryChangeValue<SmPoint3d>` (the parallel-face branch
//! of `CalcCornerFromFaces`) sets the vertex point and restores it afterwards; it is
//! restored before any error is returned too.
//! - **Ignored statuses.** The evaluations, `ComputeUVPoint`, `DropPoint`, `RefinePoint`
//! and `LocalSurfaceSurfaceIntersect` statuses are ignored as upstream (an
//! [`SolidError::Unported`] still propagates). A failed `ComputeUVPoint` leaves the
//! previous UV, a failed evaluation the previous (initially undefined) vector, and a
//! failed `AngleBetween` an angle of `0`. A failed `RefinePoint` reports a refined
//! point with undefined UVs (it sets its found flag before the failing local solve),
//! as `intersect::curve_class::p2::refine_on_pair` ports it; a failed `DropPoint` or
//! `LocalSurfaceSurfaceIntersect` reports none found.
//! - **Null pointers.** A face without surface where upstream dereferences it, and
//! [`has_parallel_faces`] on a vertex without faces (upstream reads past an empty
//! array), return [`SolidError::InvalidInput`]. `SM_DBG_WARN`, `SM_ASSERT_MSG` and the
//! `SM_DEBUG_CODE` blocks are compiled out of the reference build.
//! - No input-driven recursion.
use crate::error::{ignored, Result, SolidError};
use crate::geom::tol;
use crate::intersect::surface_intersector::p1::{IntersectionPointType, SurfaceIntersector};
use crate::kernel::Kernel;
use crate::math::{rad_to_deg, Point2, Point3, Vec3, BIG_DOUBLE};
use crate::nurbs::knots::Side;
use crate::surface::surface::p1::map_to_2d;
use crate::surface::surface::p4::NoObject;
use crate::surface::surface::p5::DropPointOptions;
use crate::surface::surface::Surface;
use crate::topology::{vertexuse, Brep, FaceId, VertexId, VertexuseId};
const OP: &str = "topology::vertex::corner";
fn null(reason: &'static str) -> SolidError {
SolidError::invalid_input(OP, reason)
}
/// The face of a vertex use (`pVertexuse->GetFaceuse()->GetFace()`), `None` without.
fn vertexuse_face(b: &Brep, vu: VertexuseId) -> Result<Option<FaceId>> {
Ok(match vertexuse::faceuse(b, vu)? {
Some(fu) => b.faceuse(fu)?.face,
None => None,
})
}
/// The surface of face `f`; upstream dereferences a null one.
fn face_surface(b: &Brep, f: FaceId) -> Result<&Surface> {
b.face(f)?
.surface
.as_ref()
.ok_or_else(|| null("face without surface"))
}
/// `SmTol::GetScaledZero(const SmVertex&)` = `SM_EFF_ZERO * (1 + |point|max)`.
fn vertex_scaled_zero(b: &Brep, v: VertexId) -> Result<f64> {
let p = b.vertex(v)?.point;
Ok(tol::eff_zero() * (1.0 + p.max_dimension()))
}
/// `SmTol::GetXSectTol3d(GetBrep())` (`SM_USE_OLDTOL`) of the vertex's B-rep.
fn brep_xsect_tol_3d(k: &Kernel, b: &Brep, v: VertexId) -> Result<f64> {
let brep = super::brep(b, v)?.ok_or_else(|| null("vertex outside a B-rep (GetBrep() null)"))?;
Ok(xsect_tol_3d_of_brep(k, brep))
}
/// `SmTol::GetXSectTol3d(const SmBrep*)` (`SM_USE_OLDTOL`): `2 * GetZoneTol3d(pBrep)`,
/// in upstream's order: the B-rep's stored tolerance made a zone tolerance
/// (`SmZoneTol3d(double)`), doubled, then made an intersection tolerance
/// (`SmXSectTol3d(double)`).
#[must_use]
pub fn xsect_tol_3d_of_brep(k: &Kernel, brep: &Brep) -> f64 {
tol::xsect_tol3d(k, 2.0 * tol::zone_tol3d(k, brep.tolerance()))
}
/// `SmVertex::IsWithinXSectTol3dOfFaces(&maxGap3d, &maxGapFace, bForceCalc)`: whether
/// every cached vertex/face gap of the vertex's uses is below the B-rep's
/// intersection tolerance, with the largest gap (`0` when none is above `0`) and its
/// face. `force_calc` clears every use's gap cache first, so all gaps are recomputed.
///
/// # Errors
///
/// [`SolidError::NotFound`] for a dangling id; [`SolidError::InvalidInput`] for a vertex
/// outside a B-rep; those of [`vertexuse::vertex_face_gap`].
pub fn is_within_xsect_tol_3d_of_faces(
k: &Kernel,
b: &mut Brep,
v: VertexId,
force_calc: bool,
) -> Result<(bool, f64, Option<FaceId>)> {
// return value
let mut rtn = true;
// init output
let mut max_gap_3d = 0.0;
let mut max_gap_face = None;
// locals (SM_USE_OLDTOL)
let xsect_tol_3d = brep_xsect_tol_3d(k, b, v)?;
let vertexuses = super::vertexuses(b, v)?;
// when asked - clear Vertex/Face gap cache to force gap calculations for every
// Vertexuse
if force_calc {
for &vu in &vertexuses {
vertexuse::init_vertex_face_gap(b, vu)?;
}
}
// for every Vertexuse
for &vu in &vertexuses {
// look for Vertex/Face gaps that exceed XSectTol3d
if vertexuse::has_vertex_face_gap(b, vu)? {
// side effect: Calc Vertex/Face gap on 1st fetch
let gap = vertexuse::vertex_face_gap(k, b, vu, false)?
.ok_or_else(|| null("vertex use without vertex/face gap"))?;
let gap_3d = gap.gap_3d;
// check for gaps that exceed XSectTol3d
rtn &= gap_3d < xsect_tol_3d;
// set output
if max_gap_3d < gap_3d {
max_gap_3d = gap_3d;
max_gap_face = match gap.vertexuse {
Some(gvu) => vertexuse_face(b, gvu)?,
None => None,
};
}
}
}
// all done
Ok((rtn, max_gap_3d, max_gap_face))
}
/// The outputs of [`has_parallel_faces`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ParallelFaces {
/// The return value: some pair of faces has normals within the angle tolerance.
pub parallel: bool,
/// `pOptMinDihedralAngDeg`: the smallest angle (degrees) between face normals seen.
pub min_dihedral_ang_deg: f64,
/// `pOptMaxParallelFaceGap`: the largest gap between parallel face pairs.
pub max_parallel_face_gap: f64,
/// `pOptFace1`: the first face of that pair.
pub face1: Option<FaceId>,
/// `pOptFace2`: the second face of that pair.
pub face2: Option<FaceId>,
/// `pOptUV1`: the first face's gap UV point (undefined when no pair).
pub uv1: Point2,
/// `pOptUV2`: the second face's gap UV point (undefined when no pair).
pub uv2: Point2,
}
/// The surface point and normal at a vertex use's gap UV, both statuses ignored (the
/// previous values stay on failure).
fn gap_point_normal(k: &Kernel, surface: &Surface, uv: Point2, pn: &mut [Vec3; 2]) -> Result<()> {
if let Some(p) = ignored(surface.evaluate_point(k, uv))? {
pn[0] = p;
}
if let Some(n) = ignored(surface.evaluate_normal(k, uv, Side::Left, Side::Left))? {
pn[1] = n;
}
Ok(())
}
/// `SmVertex::HasParallelFaces`: whether two of the vertex's faces have normals (at
/// their vertex/face gap points) within `ang_tol_deg` (`SM_ANG_TOL_DEG` when `None`)
/// of each other, with the smallest dihedral angle and the parallel pair of largest
/// gap. The gaps are taken from (and fill) the vertex uses' gap caches.
///
/// # Errors
///
/// [`SolidError::NotFound`] for a dangling id; [`SolidError::InvalidInput`] for a vertex
/// without faces or a face without surface (upstream dereferences them); those of
/// [`vertexuse::vertex_face_gap`].
pub fn has_parallel_faces(
k: &Kernel,
b: &mut Brep,
v: VertexId,
ang_tol_deg: Option<f64>,
) -> Result<ParallelFaces> {
let mut out = ParallelFaces {
parallel: false,
min_dihedral_ang_deg: BIG_DOUBLE,
max_parallel_face_gap: 0.0,
face1: None,
face2: None,
uv1: Point2::UNDEF,
uv2: Point2::UNDEF,
};
let mut max_parallel_face_gap = 0.0;
let ang_tol_deg = ang_tol_deg.unwrap_or(tol::ANG_TOL_DEG);
let mut pn1 = [Vec3::UNDEF; 2];
let mut pn2 = [Vec3::UNDEF; 2];
let faces = super::faces(b, v)?;
let mut vertexuses = Vec::new();
for &f in &faces {
vertexuses.push(
super::vertexuse_of_face(b, v, Some(f))?
.ok_or_else(|| null("HasParallelFaces: face without vertex use"))?,
);
}
// ii < GetSize()-1 reads past an empty array upstream
let Some(last) = vertexuses.len().checked_sub(1) else {
return Err(null("HasParallelFaces: vertex without faces"));
};
for (ii, &vu1) in vertexuses.iter().enumerate().take(last) {
let face1 = vertexuse_face(b, vu1)?.ok_or_else(|| null("vertex use without face"))?;
let Some(gap1) = vertexuse::vertex_face_gap(k, b, vu1, false)? else {
continue;
};
gap_point_normal(k, face_surface(b, face1)?, gap1.face_uv, &mut pn1)?;
for &vu2 in vertexuses.iter().skip(ii + 1) {
let face2 = vertexuse_face(b, vu2)?.ok_or_else(|| null("vertex use without face"))?;
let Some(gap2) = vertexuse::vertex_face_gap(k, b, vu2, false)? else {
continue;
};
gap_point_normal(k, face_surface(b, face2)?, gap2.face_uv, &mut pn2)?;
// AngleBetween reports 0 for a zero-length normal (status ignored)
let dihedral_ang_rad = pn1[1].angle_between(pn2[1]).unwrap_or(0.0);
let dihedral_ang_deg = rad_to_deg(dihedral_ang_rad);
if out.min_dihedral_ang_deg > dihedral_ang_deg {
out.min_dihedral_ang_deg = dihedral_ang_deg;
}
if dihedral_ang_deg < ang_tol_deg {
out.parallel = true;
let gap_3d = pn1[0].distance(pn2[0]);
if max_parallel_face_gap <= gap_3d {
max_parallel_face_gap = gap_3d;
out.max_parallel_face_gap = gap_3d;
out.face1 = Some(face1);
out.face2 = Some(face2);
out.uv1 = gap1.face_uv;
out.uv2 = gap2.face_uv;
}
}
}
}
Ok(out)
}
/// The outputs of [`calc_corner_from_faces`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FaceCorner {
/// `rbFoundPoint`: the faces define a corner point.
pub found_point: bool,
/// `rdMaxGap3d`: when found, the distance of the vertex point from the corner
/// (`-1` otherwise).
pub max_gap_3d: f64,
/// `rpMaxGapFace`: the face of the largest gap (set by the parallel-face branch).
pub max_gap_face: Option<FaceId>,
/// `rsBestPoint`: when found, the corner point (undefined otherwise).
pub best_point: Point3,
}
/// `SmVertex::CalcCornerFromFaces`: the vertex position that best fits its faces'
/// surfaces. With parallel faces further apart than the approximation tolerance it is
/// the midpoint of their gap points; else, from the first up to three faces with
/// mutually non-parallel normals at the vertex, the surface/surface/surface
/// intersection (three), the refined surface/surface intersection point (two) or the
/// drop of the vertex point to the surface (one).
///
/// # Errors
///
/// [`SolidError::NotFound`] for a dangling id; [`SolidError::InvalidInput`] for a vertex
/// outside a B-rep and where upstream dereferences null; `SM_ERR` (more than three
/// target faces, unreachable upstream); [`SolidError::Unported`] from the evaluations
/// and solves.
#[allow(clippy::too_many_lines)] // one upstream routine
pub fn calc_corner_from_faces(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<FaceCorner> {
let mut out = FaceCorner {
found_point: false,
max_gap_3d: -1.0,
max_gap_face: None,
best_point: Vec3::UNDEF,
};
let mut uv = Point2::UNDEF;
let mut tgt_faces: Vec<FaceId> = Vec::new();
let mut tgt_uvs: Vec<Point2> = Vec::new();
let mut tgt_normals: Vec<Vec3> = Vec::new();
let vertexuses = super::vertexuses(b, v)?;
let faces = super::faces(b, v)?;
if faces.is_empty() {
return Ok(out);
}
let approx_tol_3d =
tol::context_approx_tol_3d(k, Some(&k.context), tol::SmallTopoType::NormalSize);
let parallel = has_parallel_faces(k, b, v, Some(tol::ANG_TOL_DEG))?;
if parallel.parallel && parallel.max_parallel_face_gap > approx_tol_3d {
let f1 = parallel
.face1
.ok_or_else(|| null("parallel faces without face1"))?;
let f2 = parallel
.face2
.ok_or_else(|| null("parallel faces without face2"))?;
let mut pt1 = Vec3::UNDEF;
let mut pt2 = Vec3::UNDEF;
if let Some(p) = ignored(face_surface(b, f1)?.evaluate_point(k, parallel.uv1))? {
pt1 = p;
}
if let Some(p) = ignored(face_surface(b, f2)?.evaluate_point(k, parallel.uv2))? {
pt2 = p;
}
out.found_point = true;
out.best_point = (pt1 + pt2) / 2.0;
// GetVertexuses appends to sVertexuses: every use is listed twice, the resets
// repeat
for &vu in vertexuses.iter().chain(vertexuses.iter()) {
vertexuse::init_vertex_face_gap(b, vu)?;
}
// SmTemporaryChangeValue: the vertex point is the best point for the gap pass
let saved = b.vertex(v)?.point;
b.vertex_mut(v)?.point = out.best_point;
let within = is_within_xsect_tol_3d_of_faces(k, b, v, true);
b.vertex_mut(v)?.point = saved;
let (_, gap, face) = within?;
out.max_gap_3d = gap;
out.max_gap_face = face;
for &vu in vertexuses.iter().chain(vertexuses.iter()) {
vertexuse::init_vertex_face_gap(b, vu)?;
}
// (the destructor restores the point here upstream)
return Ok(out);
}
for &vu in &vertexuses {
let Some(fu) = vertexuse::faceuse(b, vu)? else {
continue;
};
// FALSE = Use GlobalSolve without touching UVTrimCurve (status ignored)
if let Some(p) = ignored(vertexuse::compute_uv_point(k, b, vu, false))? {
uv = p;
}
let face = b
.faceuse(fu)?
.face
.ok_or_else(|| null("face use without face"))?;
let Some(surface) = b.face(face)?.surface.as_ref() else {
continue;
};
let Some(surface_normal) = ignored(surface.evaluate_normal(k, uv, Side::Left, Side::Left))?
else {
continue;
};
if let Some(indx) = tgt_faces.iter().position(|&f| f == face) {
let zone = crate::topology::face::p2::tolerance(b, face)?;
let tol_2d = map_to_2d(k, zone, uv, surface)?;
let close = tgt_uvs.get(indx).is_some_and(|t| t.closer_than(tol_2d, uv));
if close {
continue;
}
}
// switch with fall-through on the number of found targets
let n = tgt_faces.len();
if n <= 2 {
// case 2: skip surfaces with normals parallel to 2nd target
let skip2 = n == 2
&& tgt_normals
.get(1)
.is_some_and(|&t| surface_normal.is_parallel_to(t, 1.0));
// case 1: skip surfaces with normals parallel to 1st target
let skip1 = !skip2
&& n >= 1
&& tgt_normals
.first()
.is_some_and(|&t| surface_normal.is_parallel_to(t, 1.0));
// case 0: arrive here - save the face as next target
if !skip2 && !skip1 {
tgt_faces.push(face);
tgt_uvs.push(uv);
tgt_normals.push(surface_normal);
}
}
// default: the vertexuse goes to sChkVertexuses, which nothing reads
}
match (tgt_faces.as_slice(), tgt_uvs.as_slice()) {
([], _) => {
// SM_DBG_WARN: unexpected number of TgtFaces
return Ok(out);
}
([f0], [uv0]) => {
// Drop Current Vertex->Point to Face->Surface
let surface = face_surface(b, *f0)?;
let point = b.vertex(v)?.point;
let opts = DropPointOptions {
guess_uv: Some(*uv0),
..Default::default()
};
let domain = surface.natural_uv_domain_with_nurb(k)?;
let found_answer =
match ignored(surface.drop_point::<NoObject>(k, point, &domain, &opts))? {
Some(d) => {
uv = d.result_uv;
out.max_gap_3d = d.gap;
d.success
}
None => false,
};
if !found_answer {
return Ok(out);
}
}
([f0, f1], [uv0, uv1]) => {
// Refine Tgt DropPoints to Intersection Curve between two faces
let surface1 = face_surface(b, *f0)?;
let surface2 = face_surface(b, *f1)?;
let ssi = SurfaceIntersector::new(
k,
surface1,
&surface1.natural_uv_domain_with_nurb(k)?,
surface2,
&surface2.natural_uv_domain_with_nurb(k)?,
false,
)?;
// CROSSING here works for Tangent curve cases as well
let r = ssi.refine_point(
k,
*uv0,
*uv1,
IntersectionPointType::Crossing,
None,
None,
false,
);
let (found_answer, uvs) = match ignored(r)? {
Some(Some(uvs)) => (true, uvs),
Some(None) => (false, [Point2::UNDEF; 2]),
None => (true, [Point2::UNDEF; 2]),
};
if !found_answer {
return Ok(out);
}
uv = uvs[0];
}
([f0, f1, f2], [uv0, uv1, uv2]) => {
// find the surf/surf/surf intersection of the 3 face->Surfaces
let d0 = crate::topology::face::p2::uv_domain(b, *f0)?;
let d1 = crate::topology::face::p2::uv_domain(b, *f1)?;
let d2 = crate::topology::face::p2::uv_domain(b, *f2)?;
let s0 = face_surface(b, *f0)?;
let s1 = face_surface(b, *f1)?;
let s2 = face_surface(b, *f2)?;
let sol = ignored(
s0.local_surface_surface_intersect(k, &d0, s1, &d1, s2, &d2, *uv0, *uv1, *uv2),
)?
.flatten();
let Some(s) = sol else {
return Ok(out);
};
uv = s.uv1;
}
_ => {
// SM_DBG_WARN: unexpected number of TgtFaces
return Err(SolidError::sm_err(OP));
}
}
// SM_ASSERT_MSG(bFoundAnswer == TRUE) is compiled out
out.found_point = true;
let f0 = *tgt_faces.first().ok_or_else(|| null("no target face"))?;
if let Some(p) = ignored(face_surface(b, f0)?.evaluate_point(k, uv))? {
out.best_point = p;
}
out.max_gap_3d = out.best_point.distance(b.vertex(v)?.point);
Ok(out)
}
/// `SmVertex::IsOnFaceCorner`: whether the vertex lies within its scaled zero of the
/// corner point its faces define ([`calc_corner_from_faces`], status ignored), or the
/// faces define none. Returns `(on_corner, corner)`.
///
/// # Errors
///
/// Those of [`calc_corner_from_faces`] but its `SM_ERR` (more than three target faces),
/// whose status upstream ignores.
pub fn is_on_face_corner(k: &Kernel, b: &mut Brep, v: VertexId) -> Result<(bool, FaceCorner)> {
// look for a best vertex point position defined by the faces connected to the
// vertex (status ignored: the outputs keep their initial values)
let corner = match calc_corner_from_faces(k, b, v) {
Ok(c) => c,
// the SM_ERR of more than three target faces leaves the initial outputs
Err(SolidError::Upstream { operation: OP, .. }) => FaceCorner {
found_point: false,
max_gap_3d: -1.0,
max_gap_face: None,
best_point: Vec3::UNDEF,
},
Err(e) => return Err(e),
};
// check the gap
let scaled_zero = vertex_scaled_zero(b, v)?;
let rtn = !corner.found_point || tol::in_tol(corner.max_gap_3d, scaled_zero);
// all done
Ok((rtn, corner))
}
/// `sm_AssertTestVertex1(pVertex, &dMaxGap3d, &pMaxGapFace)` as `SmVertex::AssertValid`
/// calls it: `(passed, max_gap_3d, max_gap_face)`. Passed when every vertex/face gap is
/// below the B-rep's intersection tolerance ([`is_within_xsect_tol_3d_of_faces`]) and,
/// only then, the vertex lies on its faces' corner ([`is_on_face_corner`]).
///
/// # Errors
///
/// Those of [`is_within_xsect_tol_3d_of_faces`] and [`is_on_face_corner`].
pub fn assert_test_vertex1(
k: &Kernel,
b: &mut Brep,
v: VertexId,
) -> Result<(bool, f64, Option<FaceId>)> {
// Vertex/Face gaps <= XSectTol3d
let (mut rtn, max_gap_3d, max_gap_face) = is_within_xsect_tol_3d_of_faces(k, b, v, false)?;
// bRtn == FALSE when Vertex Point is more than ScaledZero from best corner pos
// defined by connected faces
if rtn {
rtn &= is_on_face_corner(k, b, v)?.0;
}
// all done
Ok((rtn, max_gap_3d, max_gap_face))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitive::api_primitives::create_box;
/// A 1 x 2 x 3 box and its vertex at the origin corner.
fn box_corner() -> (Kernel, Brep, VertexId) {
let mut k = Kernel::default();
let b = create_box(&mut k, Vec3::ZERO, 1.0, 2.0, 3.0).unwrap();
let v = b
.vertices()
.ids()
.find(|&v| b.vertex(v).unwrap().point.distance(Vec3::ZERO) == 0.0)
.unwrap();
(k, b, v)
}
/// Moves the vertex by `d` and clears its uses' gap caches.
fn move_vertex(b: &mut Brep, v: VertexId, d: Vec3) {
let p = b.vertex(v).unwrap().point;
b.vertex_mut(v).unwrap().point = p + d;
for vu in super::super::vertexuses(b, v).unwrap() {
vertexuse::init_vertex_face_gap(b, vu).unwrap();
}
}
#[test]
fn box_corner_passes() {
let (k, mut b, v) = box_corner();
let (within, gap, _) = is_within_xsect_tol_3d_of_faces(&k, &mut b, v, false).unwrap();
assert!(within);
assert_eq!(gap, 0.0);
let (on, corner) = is_on_face_corner(&k, &mut b, v).unwrap();
assert!(on);
assert!(corner.found_point);
assert!(corner.max_gap_3d.abs() <= vertex_scaled_zero(&b, v).unwrap());
let (passed, gap, _) = assert_test_vertex1(&k, &mut b, v).unwrap();
assert!(passed);
assert_eq!(gap, 0.0);
let p = has_parallel_faces(&k, &mut b, v, None).unwrap();
assert!(!p.parallel);
assert!((p.min_dihedral_ang_deg - 90.0).abs() < 1e-9);
}
#[test]
fn vertex_off_its_faces_fails_the_gap_test() {
let (k, mut b, v) = box_corner();
// 1e-3 off the x = 0 face: beyond XSectTol3d = 2 * the B-rep tolerance
move_vertex(&mut b, v, Vec3::new(1.0e-3, 0.0, 0.0));
let xsect = brep_xsect_tol_3d(&k, &b, v).unwrap();
assert!(xsect < 1.0e-3);
let (passed, gap, face) = assert_test_vertex1(&k, &mut b, v).unwrap();
assert!(!passed);
assert!((gap - 1.0e-3).abs() <= 1e-15, "{gap}");
assert!(face.is_some());
}
#[test]
fn vertex_within_xsect_tol_but_off_the_corner_fails() {
let (k, mut b, v) = box_corner();
// 1e-7: below XSectTol3d, above the vertex's scaled zero
move_vertex(&mut b, v, Vec3::new(0.0, 1.0e-7, 0.0));
let (within, gap, _) = is_within_xsect_tol_3d_of_faces(&k, &mut b, v, false).unwrap();
assert!(within);
assert!((gap - 1.0e-7).abs() <= 1e-18, "{gap}");
let (on, corner) = is_on_face_corner(&k, &mut b, v).unwrap();
assert!(!on);
assert!(corner.found_point);
assert!(
(corner.max_gap_3d - 1.0e-7).abs() <= 1e-15,
"{}",
corner.max_gap_3d
);
assert!(corner.best_point.distance(Vec3::ZERO) <= 1e-12);
let (passed, gap, _) = assert_test_vertex1(&k, &mut b, v).unwrap();
assert!(!passed);
assert!((gap - 1.0e-7).abs() <= 1e-18, "{gap}");
}
}
@@ -3,9 +3,6 @@
//!
//! Valid primitive bodies must report no failure at level 2, and bodies broken by a
//! direct edit must report the exact upstream report lines and `is_valid() == false`.
//! Checks that need a dependency seam not yet ported (the WP4 package of
//! #1419) are `#[ignore]`d with the seam named, and a live test pins that they still
//! return `Unported` so the ignore is removed when the seam is wired.
#![allow(
clippy::unwrap_used,
@@ -17,7 +14,7 @@
use solidcore::primitive::api_primitives as ap;
use solidcore::topology::brep::validation::{check_brep, BrepCheck, CheckLevel};
use solidcore::topology::Brep;
use solidcore::{Kernel, SolidError, Vec3};
use solidcore::{Kernel, Vec3};
fn origin() -> Vec3 {
Vec3::new(0.0, 0.0, 0.0)
@@ -39,11 +36,9 @@ fn check(k: &mut Kernel, b: &mut Brep) -> BrepCheck {
check_brep(k, b, CheckLevel::Level2).unwrap()
}
/// Pending: every non-empty body reaches `sm_AssertTestVertex1` (WP4, every level), and
/// level 2 reaches `sm_AssertTestFace32/33/38` and `sm_AssertTestEdge13` (WP4). The
/// WP2 helpers (#1472) and `SmSurface::AssertValid` (package 3a, #1460) are wired.
/// Every non-empty body reaches `sm_AssertTestVertex1` and, at level 2,
/// `sm_AssertTestFace32/33/38` and `sm_AssertTestEdge13` (WP4, #1478).
#[test]
#[ignore = "pending #1419 WP4 seams (#1478): sm_AssertTestVertex1, sm_AssertTestFace32/33/38, sm_AssertTestEdge13"]
fn valid_bodies_report_no_failures_at_level_2() {
let mut k = Kernel::default();
for (name, mut b) in bodies(&mut k) {
@@ -53,22 +48,6 @@ fn valid_bodies_report_no_failures_at_level_2() {
}
}
/// The pending cases above still stop at a seam; when this fails, a seam was wired:
/// drop the `#[ignore]` above.
#[test]
fn valid_bodies_are_pending_on_seams() {
let mut k = Kernel::default();
for (name, mut b) in bodies(&mut k) {
match check_brep(&mut k, &mut b, CheckLevel::Level2) {
Err(SolidError::Unported { unit, .. }) => assert!(
["topology.vertex", "topology.edge", "topology.face_p4"].contains(&unit),
"{name}: {unit}"
),
other => panic!("{name}: seam wired? {:?}", other.map(|c| c.lines())),
}
}
}
fn box_body(k: &mut Kernel) -> Brep {
ap::create_box(k, origin(), 1.0, 2.0, 3.0).unwrap()
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+98
View File
@@ -39706,6 +39706,20 @@
"lines": 4,
"name": "SmEdge::IsKindOf",
"usr": "c:@S@SmEdge@F@IsKindOf#L#1"
},
{
"file": "source/SMLib/src/SmEdge.cpp",
"line": 5460,
"lines": 51,
"name": "SmEdge::IsOnFaceEdgeForm",
"usr": "c:@S@SmEdge@F@IsOnFaceEdgeForm#*d#1"
},
{
"file": "source/SMLib/src/SmEdge.cpp",
"line": 7046,
"lines": 11,
"name": "sm_AssertTestEdge13",
"usr": "c:@F@sm_AssertTestEdge13#*1$@S@SmEdge#*d#"
}
],
"module": "topology",
@@ -40789,6 +40803,55 @@
"lines": 55,
"name": "sm_AssertTestFace37",
"usr": "c:@F@sm_AssertTestFace37#*1$@S@SmFace#"
},
{
"file": "source/SMLib/src/SmFace.cpp",
"line": 16646,
"lines": 83,
"name": "SmFace::HasCoincidentVertices",
"usr": "c:@S@SmFace@F@HasCoincidentVertices#*$@S@SmTArray>#*$@S@SmVertex#S0_#1"
},
{
"file": "source/SMLib/src/SmFace.cpp",
"line": 16756,
"lines": 40,
"name": "sm_FindCoincidentVerts",
"usr": "c:@F@sm_FindCoincidentVerts#*$@S@SmVertex#&$@S@SmTArray>#S0_#S1_#S1_#"
},
{
"file": "source/SMLib/src/SmFace.cpp",
"line": 16807,
"lines": 103,
"name": "SmFace::HasCoincidentEdges",
"usr": "c:@S@SmFace@F@HasCoincidentEdges#*$@S@SmTArray>#*$@S@SmEdge#S0_#*$@S@SmTArray>#*$@S@SmVertex#S2_#1"
},
{
"file": "source/SMLib/src/SmFace.cpp",
"line": 20046,
"lines": 51,
"name": "sm_DoLoopsIntersectInUV",
"usr": "c:SmFace.cpp@F@sm_DoLoopsIntersectInUV#*1$@S@SmLoop#S0_#&1$@S@SmSurface#"
},
{
"file": "source/SMLib/src/SmFace.cpp",
"line": 20128,
"lines": 36,
"name": "sm_AssertTestFace38",
"usr": "c:@F@sm_AssertTestFace38#*1$@S@SmFace#"
},
{
"file": "source/SMLib/src/SmFace.cpp",
"line": 20341,
"lines": 16,
"name": "sm_AssertTestFace33",
"usr": "c:@F@sm_AssertTestFace33#*1$@S@SmFace#*$@S@SmTArray>#*$@S@SmEdge#S1_#*$@S@SmTArray>#*$@S@SmVertex#S3_#"
},
{
"file": "source/SMLib/src/SmFace.cpp",
"line": 20501,
"lines": 14,
"name": "sm_AssertTestFace32",
"usr": "c:@F@sm_AssertTestFace32#*1$@S@SmFace#*$@S@SmTArray>#*$@S@SmVertex#S1_#"
}
],
"module": "topology",
@@ -43887,6 +43950,41 @@
"lines": 262,
"name": "SmVertex::CheckPointers",
"usr": "c:@S@SmVertex@F@CheckPointers#1"
},
{
"file": "source/SMLib/src/SmVertex.cpp",
"line": 1223,
"lines": 142,
"name": "SmVertex::HasParallelFaces",
"usr": "c:@S@SmVertex@F@HasParallelFaces#1"
},
{
"file": "source/SMLib/src/SmVertex.cpp",
"line": 1720,
"lines": 82,
"name": "SmVertex::IsWithinXSectTol3dOfFaces",
"usr": "c:@S@SmVertex@F@IsWithinXSectTol3dOfFaces#*d#**$@S@SmFace#I#1"
},
{
"file": "source/SMLib/src/SmVertex.cpp",
"line": 1904,
"lines": 53,
"name": "SmVertex::IsOnFaceCorner",
"usr": "c:@S@SmVertex@F@IsOnFaceCorner#*I#*d#*$@S@SmPoint3d#1"
},
{
"file": "source/SMLib/src/SmVertex.cpp",
"line": 1973,
"lines": 330,
"name": "SmVertex::CalcCornerFromFaces",
"usr": "c:@S@SmVertex@F@CalcCornerFromFaces#&I#&d#&*$@S@SmFace#&$@S@SmPoint3d#1"
},
{
"file": "source/SMLib/src/SmVertex.cpp",
"line": 2551,
"lines": 29,
"name": "sm_AssertTestVertex1",
"usr": "c:@F@sm_AssertTestVertex1#*1$@S@SmVertex#*d#**$@S@SmFace#*I#S1_#*$@S@SmPoint3d#"
}
],
"module": "topology",
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
// Unit surface.surface_assert_valid. Builds a real SmBSplineSurface, SmPlane, SmCylinder,
// SmCone, SmSphere, SmTorus or SmSurfOfRevolution from plain data and runs the pinned
// virtual SmSurface::AssertValid on it, returning its result and every report.
// SmCone, SmSphere, SmTorus, SmSurfOfRevolution or SmSurfOfExtrusion from plain data and
// runs the pinned virtual SmSurface::AssertValid on it, returning its result and every
// report.
//
// Input block: `kind level make` then, by kind,
// 0 B-spline: p q nu nv rational knots_u[nu+p+1] knots_v[nv+q+1] pts[nu*nv*4] (row i = u)
@@ -11,6 +12,9 @@
// 4 sphere: O X Y domain[4] r swap inside
// 5 torus: O X Y domain[4] major minor swap inside
// 6 revolution: O X Y domain[4] swap, generator: p n rational pts[n*4] knots[n+p+1]
// 7 extrusion: origin[3] vec[3] domain[4] swap, generator: p n rational pts[n*4]
// knots[n+p+1], then `stale cp[3] cv[3]` (stale != 0: SetCurvePoint(cp)
// and SetCurveVec(cv) after construction, a stale cached plane or line)
// `make` calls MakeNurb before the check. Output: `result n` then per report
// `type list test assert_type ok reporter owner other owner_type other_type tol value
// dist3d name message message_for_file owner_string other_string` (texts as
@@ -27,6 +31,7 @@
#include <SmCylinder.h>
#include <SmPlane.h>
#include <SmSphere.h>
#include <SmSurfOfExtrusion.h>
#include <SmSurfOfRevolution.h>
#include <SmTol.h>
#include <SmTorus.h>
@@ -53,6 +58,20 @@ struct AvRevolution : SmSurfOfRevolution
}
};
struct AvExtrusion : SmSurfOfExtrusion
{
AvExtrusion(SmBSplineCurve* gen, const SmPoint3d& o, const SmVector3d& v, const SmExtent2d& d,
SmBoolean swap)
: SmSurfOfExtrusion(gen, o, v, d, swap)
{
}
void stale(const SmPoint3d& cp, const SmVector3d& cv)
{
SetCurvePoint(cp);
SetCurveVec(cv);
}
};
struct AvRd
{
const double* in;
@@ -231,6 +250,34 @@ SC_EXPORT int32_t sc_surface_surface_assert_valid_eval(const double* input, int6
surface = r;
}
}
else if (kind == 7)
{
SmPoint3d o = rd.vec();
SmVector3d v = rd.vec();
SmExtent2d d = rd.dom();
SmBoolean swap = rd.b();
int64_t p = rd.idx(), cn = rd.idx();
bool rational = rd.num() != 0.0;
if (p < 1 || cn < 2 || cn > 64)
return SC_ERR_ARG;
std::vector<gw_CPOINT> pts(cn);
for (auto& c : pts)
c = av_point(rd, rational);
std::vector<double> k(cn + p + 1);
for (auto& x : k)
x = rd.num();
bool stale = rd.num() != 0.0;
SmPoint3d cp = rd.vec();
SmVector3d cv = rd.vec();
if (!rd.ok)
return SC_ERR_ARG;
SmBSplineCurve* gen = new (ctx)
SmBSplineCurve(3, (ULONG)(cn - 1), (ULONG)(cn + p), (short)p, pts.data(), k.data());
AvExtrusion* e = new (ctx) AvExtrusion(gen, o, v, d, swap);
if (stale)
e->stale(cp, cv);
surface = e;
}
else
return SC_ERR_ARG;
if (!rd.ok || rd.at != n)
+36 -3
View File
@@ -13,6 +13,14 @@
// Defined (not declared) in SmAssertArray.cpp.
TCHAR const* SM_GetObjTypeString(SM_TYPE lExeClassType);
// The level-2 AssertValid tests, defined (not declared) in SmFace.cpp, SmEdge.cpp and
// SmVertex.cpp (their default arguments are NULL).
SmBoolean sm_AssertTestFace32(const SmFace*, SmTArray<SmVertex*>*, SmTArray<SmVertex*>*);
SmBoolean sm_AssertTestFace33(const SmFace*, SmTArray<SmEdge*>*, SmTArray<SmEdge*>*, SmTArray<SmVertex*>*,
SmTArray<SmVertex*>*);
SmBoolean sm_AssertTestFace38(const SmFace*);
SmBoolean sm_AssertTestEdge13(const SmEdge*, double*);
SmBoolean sm_AssertTestVertex1(const SmVertex*, double*, SmFace**, SmBoolean*, double*, SmPoint3d*);
namespace {
using Id = std::array<int64_t, 2>;
@@ -147,10 +155,13 @@ SmBrep* primitive(const double* in, int64_t n, SmStatus& status) {
} // namespace
// op 0: AssertValid(&reports, level, walk); op 1: the capi sequence at `level` with
// the walk. args: level, walk, mutation kind, index, value.
// the walk; op 2: the level-2 tests one by one, as SmFace/SmEdge/SmVertex::AssertValid
// call them: per face sm_AssertTestFace32, 33 and 38 (-1 without surface), then per
// edge sm_AssertTestEdge13 (passed, max gap), then per vertex sm_AssertTestVertex1
// (passed, max gap 3d, max gap face index or -1), after the three counts. args: level, walk, mutation kind, index, value.
SC_EXPORT int32_t sc_topology_assert_valid_eval(int32_t op, const char* path, const double* input, int64_t n,
const double* args, int64_t na, double* out, int64_t cap, int64_t* len) {
if (op < 0 || op > 1 || !input || n < 1 || !args || na != 5) return SC_ERR_ARG;
if (op < 0 || op > 2 || !input || n < 1 || !args || na != 5) return SC_ERR_ARG;
SC_TRY
Rd rd{input, 0};
SmStatus status = SM_SUCCESS;
@@ -168,7 +179,29 @@ SC_EXPORT int32_t sc_topology_assert_valid_eval(int32_t op, const char* path, co
{
Mutation mutation(g, (int)args[2], (size_t)args[3], args[4]);
const SmAssertTestLevel level = (SmAssertTestLevel)(int)args[0];
if (op == 0) {
if (op == 2) {
values.insert(values.end(), {(double)g.faces.size(), (double)g.edges.size(), (double)g.vertices.size()});
for (SmFace* f : g.faces) {
values.push_back(flg(sm_AssertTestFace32(f, NULL, NULL)));
values.push_back(flg(sm_AssertTestFace33(f, NULL, NULL, NULL, NULL)));
values.push_back(f->GetSurface() ? flg(sm_AssertTestFace38(f)) : -1.0);
}
for (SmEdge* e : g.edges) {
double gap = 0.0;
values.push_back(flg(sm_AssertTestEdge13(e, &gap)));
values.push_back(gap);
}
for (SmVertex* v : g.vertices) {
double gap = 0.0;
SmFace* face = NULL;
values.push_back(flg(sm_AssertTestVertex1(v, &gap, &face, NULL, NULL, NULL)));
values.push_back(gap);
double at = -1;
for (size_t i = 0; i < g.faces.size(); i++)
if (g.faces[i] == face) at = (double)i;
values.push_back(at);
}
} else if (op == 0) {
SmAssertArray reports;
values.push_back(flg(built.brep->AssertValid(&reports, level, args[1] != 0 ? SM_WALK : SM_NO_WALK)));
put_reports(values, g, built.brep, reports);
+36
View File
@@ -850,6 +850,7 @@ enum Op : int32_t
OP_BLEND = 23,
OP_ODE = 24,
OP_SET_INTERVAL = 25,
OP_EDGE_FORM = 26,
};
// The type codes `IsKindOf` is asked about.
@@ -1621,6 +1622,41 @@ void run_brep_op(int32_t op, Rd& rd, const char* path, std::vector<double>& o)
o.push_back(dydx[k]);
break;
}
case OP_EDGE_FORM:
{
// `dx dy dz`: translate the edge curve first (`SmCurve::Transform`, skipped for
// a zero move), then `IsOnFaceEdgeForm(&dMaxGap3d)` (Edge AssertValid test 13)
SmVector3d d;
d.x = rd.num();
d.y = rd.num();
d.z = rd.num();
SmTArray<SmFace*> fs;
e->GetFaces(fs);
// GetMaxEdgeFaceGap's gap calculation dereferences the edge curve and the face
// surface
bool ok = fs.GetSize() == 0 || e->GetCurve() != nullptr;
for (ULONG k = 0; k < fs.GetSize(); k++)
if (fs[k] == nullptr || fs[k]->GetSurface() == nullptr)
ok = false;
if (!ok)
{
o.push_back(ST_NULL_DEREF);
break;
}
double moved = 0.0;
if ((d.x != 0.0 || d.y != 0.0 || d.z != 0.0) && e->GetCurve() != nullptr)
{
SmAxis2Placement place;
place.Translate(d);
moved = e->GetCurve()->Transform(place) == SM_SUCCESS ? 0.0 : 1.0;
}
o.push_back(moved);
double gap = -7.0;
SmBoolean r = e->IsOnFaceEdgeForm(&gap);
o.push_back(flg(r));
o.push_back(gap);
break;
}
case OP_SET_INTERVAL:
{
double a = rd.num();
+94
View File
@@ -83,6 +83,7 @@ enum Op : int32_t
OP_SET_TOLERANCE = 14,
OP_REFRESH_TOLERANCE = 15,
OP_EXACT = 16,
OP_CORNER = 17,
};
// Status of an op the shim refuses because upstream would dereference null.
@@ -1177,6 +1178,35 @@ void put_gap(std::vector<double>& o, Graph& g, const SmGap* p)
o.push_back(-3.0);
}
// sm_AssertTestVertex1 and its callees: GetBrep()->GetTolerance() (IsWithinXSectTol3dOfFaces),
// the face gaps (CalcVertexFaceGap / ComputeUVPoint), GetFaceuse()->GetFace()->GetSurface()
// (HasParallelFaces, CalcCornerFromFaces) are dereferenced.
bool ok_corner(SmVertex* v)
{
if (v->GetBrep() == nullptr)
return false;
for (SmVertexuse* p : uses_of(v))
{
if (!ok_faceuse(p) || !ok_face_gap(p))
return false;
SmFaceuse* fu = p->GetFaceuse();
if (fu != nullptr && (fu->GetFace() == nullptr || fu->GetFace()->GetSurface() == nullptr))
return false;
}
return true;
}
void put_corner(std::vector<double>& o, Graph& g, SmBoolean on, SmBoolean found, double gap, const SmPoint3d& best)
{
o.push_back(flg(on));
o.push_back(flg(found));
o.push_back(gap);
put_vec(o, best);
}
void put_gap_states(std::vector<double>& o, Graph& g);
void run_brep_op(int32_t op, Rd& rd, const char* path, std::vector<double>& o)
{
SmStatus bst;
@@ -1491,6 +1521,70 @@ void run_brep_op(int32_t op, Rd& rd, const char* path, std::vector<double>& o)
v->SetPolyVertex(NULL);
break;
}
case OP_CORNER:
{
bool has_move = rd.flag();
SmPoint3d d = rd.vec();
if (has_move)
v->SetPoint(v->GetPoint() + d);
if (!ok_corner(v))
{
o.push_back(ST_NULL_DEREF);
break;
}
o.push_back(0.0);
// sm_AssertTestVertex1(v, &dMaxGap3d, &pMaxGapFace) as SmVertex::AssertValid calls it
double max_gap = -7.0;
SmFace* max_face = (SmFace*)0x1;
SmBoolean within = v->IsWithinXSectTol3dOfFaces(&max_gap, &max_face);
SmBoolean passed = within;
o.push_back(flg(within));
o.push_back(max_gap);
o.push_back((double)g.find(3, max_face));
if (within)
{
SmBoolean found = FALSE;
double gap = -7.0;
SmPoint3d best;
SmBoolean on = v->IsOnFaceCorner(&found, &gap, &best);
passed &= on;
put_corner(o, g, on, found, gap, best);
}
o.push_back(flg(passed));
// then IsOnFaceCorner on its own
{
SmBoolean found = FALSE;
double gap = -7.0;
SmPoint3d best;
SmBoolean on = v->IsOnFaceCorner(&found, &gap, &best);
put_corner(o, g, on, found, gap, best);
}
// then HasParallelFaces (an empty face list is read past upstream)
SmTArray<SmFace*> fs;
v->GetFaces(fs);
if (fs.GetSize() == 0)
o.push_back(-1.0);
else
{
double tol = SM_ANG_TOL_DEG, min_ang = -7.0, max_par = -7.0;
SmFace *f1 = (SmFace*)0x1, *f2 = (SmFace*)0x1;
SmPoint2d uv1, uv2;
SmBoolean par = v->HasParallelFaces(&tol, &min_ang, &max_par, &f1, &f2, &uv1, &uv2);
o.push_back(flg(par));
o.push_back(min_ang);
o.push_back(max_par);
o.push_back((double)g.find(3, f1));
o.push_back((double)g.find(3, f2));
}
// and IsWithinXSectTol3dOfFaces forcing every gap
double fgap = -7.0;
SmFace* fface = (SmFace*)0x1;
o.push_back(flg(v->IsWithinXSectTol3dOfFaces(&fgap, &fface, TRUE)));
o.push_back(fgap);
o.push_back((double)g.find(3, fface));
put_gap_states(o, g);
break;
}
default:
break;
}
@@ -0,0 +1,28 @@
Updates: plan/findings/topology.vertex--moved-seam-pole-vertex-face-gap.md
# Resolved: the imported face surfaces had no face owner (2026-10-29)
Not a kernel divergence. The `topology.vertex` runner builds the Rust B-rep of an
`.smb` source from the shim's export, and that import added each face's surface without
an owner. Upstream `SmFace::SetSurface` owns it (`GetOwner() == face`).
`SmSurface::LocalPointSolve_1` takes its loose tolerance from that owner
(`SmTol::GetZoneTol3d(GetOwner())`, the face tolerance, here `1e-5`). Without an owner it
uses `SM_EFF_ZERO_SQRT * 100` (`1e-4`). After a solve converges on a domain boundary, it
jumps the seam only when the gap exceeds the loose tolerance.
On `corner_sphere_v0_off1em5` (gdb on both sides), the vertex use whose guess sits on the
seam at `u = umax` converged on that boundary with gap `1.0198e-5`:
- C++ (loose `1e-5`): `IsOnSeam` holds and the solve jumps to `u = 0`.
`ComputeUVPoint`'s half-domain check rejects that, `GlobalPointSolve` runs, and the gap
is `1.1007e-5`.
- Rust (loose `1e-4`): no jump, so the solution at `u = umax` is kept and the gap is
`1.0198e-5`.
The torus cases are the same seam jump. Every step before the solve matched.
Fix (harness only): the import owns each face surface under the arena identity of the
runner's conventions, and `add_face_record` records the face tolerance on it. The 9
`corner_{sphere,torus,toruswvy}_v{0,1,2}_off1em5` entries are removed from
`crates/difftest/known_mismatches/topology.vertex.toml`. No C++ state reaches the Rust
side beyond the existing export import.
@@ -0,0 +1,27 @@
# topology.vertex: vertex/face gap of a sphere or torus vertex moved 1e-5 off its faces (2026-10-29)
- **Unit:** found by the `Corner` op of `topology.vertex` (PR #1478, #1419 WP4a:
`sm_AssertTestVertex1`). The differing value is the one the existing gap code
computes (`vertexuse::vertex_face_gap`, via `SmVertexuse::GetVertexFaceGap` and
`ComputeUVPoint`), not one of the new corner functions.
- **Cases (9):** `corner_{sphere,torus,toruswvy}_v{0,1,2}_off1em5` in
`fixtures/topology.vertex/corner.json`. Upstream `sphere.smb`, `torus.smb` and
`toruswvy.smb` vertices 0-2 (pole and seam vertices) are moved by `(1e-5, -1e-5, 2e-6)`
with `SmVertex::SetPoint`, and then `IsWithinXSectTol3dOfFaces` runs.
- **Values (`state[2]`, the max vertex/face gap; `state[23]` repeats it):**
- sphere: C++ `1.1007145606666338e-5`, Rust `1.0198037066258565e-5`;
- torus: C++ `1.4142135907336496e-5`, Rust `1.4282856856820645e-5`;
- toruswvy: C++ `1.4104227772077376e-5`, Rust `1.4282856860970198e-5`.
The pass/fail flags and every other value agree. The 1e-3 and 1e-7 moves of the same
vertices also agree.
- **Observation:** on the tori the Rust gap is exactly the length of the move
(`|d| = 1.42829e-5`). The C++ gap is close to its tangential part only (`sqrt(2) *
1e-5`). The Rust side seems to measure from the UV point found before the move, where
the C++ re-solves it.
- **Suspected cause:** the UV point or lazily created UV trim used for a seam or pole
vertex use after `SetPoint`. This may be the same family as
`plan/findings/topology.assert_valid--sphere-moved-pole-lazy-trim.md`. Not resolved.
- **Status:** the 9 cases are listed in
`crates/difftest/known_mismatches/topology.vertex.toml` (a Rust divergence, no
`cpp_defect`).
+3 -3
View File
@@ -1,7 +1,7 @@
id = "surface.surface_assert_valid"
upstream = ["source/SMLib/src/SmSurface.cpp", "source/SMLib/src/SmBSplineSurface.cpp", "source/SMLib/src/SmNurbsSrf.cpp", "source/SMLib/src/SmPlane.cpp", "source/SMLib/src/SmCone.cpp", "source/SMLib/src/SmCylinder.cpp", "source/SMLib/src/SmSphere.cpp", "source/SMLib/src/SmTorus.cpp", "source/SMLib/src/SmSurfOfRevolution.cpp", "source/SMLib/src/SmAxis2Placement.cpp", "source/SMLib/src/SmPointSet.cpp"]
upstream = ["source/SMLib/src/SmSurface.cpp", "source/SMLib/src/SmBSplineSurface.cpp", "source/SMLib/src/SmNurbsSrf.cpp", "source/SMLib/src/SmPlane.cpp", "source/SMLib/src/SmCone.cpp", "source/SMLib/src/SmCylinder.cpp", "source/SMLib/src/SmSphere.cpp", "source/SMLib/src/SmTorus.cpp", "source/SMLib/src/SmSurfOfRevolution.cpp", "source/SMLib/src/SmSurfOfExtrusion.cpp", "source/SMLib/src/SmAxis2Placement.cpp", "source/SMLib/src/SmPointSet.cpp"]
rust_module = "surface::surface::assert_valid"
depends_on = ["geom.assert_array", "surface.surface_p1", "surface.surface_p2", "surface.surface_p3", "surface.surface_p6", "surface.surface_p7", "surface.surface_p9", "nurbs.b_spline_surface_p1", "surface.plane_p1", "surface.cone_p1", "surface.sphere", "surface.torus", "surface.surf_of_revolution_p1"]
depends_on = ["geom.assert_array", "surface.surface_p1", "surface.surface_p2", "surface.surface_p3", "surface.surface_p6", "surface.surface_p7", "surface.surface_p9", "nurbs.b_spline_surface_p1", "surface.plane_p1", "surface.cone_p1", "surface.sphere", "surface.torus", "surface.surf_of_revolution_p1", "surface.surf_of_extrusion_p1"]
soft_depends_on = []
est_lines = 1100
layer = 31
@@ -9,4 +9,4 @@ tests = []
status = "green"
claimed_by = ""
claimed_at = ""
notes = "PR #1460, issue #1419 (WP3a): SmSurface::AssertValid (virtual) and the SmBSplineSurface, SmPlane, SmCone, SmCylinder, SmSphere, SmTorus, SmSurfOfRevolution and SmAxis2Placement checks, with HasKnotMultiplicityGreaterThanDegree, HasInternalPole, HasDegenerateControlPointRow, HasSameParameterization (curve) and SmPointGrid::SetUpMonotonic (none of them in harness/callgraph). Pinned upstream behaviour (no fork torus flags). SmCurveBoundedSurface, SmSrfInVolume, SmOffsetSurface and SmSurfOfExtrusion checks return Unported (WP3b). GREEN: 1500 generated cases, 1428 pass, 0 deferred, 72 rejected by the FFI guards, max_rel_err 0. B-spline test 1 (knot multiplicity above the degree) is covered by the solidcore unit tests only: plan/findings/surface.surface_assert_valid--iso-curve-at-overfull-knot.md. Regression tests: the tests module of crates/solidcore/src/surface/surface/assert_valid.rs."
notes = "PR #1460, issue #1419 (WP3a): SmSurface::AssertValid (virtual) and the SmBSplineSurface, SmPlane, SmCone, SmCylinder, SmSphere, SmTorus, SmSurfOfRevolution and SmAxis2Placement checks, with HasKnotMultiplicityGreaterThanDegree, HasInternalPole, HasDegenerateControlPointRow, HasSameParameterization (curve) and SmPointGrid::SetUpMonotonic (none of them in harness/callgraph). Pinned upstream behaviour (no fork torus flags). SmCurveBoundedSurface, SmSrfInVolume and SmOffsetSurface checks return Unported (WP3b). SmSurfOfExtrusion::AssertValid ported (WP3b, its own PR): 170 named fixture cases (fixtures/surface.surface_assert_valid/extrusion.json, from fixer-5's independent port: line, perpendicular, independent, coplanar and non-planar generators with and without a tangent along the extrusion, at three scales, levels, make and swap, and test 7/8 corruptions) and 400 generated extrusion cases ext0000.. on their own seed stream (main's gen cases unchanged). GATE COUNTS: TODO from the leader's run. GREEN: 1500 generated cases, 1428 pass, 0 deferred, 72 rejected by the FFI guards, max_rel_err 0. B-spline test 1 (knot multiplicity above the degree) is covered by the solidcore unit tests only: plan/findings/surface.surface_assert_valid--iso-curve-at-overfull-knot.md. Regression tests: the tests module of crates/solidcore/src/surface/surface/assert_valid.rs."
+2 -2
View File
@@ -6,7 +6,7 @@ soft_depends_on = []
est_lines = 2700
layer = 36
tests = ["prog_test:my_test_topology"]
status = "partial"
status = "green"
claimed_by = "Claude Code"
claimed_at = "2026-10-29"
notes = "PR #1459, issue #1419 (WP1): SmBrep::AssertValid whole-body walk (Brep, Region, Shell, Face, Faceuse, Loop, Loopuse, Edge, Edgeuse, Vertex, Vertexuse level-0 bodies; SmTopology/SmOwningTopology/SmSAGObject base checks; validate-only tolerance pass; SmCurve::AssertValid dispatch), FormatLogMessage, SM_GetObjTypeString, public check_brep -> BrepCheck (capi smlib_brep_check). PARTIAL: 532 cases (516 fixtures: 14 primitive/constructed bodies + 14 upstream .smb files at levels 0-4, no-walk and capi levels 0-2, 10 mutation kinds on 6 bodies; 16 generated), 154 pass, 378 deferred (all on WP4 seams of #1478 except 8: topology.face_p4 201 sm_AssertTestFace32; topology.vertex 91 sm_AssertTestVertex1; topology.edge 78 sm_AssertTestEdge13; surface.surf_of_extrusion_p1 8 SmSurfOfExtrusion::AssertValid), 0 known mismatches, max_rel_err 0. sphere_mutation6_capi fixed: the legacy MakeFace copy now shares the surface's cache slot, so the trimmed cache built during creation persists as upstream's does (plan/findings/topology.assert_valid--sphere-moved-pole-lazy-trim--addendum-2026-10-29-fixed.md). Seams in assert_valid/seams.rs: WP2 helpers wired to #1472, SmSurface::AssertValid wired to #1460; WP3b curve checks and WP4 tests remain stubs (UNPORTED tags; sm_AssertTestFace32/33/37/38, sm_AssertTestEdge13 and sm_AssertTestVertex1 are not in units.json and are tagged with the topology units WP2/WP4 extend; SmSurface::AssertValid wired to surface.surface_assert_valid (PR #1460); SmPlane placement reports are named by the surface key on both sides). Regression tests: crates/solidcore/tests/issue_1419_brep_check.rs."
notes = "GREEN: difftest 828 cases, 819 pass, 0 failed, 0 deferred, 9 rejected (WP4a seams of #1478 plus the extrusion AssertValid; harness owner/tolerance fixes). PR #1459, issue #1419 (WP1): SmBrep::AssertValid whole-body walk (Brep, Region, Shell, Face, Faceuse, Loop, Loopuse, Edge, Edgeuse, Vertex, Vertexuse level-0 bodies; SmTopology/SmOwningTopology/SmSAGObject base checks; validate-only tolerance pass; SmCurve::AssertValid dispatch), FormatLogMessage, SM_GetObjTypeString, public check_brep -> BrepCheck (capi smlib_brep_check). PARTIAL: 532 cases (516 fixtures: 14 primitive/constructed bodies + 14 upstream .smb files at levels 0-4, no-walk and capi levels 0-2, 10 mutation kinds on 6 bodies; 16 generated), 154 pass, 378 deferred (all on WP4 seams of #1478 except 8: topology.face_p4 201 sm_AssertTestFace32; topology.vertex 91 sm_AssertTestVertex1; topology.edge 78 sm_AssertTestEdge13; surface.surf_of_extrusion_p1 8 SmSurfOfExtrusion::AssertValid), 0 known mismatches, max_rel_err 0. sphere_mutation6_capi fixed: the legacy MakeFace copy now shares the surface's cache slot, so the trimmed cache built during creation persists as upstream's does (plan/findings/topology.assert_valid--sphere-moved-pole-lazy-trim--addendum-2026-10-29-fixed.md). Seams in assert_valid/seams.rs: WP2 helpers wired to #1472, SmSurface::AssertValid wired to #1460; WP3b curve checks and WP4 tests remain stubs (UNPORTED tags; sm_AssertTestFace32/33/37/38, sm_AssertTestEdge13 and sm_AssertTestVertex1 are not in units.json and are tagged with the topology units WP2/WP4 extend; SmSurface::AssertValid wired to surface.surface_assert_valid (PR #1460); SmPlane placement reports are named by the surface key on both sides). Regression tests: crates/solidcore/tests/issue_1419_brep_check.rs. [PR #1478 (#1419 WP4a, stacked on #1459): level-2 seams wired (sm_AssertTestFace32/33/38 -> topology::face::assert_tests, sm_AssertTestEdge13 -> topology::edge::edge_form, sm_AssertTestVertex1 -> topology::vertex::corner). Shim op 2 compares those tests entity by entity: 202 cases in fixtures/topology.assert_valid/level2_tests.json (197 pass, 5 rejected: null-link mutations 8-10 and bodies the shim cannot build); 94 level-2 AssertValid/capi cases in level2_assert_valid.json (deferred at the WP2/WP3a seams). PARTIAL topology.assert_valid cases=828 passed=351 failed=0 deferred=467 rejected=9 known_mismatch=1 max_rel_err=0e0]"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long