mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #1472 from brett19/port/assert-valid-level0-helpers
port: level-0 helpers for topology AssertValid (WP2 of #1419)
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
//! curves reaches a Newton intersection (`SmCurve::GlobalCurveIntersect` through
|
||||
//! `SmCCIntersectENFO::Evaluate`, `curve.curve_p5`) and `AssertValid` past its
|
||||
//! knot checks (`SmCurve::AssertValid`, `curve.curve_p4`; `SmTol::IsDegenerate`).
|
||||
//! `AssertNoPeriodicJump` runs on built-in cases (`jump_*`, see [`jump_cases`]).
|
||||
//! Every run has at least [`MIN_GENERATED`] generated cases; generated inputs the ffi
|
||||
//! wrapper rejects are redrawn.
|
||||
|
||||
@@ -37,6 +38,8 @@ use serde_json::Value;
|
||||
use solidcore::geom::assert_array::{AssertArray, ObjectKey};
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::vector2d::Point2;
|
||||
use solidcore::geom::geom_utility::p2::ContinuityType;
|
||||
use solidcore::geom::vector3d::Vec3;
|
||||
use solidcore::kernel::Kernel;
|
||||
@@ -169,6 +172,12 @@ enum OpIn {
|
||||
Assert {
|
||||
given: bool,
|
||||
},
|
||||
Jump {
|
||||
closed_u: bool,
|
||||
closed_v: bool,
|
||||
/// `u0 v0 u1 v1`: `SmExtent2d(SmPoint2d(u0, v0), SmPoint2d(u1, v1))`.
|
||||
dom: [f64; 4],
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -190,6 +199,7 @@ impl OpIn {
|
||||
OpIn::Subdivide { .. } => Op::Subdivide,
|
||||
OpIn::Gap { .. } => Op::Gap,
|
||||
OpIn::Assert { .. } => Op::Assert,
|
||||
OpIn::Jump { .. } => Op::Jump,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,6 +325,14 @@ fn cpp_input(i: &Inputs) -> Result<Vec<f64>, HarnessError> {
|
||||
o.extend([f64::from(*fix), *dmin]);
|
||||
}
|
||||
OpIn::Assert { given } => o.push(flag(*given)),
|
||||
OpIn::Jump {
|
||||
closed_u,
|
||||
closed_v,
|
||||
dom,
|
||||
} => {
|
||||
o.extend([flag(*closed_u), flag(*closed_v)]);
|
||||
o.extend_from_slice(dom);
|
||||
}
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
@@ -408,7 +426,7 @@ fn to_output(i: &Inputs, flat: &[f64]) -> Result<Output, HarnessError> {
|
||||
s.state("new_")?;
|
||||
}
|
||||
}
|
||||
OpIn::Pole { .. } | OpIn::Gap { .. } => s.vals("flag", 1)?,
|
||||
OpIn::Pole { .. } | OpIn::Gap { .. } | OpIn::Jump { .. } => s.vals("flag", 1)?,
|
||||
OpIn::Segment { .. } => {
|
||||
s.vals("flag", 1)?;
|
||||
s.vals("line", 6)?;
|
||||
@@ -609,6 +627,15 @@ fn rust_op(
|
||||
let other = other.ok_or_else(|| harness("gap without other curve"))?;
|
||||
o.push(flag(c.trim_gap(&k, other, fix_of(*fix)?, *dmin)?));
|
||||
}
|
||||
OpIn::Jump {
|
||||
closed_u,
|
||||
closed_v,
|
||||
dom,
|
||||
} => {
|
||||
let [u0, v0, u1, v1] = *dom;
|
||||
let d = Extent2d::from_points(Point2::new(u0, v0), Point2::new(u1, v1));
|
||||
o.push(flag(c.assert_no_periodic_jump(*closed_u, *closed_v, &d)?));
|
||||
}
|
||||
OpIn::Assert { given } => {
|
||||
let mut list = AssertArray::new();
|
||||
let ok = c.assert_valid(&k, Some(&mut list), *given, ObjectKey(1))?;
|
||||
@@ -1057,6 +1084,73 @@ fn gen_case(rng: &mut Rng) -> Inputs {
|
||||
Inputs { this, op }
|
||||
}
|
||||
|
||||
/// `AssertNoPeriodicJump` cases (systematic, fixed seed): 2D and 3D curves of degree 1
|
||||
/// to 3 against domains from a twentieth to twice the polygon's extent (so that the
|
||||
/// 0.9-period threshold falls on both sides of the control point steps, including
|
||||
/// tiny and huge scales), closed in U, V, both or neither; and 2-point lines.
|
||||
fn jump_cases(unit: &str) -> Result<Vec<Case>, HarnessError> {
|
||||
let mut rng = Rng::new(0x1419_0002);
|
||||
let mut out = Vec::new();
|
||||
let mut n = 0;
|
||||
while out.len() < 240 && n < 2000 {
|
||||
n += 1;
|
||||
let p = 1 + rng.below(3) as usize;
|
||||
let three_d = rng.chance(0.15);
|
||||
let scale = *rng.choose(&[1e-3, 1.0, 1.0, 1e3]).unwrap_or(&1.0);
|
||||
let mut c = if rng.chance(0.1) {
|
||||
gen_crv_full(&mut rng, 1, 0, 1, false, three_d)
|
||||
} else {
|
||||
let extra = rng.below(5) as usize;
|
||||
gen_crv_full(&mut rng, p, extra, 1, false, three_d)
|
||||
};
|
||||
for cp in &mut c.cpts {
|
||||
cp[0] *= scale;
|
||||
cp[1] *= scale;
|
||||
}
|
||||
let (lo, hi) = c.cpts.iter().fold(
|
||||
([f64::MAX; 2], [f64::MIN; 2]),
|
||||
|(l, h), cp| {
|
||||
(
|
||||
[l[0].min(cp[0]), l[1].min(cp[1])],
|
||||
[h[0].max(cp[0]), h[1].max(cp[1])],
|
||||
)
|
||||
},
|
||||
);
|
||||
let f = *rng.choose(&[0.05, 0.3, 0.6, 1.0, 1.05, 1.11, 2.0]).unwrap_or(&1.0);
|
||||
let ext_u = ((hi[0] - lo[0]) * f).max(1e-9 * scale);
|
||||
let ext_v = ((hi[1] - lo[1]) * f).max(1e-9 * scale);
|
||||
let dom = [lo[0], lo[1], lo[0] + ext_u, lo[1] + ext_v];
|
||||
let closed = rng.below(4);
|
||||
let i = Inputs {
|
||||
this: This {
|
||||
dim: if three_d { 3 } else { 2 },
|
||||
oob: false,
|
||||
crv: Some(CrvIn::Inline(c)),
|
||||
},
|
||||
op: OpIn::Jump {
|
||||
closed_u: closed & 1 != 0,
|
||||
closed_v: closed & 2 != 0,
|
||||
dom,
|
||||
},
|
||||
};
|
||||
let ok = cpp_input(&i).is_ok_and(|input| ffi::validate(i.op.ffi(), &input).is_ok());
|
||||
if !ok {
|
||||
continue;
|
||||
}
|
||||
out.push(Case {
|
||||
unit: unit.to_string(),
|
||||
name: format!("jump_{:03}", out.len()),
|
||||
source: crate::CaseSource::Fixture {
|
||||
path: "built-in: AssertNoPeriodicJump cases".to_string(),
|
||||
index: out.len(),
|
||||
upstream_ref: None,
|
||||
},
|
||||
inputs: serde_json::to_value(&i).map_err(|e| harness(e.to_string()))?,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// A generated case the ffi wrapper accepts (redrawn up to 64 times).
|
||||
fn gen_valid(rng: &mut Rng) -> Option<Inputs> {
|
||||
for _ in 0..64 {
|
||||
@@ -1083,7 +1177,9 @@ impl UnitDiff for Runner {
|
||||
seeds: ctx.seeds.max(MIN_GENERATED),
|
||||
..ctx.clone()
|
||||
};
|
||||
default_cases(self, &ctx)
|
||||
let mut v = default_cases(self, &ctx)?;
|
||||
v.extend(jump_cases(self.id())?);
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
|
||||
@@ -795,13 +795,34 @@ impl UnitDiff for Runner {
|
||||
}
|
||||
|
||||
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if super::topology_face_p4::level0::is_level0(case) {
|
||||
return super::topology_face_p4::level0::run_cpp(case);
|
||||
}
|
||||
run_cpp(&case.parse::<Inputs>()?)
|
||||
}
|
||||
|
||||
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if super::topology_face_p4::level0::is_level0(case) {
|
||||
return super::topology_face_p4::level0::run_rust(case);
|
||||
}
|
||||
run_rust(&case.parse::<Inputs>()?)
|
||||
}
|
||||
|
||||
fn cases(&self, ctx: &crate::CaseContext) -> Result<Vec<Case>, HarnessError> {
|
||||
let mut v = crate::runner::default_cases(self, ctx)?;
|
||||
{
|
||||
use super::topology_face_p4::level0::Level0Op;
|
||||
let ops: Vec<Level0Op> = vec![
|
||||
Level0Op::EdgeuseSide { smp_count: 6, periodic_u: -1, periodic_v: -1 },
|
||||
Level0Op::EdgeuseSide { smp_count: 1, periodic_u: 1, periodic_v: 1 },
|
||||
Level0Op::EdgeuseSide { smp_count: 0, periodic_u: 0, periodic_v: 0 },
|
||||
Level0Op::EdgeuseSide { smp_count: 13, periodic_u: 1, periodic_v: -1 },
|
||||
];
|
||||
v.extend(super::topology_face_p4::level0::cases(self.id(), &ops, 0x1419_0103, 48)?);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn generate(&self, rng: &mut Rng) -> Option<Value> {
|
||||
let op = gen_op(rng);
|
||||
let i = Inputs {
|
||||
|
||||
@@ -71,7 +71,7 @@ fn harness(msg: impl Into<String>) -> HarnessError {
|
||||
const NULL_DEREF: f64 = -2.0;
|
||||
|
||||
/// The `.smb` files the systematic cases run on (those of unit `topology.vertexuse`).
|
||||
const FILES: [&str; 15] = [
|
||||
pub(super) const FILES: [&str; 15] = [
|
||||
"Mouse.smb",
|
||||
"Endcap.smb",
|
||||
"box_with_large_fillet.smb",
|
||||
@@ -89,6 +89,18 @@ const FILES: [&str; 15] = [
|
||||
"vase_rev.smb",
|
||||
];
|
||||
|
||||
/// Further `.smb` files with faces of several loops, for `sm_AssertTestFace37`.
|
||||
pub(super) const FACE37_FILES: [&str; 5] = [
|
||||
"Boolean/extrudeHoles1.smb",
|
||||
"Boolean/BoltMBox1.smb",
|
||||
"Fillet/cyl_on_box_w_hole.smb",
|
||||
"Fillet/ext_w_hole.smb",
|
||||
"Solids/l_shape_with_boss.smb",
|
||||
];
|
||||
|
||||
#[path = "topology_face_p4/level0.rs"]
|
||||
pub(super) mod level0;
|
||||
|
||||
// ---- inputs -----------------------------------------------------------------------------
|
||||
|
||||
/// Which upward-use getter a `List` op calls.
|
||||
@@ -147,6 +159,8 @@ enum OpIn {
|
||||
periodic_u: i64,
|
||||
periodic_v: i64,
|
||||
},
|
||||
/// `sm_AssertTestFace37` (the `SmFace::AssertValid` rule 37 loop nesting test).
|
||||
Face37,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -166,7 +180,7 @@ fn flag(b: bool) -> f64 {
|
||||
}
|
||||
|
||||
/// The B-rep block (`validate_only` and `init_gaps` off).
|
||||
fn brep_block(b: &BrepIn) -> Result<Vec<f64>, HarnessError> {
|
||||
pub(super) fn brep_block(b: &BrepIn) -> Result<Vec<f64>, HarnessError> {
|
||||
let mut out = Vec::new();
|
||||
match b {
|
||||
BrepIn::File { rebuild, .. } => out.extend([0.0, flag(*rebuild)]),
|
||||
@@ -247,6 +261,7 @@ fn op_args(op: &OpIn) -> (Op, Vec<f64>) {
|
||||
flag(tol.is_none()),
|
||||
],
|
||||
),
|
||||
OpIn::Face37 => (Op::Face37, vec![]),
|
||||
OpIn::Seam {
|
||||
outer,
|
||||
periodic_u,
|
||||
@@ -896,6 +911,19 @@ fn rust_record(i: &Inputs, mut b: Brep) -> Result<Vec<f64>, Fail> {
|
||||
put_class(&mut o, &s.class_u);
|
||||
put_class(&mut o, &s.class_v);
|
||||
}
|
||||
OpIn::Face37 => {
|
||||
let no_surface = b.face(f).map_err(|e| fail(&e))?.surface.is_none();
|
||||
let n_loops = solidcore::topology::face::p2::loops(&b, f, false, None)
|
||||
.map_err(|e| fail(&e))?
|
||||
.len();
|
||||
if no_surface && n_loops >= 2 {
|
||||
o.push(NULL_DEREF);
|
||||
} else {
|
||||
o.push(flag(
|
||||
fp4::assert_test_face37(&k, &mut b, f).map_err(|e| fail(&e))?,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
@@ -1095,6 +1123,56 @@ impl UnitDiff for Runner {
|
||||
}
|
||||
}
|
||||
}
|
||||
// sm_AssertTestFace37 on the first faces of every upstream file and of bodies with
|
||||
// holed faces (the AssertValid level-0 helpers, #1419)
|
||||
for file in FILES.iter().chain(FACE37_FILES.iter()) {
|
||||
for rebuild in [false, true] {
|
||||
for s in 0..10i64 {
|
||||
let inputs = Inputs {
|
||||
brep: BrepIn::File {
|
||||
file: (*file).to_string(),
|
||||
rebuild,
|
||||
},
|
||||
face: s,
|
||||
op: OpIn::Face37,
|
||||
};
|
||||
v.push(Case {
|
||||
unit: self.id().to_string(),
|
||||
name: format!("{file}_{}_f{s}_face37", u8::from(rebuild)),
|
||||
source: crate::CaseSource::Fixture {
|
||||
path: "built-in: sm_AssertTestFace37 cases".to_string(),
|
||||
index: v.len(),
|
||||
upstream_ref: None,
|
||||
},
|
||||
inputs: serde_json::to_value(inputs).map_err(|e| harness(e.to_string()))?,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut rng = Rng::new(0x1419_0037);
|
||||
for j in 0..24 {
|
||||
let brep = loop {
|
||||
let b = shared::gen_brep(&mut rng);
|
||||
if matches!(b, BrepIn::Face { .. }) {
|
||||
break b;
|
||||
}
|
||||
};
|
||||
v.push(Case {
|
||||
unit: self.id().to_string(),
|
||||
name: format!("gen_face37_{j:02}"),
|
||||
source: crate::CaseSource::Fixture {
|
||||
path: "built-in: sm_AssertTestFace37 cases".to_string(),
|
||||
index: v.len(),
|
||||
upstream_ref: None,
|
||||
},
|
||||
inputs: serde_json::to_value(Inputs {
|
||||
brep,
|
||||
face: 0,
|
||||
op: OpIn::Face37,
|
||||
})
|
||||
.map_err(|e| harness(e.to_string()))?,
|
||||
});
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
//! The per-entity `AssertValid` level-0 helper cases (#1419) shared by the runners of
|
||||
//! units `topology.vertex` (`SmVertex::CheckPointers`), `topology.loopuse`
|
||||
//! (`SmLoopuse::IsLoopuseSideInSurface`) and `topology.edgeuse_p2`
|
||||
//! (`SmEdgeuse::HasDogLegNearPole`, `SmEdgeuse::IsEdgeuseSideInSurface`).
|
||||
//!
|
||||
//! A case is `{"level0": <op>, "brep": <BrepIn>}`: the C++ shim builds the B-rep from the
|
||||
//! B-rep block of unit `topology.vertexuse` and records the helper on every entity of the
|
||||
//! kind in its traversal order; the Rust side builds the same B-rep itself
|
||||
//! ([`super::super::topology_face_p1::own_brep`], arena ids in that traversal order) and
|
||||
//! records the port the same way. `-2` marks a call the shim skips because upstream
|
||||
//! dereferences null or reads out of bounds there (see the shims); the Rust side skips
|
||||
//! the same calls on the same conditions.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::topology::{Brep, EdgeuseId, LoopuseId, VertexId};
|
||||
use solidcore::SolidError;
|
||||
use solidcore_ffi::units::{topology_edgeuse_p2 as ffi_eu, topology_loopuse as ffi_lu};
|
||||
use solidcore_ffi::units::topology_vertex as ffi_v;
|
||||
|
||||
use super::super::topology_vertexuse::{self as shared, BrepIn};
|
||||
use super::{brep_block, FACE37_FILES, FILES};
|
||||
use crate::{Case, HarnessError, Output, Rng};
|
||||
|
||||
/// A skipped call (see the module docs).
|
||||
const SKIPPED: f64 = -2.0;
|
||||
|
||||
/// The helper a level-0 case records.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "op", rename_all = "snake_case")]
|
||||
pub(crate) enum Level0Op {
|
||||
/// `SmVertex::CheckPointers` of every vertex.
|
||||
CheckPointers,
|
||||
/// `SmLoopuse::IsLoopuseSideInSurface` of every loop use (`-1`: NULL periodic flag).
|
||||
LoopuseSide { periodic_u: i64, periodic_v: i64 },
|
||||
/// `SmEdgeuse::HasDogLegNearPole` and `SmEdgeuse::IsEdgeuseSideInSurface` of every
|
||||
/// edge use.
|
||||
EdgeuseSide {
|
||||
smp_count: u64,
|
||||
periodic_u: i64,
|
||||
periodic_v: i64,
|
||||
},
|
||||
}
|
||||
|
||||
/// A level-0 case's inputs.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct Level0In {
|
||||
level0: Level0Op,
|
||||
brep: BrepIn,
|
||||
}
|
||||
|
||||
/// Whether `case` is a level-0 case.
|
||||
pub(crate) fn is_level0(case: &Case) -> bool {
|
||||
case.inputs.get("level0").is_some()
|
||||
}
|
||||
|
||||
fn harness(msg: impl Into<String>) -> HarnessError {
|
||||
HarnessError::new(msg)
|
||||
}
|
||||
|
||||
fn flag(b: bool) -> f64 {
|
||||
if b {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn periodic_of(p: i64) -> Option<bool> {
|
||||
(p >= 0).then_some(p == 1)
|
||||
}
|
||||
|
||||
/// The C++ side of a level-0 case.
|
||||
pub(crate) fn run_cpp(case: &Case) -> Result<Output, HarnessError> {
|
||||
let i: Level0In = case.parse()?;
|
||||
let path = shared::path_of(&i.brep)?;
|
||||
let block = brep_block(&i.brep)?;
|
||||
let r = match i.level0 {
|
||||
Level0Op::CheckPointers => ffi_v::check_pointers(&path, &block),
|
||||
Level0Op::LoopuseSide {
|
||||
periodic_u,
|
||||
periodic_v,
|
||||
} => ffi_lu::side_in_surface(&path, periodic_u as f64, periodic_v as f64, &block),
|
||||
Level0Op::EdgeuseSide {
|
||||
smp_count,
|
||||
periodic_u,
|
||||
periodic_v,
|
||||
} => ffi_eu::pole(
|
||||
&path,
|
||||
usize::try_from(smp_count).map_err(|_| harness("sample count"))?,
|
||||
periodic_u as f64,
|
||||
periodic_v as f64,
|
||||
&block,
|
||||
),
|
||||
};
|
||||
let v = r.map_err(|e| harness(format!("c++ level-0 helper: {e}")))?;
|
||||
Ok(Output::new().values("out", v))
|
||||
}
|
||||
|
||||
/// A Rust-side failure: an unported callee defers the case, anything else is the
|
||||
/// harness's error.
|
||||
fn fail(e: &SolidError) -> Result<Output, HarnessError> {
|
||||
Output::unported(e).ok_or_else(|| harness(format!("rust level-0 helper: {e}")))
|
||||
}
|
||||
|
||||
/// Whether an edge use's edge has no curve while its face has a surface (upstream
|
||||
/// dereferences the null curve there).
|
||||
fn null_curve(b: &Brep, eu: EdgeuseId) -> Result<bool, SolidError> {
|
||||
let has_surface = match solidcore::topology::edgeuse::p1::face(b, eu)? {
|
||||
Some(f) => b.face(f)?.surface.is_some(),
|
||||
None => false,
|
||||
};
|
||||
let no_curve = match b.edgeuse(eu)?.edge {
|
||||
Some(e) => b.edge(e)?.curve.is_none(),
|
||||
None => false,
|
||||
};
|
||||
Ok(has_surface && no_curve)
|
||||
}
|
||||
|
||||
fn rust_record(op: &Level0Op, b: &Brep, c: [usize; 10]) -> Result<Vec<f64>, SolidError> {
|
||||
let k = Kernel::default();
|
||||
let mut o = vec![0.0];
|
||||
let ids = |n: usize| (0..n).filter_map(|i| u32::try_from(i).ok());
|
||||
match op {
|
||||
Level0Op::CheckPointers => {
|
||||
o.push(c[9] as f64);
|
||||
for v in ids(c[9]) {
|
||||
o.push(flag(solidcore::topology::vertex::check_pointers(
|
||||
b,
|
||||
VertexId::from_index(v),
|
||||
)?));
|
||||
}
|
||||
}
|
||||
Level0Op::LoopuseSide {
|
||||
periodic_u,
|
||||
periodic_v,
|
||||
} => {
|
||||
o.push(c[4] as f64);
|
||||
for l in ids(c[4]) {
|
||||
let lu = LoopuseId::from_index(l);
|
||||
let mut guard = false;
|
||||
for eu in solidcore::topology::loopuse::edgeuses(b, lu, None, None)? {
|
||||
guard |= null_curve(b, eu)?;
|
||||
}
|
||||
if guard {
|
||||
o.extend([SKIPPED; 3]);
|
||||
continue;
|
||||
}
|
||||
let r = solidcore::topology::loopuse::is_loopuse_side_in_surface(
|
||||
&k,
|
||||
b,
|
||||
lu,
|
||||
periodic_of(*periodic_u),
|
||||
periodic_of(*periodic_v),
|
||||
)?;
|
||||
o.extend([
|
||||
flag(r.in_surface),
|
||||
flag(r.on_natural_boundary),
|
||||
r.edgeuse_index as f64,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Level0Op::EdgeuseSide {
|
||||
smp_count,
|
||||
periodic_u,
|
||||
periodic_v,
|
||||
} => {
|
||||
use solidcore::topology::edgeuse::p2 as eu2;
|
||||
o.push(c[6] as f64);
|
||||
for e in ids(c[6]) {
|
||||
let eu = EdgeuseId::from_index(e);
|
||||
match eu2::has_dog_leg_near_pole(&k, b, eu) {
|
||||
Ok(d) => o.extend([
|
||||
flag(d.has_dog_leg),
|
||||
d.index.map_or(-1.0, |x| x as f64),
|
||||
]),
|
||||
// the intersector-form trim polygon of fewer than four points the
|
||||
// shim skips (upstream reads `lNum - 4`)
|
||||
Err(SolidError::InvalidInput { reason, .. })
|
||||
if reason.contains("fewer than four control points") =>
|
||||
{
|
||||
o.extend([SKIPPED; 2]);
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
if null_curve(b, eu)? {
|
||||
o.extend([SKIPPED; 2]);
|
||||
continue;
|
||||
}
|
||||
let r = eu2::is_edgeuse_side_in_surface(
|
||||
&k,
|
||||
b,
|
||||
eu,
|
||||
*smp_count,
|
||||
periodic_of(*periodic_u),
|
||||
periodic_of(*periodic_v),
|
||||
)?;
|
||||
o.extend([flag(r.in_surface), flag(r.on_natural_boundary)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
/// The Rust side of a level-0 case.
|
||||
pub(crate) fn run_rust(case: &Case) -> Result<Output, HarnessError> {
|
||||
let i: Level0In = case.parse()?;
|
||||
let (b, c) = match super::super::topology_face_p1::own_brep(&i.brep, false, false)? {
|
||||
Ok(x) => x,
|
||||
Err(o) => return Ok(o),
|
||||
};
|
||||
match rust_record(&i.level0, &b, c) {
|
||||
Ok(v) => Ok(Output::new().values("out", v)),
|
||||
Err(e) => fail(&e),
|
||||
}
|
||||
}
|
||||
|
||||
/// The level-0 cases of `unit` for `ops`: every upstream file (with and without rebuilt
|
||||
/// UV trim curves), the files with holed faces, and `n_gen` generated B-reps (fixed seed).
|
||||
pub(crate) fn cases(
|
||||
unit: &str,
|
||||
ops: &[Level0Op],
|
||||
seed: u64,
|
||||
n_gen: usize,
|
||||
) -> Result<Vec<Case>, HarnessError> {
|
||||
let mut v = Vec::new();
|
||||
let push = |v: &mut Vec<Case>, name: String, brep: BrepIn, op: &Level0Op| {
|
||||
let inputs = Level0In {
|
||||
level0: op.clone(),
|
||||
brep,
|
||||
};
|
||||
let inputs: Value = serde_json::to_value(inputs).map_err(|e| harness(e.to_string()))?;
|
||||
let index = v.len();
|
||||
v.push(Case {
|
||||
unit: unit.to_string(),
|
||||
name,
|
||||
source: crate::CaseSource::Fixture {
|
||||
path: "built-in: AssertValid level-0 helper cases".to_string(),
|
||||
index,
|
||||
upstream_ref: Some("tests/prog_test (the pt_TestFiles B-reps)".to_string()),
|
||||
},
|
||||
inputs,
|
||||
});
|
||||
Ok::<(), HarnessError>(())
|
||||
};
|
||||
for file in FILES.iter().chain(FACE37_FILES.iter()) {
|
||||
for rebuild in [false, true] {
|
||||
for (j, op) in ops.iter().enumerate() {
|
||||
let brep = BrepIn::File {
|
||||
file: (*file).to_string(),
|
||||
rebuild,
|
||||
};
|
||||
push(
|
||||
&mut v,
|
||||
format!("level0_{file}_{}_op{j}", u8::from(rebuild)),
|
||||
brep,
|
||||
op,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut rng = Rng::new(seed);
|
||||
for g in 0..n_gen {
|
||||
let brep = shared::gen_brep(&mut rng);
|
||||
let op = ops
|
||||
.get(g % ops.len().max(1))
|
||||
.ok_or_else(|| harness("no level-0 ops"))?;
|
||||
push(&mut v, format!("level0_gen_{g:03}"), brep, op)?;
|
||||
}
|
||||
for (j, op) in ops.iter().enumerate() {
|
||||
push(&mut v, format!("level0_network_op{j}"), BrepIn::Network, op)?;
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
@@ -1034,10 +1034,16 @@ impl UnitDiff for Runner {
|
||||
}
|
||||
|
||||
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if super::topology_face_p4::level0::is_level0(case) {
|
||||
return super::topology_face_p4::level0::run_cpp(case);
|
||||
}
|
||||
run_cpp(&case.parse::<Inputs>()?)
|
||||
}
|
||||
|
||||
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if super::topology_face_p4::level0::is_level0(case) {
|
||||
return super::topology_face_p4::level0::run_rust(case);
|
||||
}
|
||||
run_rust(&case.parse::<Inputs>()?)
|
||||
}
|
||||
|
||||
@@ -1058,6 +1064,15 @@ impl UnitDiff for Runner {
|
||||
inputs,
|
||||
});
|
||||
}
|
||||
{
|
||||
use super::topology_face_p4::level0::Level0Op;
|
||||
let ops: Vec<Level0Op> = vec![
|
||||
Level0Op::LoopuseSide { periodic_u: -1, periodic_v: -1 },
|
||||
Level0Op::LoopuseSide { periodic_u: 1, periodic_v: 0 },
|
||||
Level0Op::LoopuseSide { periodic_u: 0, periodic_v: 1 },
|
||||
];
|
||||
v.extend(super::topology_face_p4::level0::cases(self.id(), &ops, 0x1419_0102, 36)?);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
//! Setup failures fail the harness; topology and native integer flags compare exactly.
|
||||
|
||||
use super::topology_edgeuse_p1::{fixture_input, import};
|
||||
|
||||
#[path = "topology_shell/inner.rs"]
|
||||
mod inner;
|
||||
use crate::{Case, HarnessError, Output, Rng, UnitDiff};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
@@ -789,6 +792,9 @@ impl UnitDiff for Runner {
|
||||
}
|
||||
|
||||
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if inner::is_inner(case) {
|
||||
return inner::run_cpp(case);
|
||||
}
|
||||
let inputs: Inputs = case.parse()?;
|
||||
let (path, block) = fixture_input(inputs.brep)?;
|
||||
if let Some(filter) = inputs.region_filter {
|
||||
@@ -939,7 +945,16 @@ impl UnitDiff for Runner {
|
||||
))
|
||||
}
|
||||
|
||||
fn cases(&self, ctx: &crate::CaseContext) -> Result<Vec<Case>, HarnessError> {
|
||||
let mut v = crate::runner::default_cases(self, ctx)?;
|
||||
v.extend(inner::cases(self.id()));
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if inner::is_inner(case) {
|
||||
return inner::run_rust(case);
|
||||
}
|
||||
let inputs: Inputs = case.parse()?;
|
||||
let mut brep = match own_brep(&inputs.brep)? {
|
||||
Ok(b) => b,
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
//! `SmShell::IsInnerShellOf` cases (#1419, #1473): every pair of shells of bodies with
|
||||
//! internal voids, built from boxes and spheres with Booleans on both sides
|
||||
//! (`SmApiCreateBox`/`SmApiCreateSphere`/`SmApiBoolean` in the shim
|
||||
//! `topology_shell_inner.cpp`, [`create_box`]/[`create_sphere`]/[`boolean`] in Rust).
|
||||
//!
|
||||
//! A case is `{"inner_shell_of": [[op, kind, x, y, z, a, b, c], ...]}` (see
|
||||
//! [`ffi::inner_shell_of`]). The record is `n_regions (n_shells)*n_regions`, then per
|
||||
//! shell (region and shell list order) `IsInnerShellOf(NULL)` and `IsInnerShellOf(other)`
|
||||
//! for every other shell. No C++ state reaches the Rust side: the Rust kernel builds the
|
||||
//! same bodies itself.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use solidcore::boolean::api_brep::boolean;
|
||||
use solidcore::geom::a_object::BooleanOperation;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::math::Vec3;
|
||||
use solidcore::primitive::api_primitives::{create_box, create_sphere};
|
||||
use solidcore::topology::brep::p1;
|
||||
use solidcore::topology::shell::is_inner_shell_of;
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::SolidError;
|
||||
use solidcore_ffi::units::topology_shell as ffi;
|
||||
|
||||
use crate::{Case, HarnessError, Output};
|
||||
|
||||
/// One step: `[op, kind, x, y, z, a, b, c]`.
|
||||
type Step = [f64; 8];
|
||||
|
||||
/// Whether `case` is an `IsInnerShellOf` case.
|
||||
pub(super) fn is_inner(case: &Case) -> bool {
|
||||
case.inputs.get("inner_shell_of").is_some()
|
||||
}
|
||||
|
||||
fn steps_of(case: &Case) -> Result<Vec<Step>, HarnessError> {
|
||||
let v = case
|
||||
.inputs
|
||||
.get("inner_shell_of")
|
||||
.cloned()
|
||||
.ok_or_else(|| HarnessError::new("no inner_shell_of steps"))?;
|
||||
serde_json::from_value(v).map_err(|e| HarnessError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// The C++ side.
|
||||
pub(super) fn run_cpp(case: &Case) -> Result<Output, HarnessError> {
|
||||
let steps = steps_of(case)?;
|
||||
let mut input = vec![steps.len() as f64];
|
||||
for s in &steps {
|
||||
input.extend(s);
|
||||
}
|
||||
let v = ffi::inner_shell_of(&input)
|
||||
.map_err(|e| HarnessError::new(format!("c++ IsInnerShellOf: {e}")))?;
|
||||
Ok(Output::new().values("out", v))
|
||||
}
|
||||
|
||||
fn primitive(k: &mut Kernel, s: &Step) -> Result<Brep, SolidError> {
|
||||
let [_, kind, x, y, z, a, b, c] = *s;
|
||||
let o = Vec3::new(x, y, z);
|
||||
if kind == 1.0 {
|
||||
create_box(k, o, a, b, c)
|
||||
} else {
|
||||
create_sphere(k, o, a)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_record(steps: &[Step]) -> Result<Vec<f64>, SolidError> {
|
||||
let mut k = Kernel::default();
|
||||
let mut body: Option<Brep> = None;
|
||||
for s in steps {
|
||||
let p = primitive(&mut k, s)?;
|
||||
body = Some(match body {
|
||||
None => p,
|
||||
Some(b) => {
|
||||
let op = BooleanOperation::from_code(s[0] as i32).ok_or(
|
||||
SolidError::InvalidInput {
|
||||
operation: "difftest::topology_shell::inner",
|
||||
reason: "step operation",
|
||||
},
|
||||
)?;
|
||||
boolean(&mut k, b, p, op)?.result
|
||||
}
|
||||
});
|
||||
}
|
||||
let mut b = body.ok_or(SolidError::InvalidInput {
|
||||
operation: "difftest::topology_shell::inner",
|
||||
reason: "no steps",
|
||||
})?;
|
||||
let regions = p1::regions(&b, None)?;
|
||||
let mut o = vec![regions.len() as f64];
|
||||
let mut shells = Vec::new();
|
||||
for r in regions {
|
||||
let sh = b.region(r)?.shells.clone();
|
||||
o.push(sh.len() as f64);
|
||||
shells.extend(sh);
|
||||
}
|
||||
for (i, &s) in shells.iter().enumerate() {
|
||||
o.push(f64::from(is_inner_shell_of(&mut k, &mut b, s, None)?));
|
||||
for (j, &t) in shells.iter().enumerate() {
|
||||
if i != j {
|
||||
o.push(f64::from(is_inner_shell_of(&mut k, &mut b, s, Some(t))?));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
/// The Rust side.
|
||||
pub(super) fn run_rust(case: &Case) -> Result<Output, HarnessError> {
|
||||
let steps = steps_of(case)?;
|
||||
match rust_record(&steps) {
|
||||
Ok(v) => Ok(Output::new().values("out", v)),
|
||||
Err(e) => Output::unported(&e)
|
||||
.ok_or_else(|| HarnessError::new(format!("rust IsInnerShellOf: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn bx(op: f64, x: f64, y: f64, z: f64, a: f64, b: f64, c: f64) -> Step {
|
||||
[op, 1.0, x, y, z, a, b, c]
|
||||
}
|
||||
|
||||
fn sph(op: f64, x: f64, y: f64, z: f64, r: f64) -> Step {
|
||||
[op, 2.0, x, y, z, r, 0.0, 0.0]
|
||||
}
|
||||
|
||||
/// The built-in bodies: solids with one or two box or sphere cavities, a void inside an
|
||||
/// island inside a void, plain primitives, at 1e-3, 1 and 1e3 scales.
|
||||
pub(super) fn cases(unit: &str) -> Vec<Case> {
|
||||
const UNION: f64 = 1.0;
|
||||
const DIFF: f64 = 3.0;
|
||||
let bodies: Vec<(&str, Vec<Step>)> = vec![
|
||||
("box", vec![bx(0.0, 0.0, 0.0, 0.0, 20.0, 20.0, 20.0)]),
|
||||
("sphere", vec![sph(0.0, 0.0, 0.0, 0.0, 10.0)]),
|
||||
(
|
||||
"box_minus_box",
|
||||
vec![
|
||||
bx(0.0, 0.0, 0.0, 0.0, 20.0, 20.0, 20.0),
|
||||
bx(DIFF, 5.0, 5.0, 5.0, 10.0, 10.0, 10.0),
|
||||
],
|
||||
),
|
||||
(
|
||||
"box_minus_sphere",
|
||||
vec![
|
||||
bx(0.0, 0.0, 0.0, 0.0, 20.0, 20.0, 20.0),
|
||||
sph(DIFF, 10.0, 10.0, 10.0, 4.0),
|
||||
],
|
||||
),
|
||||
(
|
||||
"sphere_minus_box",
|
||||
vec![
|
||||
sph(0.0, 0.0, 0.0, 0.0, 20.0),
|
||||
bx(DIFF, -3.0, -4.0, -5.0, 6.0, 7.0, 8.0),
|
||||
],
|
||||
),
|
||||
(
|
||||
"box_minus_two_boxes",
|
||||
vec![
|
||||
bx(0.0, 0.0, 0.0, 0.0, 40.0, 20.0, 20.0),
|
||||
bx(DIFF, 5.0, 5.0, 5.0, 10.0, 10.0, 10.0),
|
||||
bx(DIFF, 25.0, 5.0, 5.0, 10.0, 10.0, 10.0),
|
||||
],
|
||||
),
|
||||
(
|
||||
"box_minus_box_and_sphere",
|
||||
vec![
|
||||
bx(0.0, 0.0, 0.0, 0.0, 40.0, 20.0, 20.0),
|
||||
bx(DIFF, 5.0, 5.0, 5.0, 10.0, 10.0, 10.0),
|
||||
sph(DIFF, 30.0, 10.0, 10.0, 4.0),
|
||||
],
|
||||
),
|
||||
(
|
||||
"island_in_void",
|
||||
vec![
|
||||
bx(0.0, 0.0, 0.0, 0.0, 30.0, 30.0, 30.0),
|
||||
bx(DIFF, 5.0, 5.0, 5.0, 20.0, 20.0, 20.0),
|
||||
bx(UNION, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0),
|
||||
],
|
||||
),
|
||||
(
|
||||
"void_in_island_in_void",
|
||||
vec![
|
||||
bx(0.0, 0.0, 0.0, 0.0, 40.0, 40.0, 40.0),
|
||||
bx(DIFF, 5.0, 5.0, 5.0, 30.0, 30.0, 30.0),
|
||||
bx(UNION, 10.0, 10.0, 10.0, 20.0, 20.0, 20.0),
|
||||
bx(DIFF, 15.0, 15.0, 15.0, 10.0, 10.0, 10.0),
|
||||
],
|
||||
),
|
||||
(
|
||||
"two_separate_boxes",
|
||||
vec![
|
||||
bx(0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0),
|
||||
bx(UNION, 20.0, 0.0, 0.0, 10.0, 10.0, 10.0),
|
||||
],
|
||||
),
|
||||
(
|
||||
"off_centre_cavity",
|
||||
vec![
|
||||
bx(0.0, -10.0, -7.0, -3.0, 20.0, 14.0, 6.0),
|
||||
bx(DIFF, -9.0, -6.0, -2.0, 1.0, 1.0, 1.0),
|
||||
],
|
||||
),
|
||||
];
|
||||
let mut v = Vec::new();
|
||||
for scale in [1.0, 1e-1, 1e2] {
|
||||
for (name, steps) in &bodies {
|
||||
let scaled: Vec<Step> = steps
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let mut t = *s;
|
||||
for x in t.iter_mut().skip(2) {
|
||||
*x *= scale;
|
||||
}
|
||||
t
|
||||
})
|
||||
.collect();
|
||||
let index = v.len();
|
||||
v.push(Case {
|
||||
unit: unit.to_string(),
|
||||
name: format!("inner_shell_of_{name}_x{scale:e}"),
|
||||
source: crate::CaseSource::Fixture {
|
||||
path: "built-in: IsInnerShellOf cases".to_string(),
|
||||
index,
|
||||
upstream_ref: None,
|
||||
},
|
||||
inputs: json!({ "inner_shell_of": scaled }),
|
||||
});
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
@@ -2084,13 +2084,29 @@ impl UnitDiff for Runner {
|
||||
}
|
||||
|
||||
fn run_cpp(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if super::topology_face_p4::level0::is_level0(case) {
|
||||
return super::topology_face_p4::level0::run_cpp(case);
|
||||
}
|
||||
run_cpp(&case.parse::<Inputs>()?)
|
||||
}
|
||||
|
||||
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
|
||||
if super::topology_face_p4::level0::is_level0(case) {
|
||||
return super::topology_face_p4::level0::run_rust(case);
|
||||
}
|
||||
run_rust(&case.parse::<Inputs>()?)
|
||||
}
|
||||
|
||||
fn cases(&self, ctx: &crate::CaseContext) -> Result<Vec<Case>, HarnessError> {
|
||||
let mut v = crate::runner::default_cases(self, ctx)?;
|
||||
{
|
||||
use super::topology_face_p4::level0::Level0Op;
|
||||
let ops: Vec<Level0Op> = vec![Level0Op::CheckPointers];
|
||||
v.extend(super::topology_face_p4::level0::cases(self.id(), &ops, 0x1419_0101, 24)?);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn generate(&self, rng: &mut Rng) -> Option<Value> {
|
||||
let op = gen_op(rng);
|
||||
let i = Inputs {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//! | `Subdivide` | `SubdivideAtDiscontinuity` | `continuity angle_tol` |
|
||||
//! | `Gap` | `TrimGap` | curve block `fix dmin` |
|
||||
//! | `Assert` | `AssertValid` | `level_given` |
|
||||
//! | `Jump` | `AssertNoPeriodicJump` | `closed_u closed_v u0 v0 u1 v1` (`SmExtent2d(SmPoint2d(u0, v0), SmPoint2d(u1, v1))`) |
|
||||
//!
|
||||
//! Intervals `a b` are `SmExtent1d(a, b)` (swapped when `a > b`). The output is the op
|
||||
//! status (`0` = `SM_SUCCESS`, else the `SmStatus`; `0` for the predicates), the op's
|
||||
@@ -31,6 +32,7 @@
|
||||
//! state of the other curve.
|
||||
//! - `Subdivide`: the count and the state of every piece.
|
||||
//! - `Assert`: the flag, the report count and per report `test tol3d value u v`.
|
||||
//! - `Jump`: the flag.
|
||||
//!
|
||||
//! A state is `dim oob borrowed knot_type form has_nurb [n p m cpts knots]`.
|
||||
//!
|
||||
@@ -72,6 +74,7 @@ pub enum Op {
|
||||
Subdivide,
|
||||
Gap,
|
||||
Assert,
|
||||
Jump,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
@@ -86,6 +89,7 @@ impl Op {
|
||||
Op::Subdivide => 6,
|
||||
Op::Gap => 7,
|
||||
Op::Assert => 8,
|
||||
Op::Jump => 9,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +354,13 @@ fn validate_op(op: Op, rd: &mut Reader<'_>, c: &CurveInfo) -> Result<(), FfiErro
|
||||
Op::Assert => {
|
||||
rd.flag()?;
|
||||
}
|
||||
Op::Jump => {
|
||||
rd.flag()?;
|
||||
rd.flag()?;
|
||||
for _ in 0..4 {
|
||||
rd.finite()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -245,6 +245,67 @@ fn eval_impl(
|
||||
raw(op, path, input)
|
||||
}
|
||||
|
||||
/// Largest `smp_count` of [`pole`].
|
||||
pub const MAX_POLE_SAMPLES: usize = 64;
|
||||
|
||||
mod sys_topology_edgeuse_p2_pole {
|
||||
extern "C" {
|
||||
pub fn sc_topology_edgeuse_p2_pole(
|
||||
path: *const std::ffi::c_char,
|
||||
input: *const f64,
|
||||
n_in: i64,
|
||||
out: *mut f64,
|
||||
cap: i64,
|
||||
out_len: *mut i64,
|
||||
) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// `SmEdgeuse::HasDogLegNearPole` and `IsEdgeuseSideInSurface(smp_count, periodic_u,
|
||||
/// periodic_v)` (`-1`: NULL) of every edge use of the B-rep built from `block` (shim
|
||||
/// `topology_edgeuse_p2_pole.cpp`): `0 n (dog_leg index side on_boundary)*n`.
|
||||
/// `smp_count` is at most [`MAX_POLE_SAMPLES`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for a malformed argument or B-rep block (the checks of
|
||||
/// [`super::topology_vertexuse::validate_block`], the whole input consumed);
|
||||
/// [`FfiError::Status`] for a shim failure.
|
||||
pub fn pole(path: &str, smp_count: usize, periodic_u: f64, periodic_v: f64, block: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
if smp_count > MAX_POLE_SAMPLES {
|
||||
return Err(FfiError::InvalidArgument("too many samples".into()));
|
||||
}
|
||||
for p in [periodic_u, periodic_v] {
|
||||
if p != -1.0 && p != 0.0 && p != 1.0 {
|
||||
return Err(FfiError::InvalidArgument("periodic flag must be -1, 0 or 1".into()));
|
||||
}
|
||||
}
|
||||
super::topology_vertexuse::validate_block(path, block)?;
|
||||
if super::topology_vertexuse::brep_block_at(block, path)? != block.len() {
|
||||
return Err(FfiError::InvalidArgument("trailing values after the B-rep block".into()));
|
||||
}
|
||||
let mut input: Vec<f64> = vec![smp_count as f64, periodic_u, periodic_v];
|
||||
input.extend_from_slice(block);
|
||||
let cpath = std::ffi::CString::new(path)
|
||||
.map_err(|_| FfiError::InvalidArgument("path has a NUL byte".into()))?;
|
||||
call_with_buffer(4096, |buf, len| {
|
||||
// SAFETY: cpath is a valid NUL-terminated string; input is valid for input.len()
|
||||
// reads and holds the validated arguments then a validated B-rep block (exactly
|
||||
// what the shim reads); buf is valid for buf.len() writes and the shim honours
|
||||
// the capacity; len is valid.
|
||||
unsafe {
|
||||
sys_topology_edgeuse_p2_pole::sc_topology_edgeuse_p2_pole(
|
||||
cpath.as_ptr(),
|
||||
input.as_ptr(),
|
||||
input.len() as i64,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i64,
|
||||
&raw mut *len,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod prepared_guard_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
//! | `Connected` | `kind index` (`kind` an export slot `0..=9`, `-1`: NULL) | `-2 \| connected` (`IsConnectedTo`) |
|
||||
//! | `Line` | `p(3) v(3) has_ivl t0 t1 fire face_tol tol` | `-2 \| status n (type s(3) e(3))*n` (`GlobalLineIntersect` over the face's UV domain; `face_tol`: the face's tolerance) |
|
||||
//! | `Sector` | `vertex edge use_uv at_start opposite tol validate uv_ok face_tol` | `-2 \| status vertexuse problem cause` (`FindVertexSector` of the edge's curve from its start or end, the primary edge use's UV trim curve with `use_uv`) |
|
||||
//! | `Face37` | - | `-2 \| flag` (`sm_AssertTestFace37`; `-2` for a face without surface with two or more loops) |
|
||||
//! | `Seam` | `outer periodic_u periodic_v` (`-1`: no known periodicity) | `has crossed missing near_miss class_u class_v` (`HasSeamProblem` without face properties; a class is `tgt_only n (start mid end)*n` point classes) |
|
||||
|
||||
use super::topology_vertexuse as fixture;
|
||||
@@ -40,6 +41,7 @@ pub enum Op {
|
||||
Line = 8,
|
||||
Sector = 9,
|
||||
Seam = 10,
|
||||
Face37 = 11,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
@@ -53,7 +55,7 @@ impl Op {
|
||||
#[must_use]
|
||||
pub fn n_args(self) -> usize {
|
||||
match self {
|
||||
Self::Brep => 0,
|
||||
Self::Brep | Self::Face37 => 0,
|
||||
Self::Edges
|
||||
| Self::Vertices
|
||||
| Self::UpLoopuses
|
||||
@@ -140,7 +142,7 @@ pub fn eval(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
entity(*face, c[3], false, "face")?;
|
||||
let n_l = c[5];
|
||||
match (op, args) {
|
||||
(Op::Brep, []) => {}
|
||||
(Op::Brep | Op::Face37, []) => {}
|
||||
(
|
||||
Op::Edges | Op::Vertices | Op::UpLoopuses | Op::UpVertexuses | Op::UpEdgeuses,
|
||||
[tgt_only, l],
|
||||
|
||||
@@ -204,3 +204,57 @@ pub fn eval(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
rd.done()?;
|
||||
raw(op, path, input)
|
||||
}
|
||||
|
||||
mod sys_topology_loopuse_side_in_surface {
|
||||
extern "C" {
|
||||
pub fn sc_topology_loopuse_side_in_surface(
|
||||
path: *const std::ffi::c_char,
|
||||
input: *const f64,
|
||||
n_in: i64,
|
||||
out: *mut f64,
|
||||
cap: i64,
|
||||
out_len: *mut i64,
|
||||
) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// `SmLoopuse::IsLoopuseSideInSurface(periodic_u, periodic_v)` (`-1`: NULL) of every loop
|
||||
/// use of the B-rep built from `block` (shim `topology_loopuse_side.cpp`):
|
||||
/// `0 n (side on_boundary index)*n`, `-2 -2 -2` where upstream dereferences null.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for a malformed argument or B-rep block (the checks of
|
||||
/// [`super::topology_vertexuse::validate_block`], the whole input consumed);
|
||||
/// [`FfiError::Status`] for a shim failure.
|
||||
pub fn side_in_surface(path: &str, periodic_u: f64, periodic_v: f64, block: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
for p in [periodic_u, periodic_v] {
|
||||
if p != -1.0 && p != 0.0 && p != 1.0 {
|
||||
return Err(FfiError::InvalidArgument("periodic flag must be -1, 0 or 1".into()));
|
||||
}
|
||||
}
|
||||
super::topology_vertexuse::validate_block(path, block)?;
|
||||
if super::topology_vertexuse::brep_block_at(block, path)? != block.len() {
|
||||
return Err(FfiError::InvalidArgument("trailing values after the B-rep block".into()));
|
||||
}
|
||||
let mut input: Vec<f64> = vec![periodic_u, periodic_v];
|
||||
input.extend_from_slice(block);
|
||||
let cpath = std::ffi::CString::new(path)
|
||||
.map_err(|_| FfiError::InvalidArgument("path has a NUL byte".into()))?;
|
||||
call_with_buffer(4096, |buf, len| {
|
||||
// SAFETY: cpath is a valid NUL-terminated string; input is valid for input.len()
|
||||
// reads and holds the validated arguments then a validated B-rep block (exactly
|
||||
// what the shim reads); buf is valid for buf.len() writes and the shim honours
|
||||
// the capacity; len is valid.
|
||||
unsafe {
|
||||
sys_topology_loopuse_side_in_surface::sc_topology_loopuse_side_in_surface(
|
||||
cpath.as_ptr(),
|
||||
input.as_ptr(),
|
||||
input.len() as i64,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i64,
|
||||
&raw mut *len,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -453,3 +453,73 @@ mod sys {
|
||||
) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Most steps of an [`inner_shell_of`] body.
|
||||
pub const MAX_INNER_SHELL_STEPS: usize = 6;
|
||||
|
||||
/// `SmShell::IsInnerShellOf` of every pair of shells of a body built from `steps`
|
||||
/// (shim `topology_shell_inner.cpp`): `n_steps (op kind x y z a b c)*n_steps`, a box
|
||||
/// (`kind` 1, `SmApiCreateBox(origin, a, b, c)`) or a sphere (`kind` 2,
|
||||
/// `SmApiCreateSphere(center, a)`) per step, combined into the body by
|
||||
/// `SmApiBoolean(body, primitive, op)` with `op` union (1), intersection (2) or
|
||||
/// difference (3) (`op` of the first step ignored). The record is
|
||||
/// `n_regions (n_shells)*n_regions`, then per shell `IsInnerShellOf(NULL)` and
|
||||
/// `IsInnerShellOf(other)` for every other shell, in region and shell list order.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`FfiError::InvalidArgument`] for a malformed step list (sizes and coordinates must
|
||||
/// be finite, sizes in `[1e-3, 1e4]`, coordinates within `1e4`); [`FfiError::Status`]
|
||||
/// when a primitive or Boolean fails in the shim.
|
||||
pub fn inner_shell_of(steps: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
let (&n, rest) = steps.split_first().ok_or_else(|| invalid("no steps"))?;
|
||||
if n.fract() != 0.0 || !(1.0..=MAX_INNER_SHELL_STEPS as f64).contains(&n) {
|
||||
return Err(invalid("step count"));
|
||||
}
|
||||
if rest.len() != 8 * n as usize {
|
||||
return Err(invalid("step list length"));
|
||||
}
|
||||
for (i, s) in rest.as_chunks::<8>().0.iter().enumerate() {
|
||||
if s.iter().any(|x| !x.is_finite()) {
|
||||
return Err(invalid("step not finite"));
|
||||
}
|
||||
let &[op, kind, x, y, z, a, b, c] = s;
|
||||
if i > 0 && op != 1.0 && op != 2.0 && op != 3.0 {
|
||||
return Err(invalid("step operation"));
|
||||
}
|
||||
let sizes: &[f64] = match kind {
|
||||
1.0 => &[a, b, c],
|
||||
2.0 => &[a],
|
||||
_ => return Err(invalid("step kind")),
|
||||
};
|
||||
if sizes.iter().any(|d| !(1e-3..=1e4).contains(d)) || [x, y, z].iter().any(|v| v.abs() > 1e4)
|
||||
{
|
||||
return Err(invalid("step size or position"));
|
||||
}
|
||||
}
|
||||
call_with_buffer(4096, |buffer, len| {
|
||||
// SAFETY: steps is valid for steps.len() reads and holds a validated step list
|
||||
// (exactly what the shim reads); buffer is valid for buffer.len() writes and the
|
||||
// shim honours the capacity; len is valid.
|
||||
unsafe {
|
||||
sys_inner::sc_topology_shell_inner_shell_of(
|
||||
steps.as_ptr(),
|
||||
steps.len() as i64,
|
||||
buffer.as_mut_ptr(),
|
||||
buffer.len() as i64,
|
||||
&raw mut *len,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mod sys_inner {
|
||||
unsafe extern "C" {
|
||||
pub fn sc_topology_shell_inner_shell_of(
|
||||
input: *const f64,
|
||||
count: i64,
|
||||
output: *mut f64,
|
||||
capacity: i64,
|
||||
output_len: *mut i64,
|
||||
) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,3 +482,51 @@ pub fn eval(op: Op, path: &str, input: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
rd.done()?;
|
||||
raw(op, path, input)
|
||||
}
|
||||
|
||||
mod sys_topology_vertex_check_pointers {
|
||||
extern "C" {
|
||||
pub fn sc_topology_vertex_check_pointers(
|
||||
path: *const std::ffi::c_char,
|
||||
input: *const f64,
|
||||
n_in: i64,
|
||||
out: *mut f64,
|
||||
cap: i64,
|
||||
out_len: *mut i64,
|
||||
) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// `SmVertex::CheckPointers` of every vertex of the B-rep built from `block` (shim
|
||||
/// `topology_vertex_check_pointers.cpp`): `0 n flag*n` in traversal order, or `-1 status`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`FfiError::InvalidArgument`] for a malformed argument or B-rep block (the checks of
|
||||
/// [`super::topology_vertexuse::validate_block`], the whole input consumed);
|
||||
/// [`FfiError::Status`] for a shim failure.
|
||||
pub fn check_pointers(path: &str, block: &[f64]) -> Result<Vec<f64>, FfiError> {
|
||||
super::topology_vertexuse::validate_block(path, block)?;
|
||||
if super::topology_vertexuse::brep_block_at(block, path)? != block.len() {
|
||||
return Err(FfiError::InvalidArgument("trailing values after the B-rep block".into()));
|
||||
}
|
||||
let mut input: Vec<f64> = vec![];
|
||||
input.extend_from_slice(block);
|
||||
let cpath = std::ffi::CString::new(path)
|
||||
.map_err(|_| FfiError::InvalidArgument("path has a NUL byte".into()))?;
|
||||
call_with_buffer(4096, |buf, len| {
|
||||
// SAFETY: cpath is a valid NUL-terminated string; input is valid for input.len()
|
||||
// reads and holds the validated arguments then a validated B-rep block (exactly
|
||||
// what the shim reads); buf is valid for buf.len() writes and the shim honours
|
||||
// the capacity; len is valid.
|
||||
unsafe {
|
||||
sys_topology_vertex_check_pointers::sc_topology_vertex_check_pointers(
|
||||
cpath.as_ptr(),
|
||||
input.as_ptr(),
|
||||
input.len() as i64,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i64,
|
||||
&raw mut *len,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -239,6 +239,8 @@ pub struct UpstreamFixes {
|
||||
pub slice_coincident_faces_sheet_side: bool,
|
||||
/// After step 8, a Union, Difference or Intersection whose result is a manifold solid with more than two regions (the infinite region first) runs `SetRegionIsVoidFlagsForNestedSolids`, which puts each region's enclosing shell first (the fork's tangency-labs/smlib `abastran/void-shell-check`). It also rewrites the void flags by nesting from the infinite region, so where step 7 left flags that disagree with the nesting the result's void flags change too, not only its shell order (WR-265, #388; `plan/upstream-issues/WR-265--manifold-boolean-void-shell-first.md` and its 2026-10-26 addendum).
|
||||
pub boolean_outer_shell_first: bool,
|
||||
/// `SmShell::IsInnerShellOf` passes on the statuses it ignores (fork 4038552): `UNSURE` when the outer shell's closure, the B-rep's box, the sample points or a crossing face's point or normal cannot be computed; a ray that cannot be solved or classified decides nothing and the next ray is tried (WR-497, #1473).
|
||||
pub shell_inner_shell_of_statuses: bool,
|
||||
/// `SmEdge::ReverseOrientation` returns a failed curve reversal's status before installing the interval, flipping the edge uses or deleting their UV trims (WR-260, #521).
|
||||
pub edge_reversal_keeps_failed_edge: bool,
|
||||
/// `GetUVCornerBoundaries`, `GetStartPointUVLoopGap` and `sm_CalcDistAndDev` return failed curve/surface evaluations and point drops instead of reading unwritten or earlier outputs (WR-262, WR-263, WR-264, #521).
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
//! | [`BSplineCurve::subdivide_at_discontinuity`] | `SubdivideAtDiscontinuity` |
|
||||
//! | [`BSplineCurve::trim_gap`], [`GapFix`] | `TrimGap` |
|
||||
//! | [`BSplineCurve::assert_valid`] | `AssertValid` |
|
||||
//! | [`BSplineCurve::assert_no_periodic_jump`], [`assert_no_periodic_jump_of`] | `AssertNoPeriodicJump` |
|
||||
//!
|
||||
//! The other members of the file belong to `nurbs.b_spline_curve_p1`, `_p2` and
|
||||
//! `nurbs.b_spline_curve_n_lib`.
|
||||
@@ -79,6 +80,7 @@ use crate::error::{ignored, Result, SolidError};
|
||||
use crate::geom::assert_array::{labels, AssertArray, AssertReport, ObjectKey, ReportSpec};
|
||||
use crate::geom::axis2_placement::Axis2Placement;
|
||||
use crate::geom::extent1d::Extent1d;
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
use crate::geom::geom_utility::p1::{
|
||||
line_closest_point, line_point_distance, plane_point_distance, point_project_to_plane,
|
||||
segment_point_distance,
|
||||
@@ -1419,6 +1421,98 @@ impl BSplineCurve {
|
||||
|
||||
Ok(rtn)
|
||||
}
|
||||
|
||||
/// Whether the control polygon of this 2D (UV trim) curve stays clear of a jump
|
||||
/// across a periodic boundary of the surface domain `surface_uv_domain`
|
||||
/// (`AssertNoPeriodicJump`): `false` when two consecutive control points differ by
|
||||
/// at least nine tenths of the domain length in a direction the surface is closed
|
||||
/// in (`surface_closed_u` / `surface_closed_v`). A curve that is not 2D, a surface
|
||||
/// closed in neither direction and a two-point polygon are `true`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR` without a NURB (upstream makes one or dereferences `NULL`; a plain
|
||||
/// B-spline cannot make one); those of [`BSplineCurve::polygon`].
|
||||
pub fn assert_no_periodic_jump(
|
||||
&self,
|
||||
surface_closed_u: bool,
|
||||
surface_closed_v: bool,
|
||||
surface_uv_domain: &Extent2d,
|
||||
) -> Result<bool> {
|
||||
// return value
|
||||
let mut rtn = true;
|
||||
|
||||
// no work - not 2d, not periodic
|
||||
if self.dim() != 2 || (!surface_closed_u && !surface_closed_v) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// get curve control points in 2d euclidean space (the NURB is made by the
|
||||
// caller, `assert_no_periodic_jump_of`, for the kinds that can make one)
|
||||
let ivl = self.natural_interval();
|
||||
let polygon = self.polygon(&ivl)?;
|
||||
|
||||
// no work - single pair control point curves are assumed to not jump boundaries
|
||||
if polygon.len() == 2 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// get slightly shortened surface periods
|
||||
let period_u = 0.9 * surface_uv_domain.x_length();
|
||||
let period_v = 0.9 * surface_uv_domain.y_length();
|
||||
|
||||
// for every pair of control Points
|
||||
for pair in polygon.windows(2) {
|
||||
let (Some(a), Some(b)) = (pair.first(), pair.get(1)) else {
|
||||
continue;
|
||||
};
|
||||
let jump_u = (b.x - a.x).abs();
|
||||
let jump_v = (b.y - a.y).abs();
|
||||
|
||||
// look for a potential bad U Jump
|
||||
if surface_closed_u && jump_u >= period_u {
|
||||
rtn = false;
|
||||
}
|
||||
if surface_closed_v && jump_v >= period_v {
|
||||
rtn = false;
|
||||
}
|
||||
}
|
||||
|
||||
// all done
|
||||
Ok(rtn)
|
||||
}
|
||||
}
|
||||
|
||||
/// `pUVTrimCurve->AssertNoPeriodicJump(bClosedU, bClosedV, rSurfaceUVDomain)` for a
|
||||
/// curve of any B-spline kind: [`BSplineCurve::assert_no_periodic_jump`] of its
|
||||
/// `SmBSplineCurve` part, a line's NURB made on a copy first (`GetPolygon`'s
|
||||
/// `GetOrCreateGwNurbPointer`; upstream makes it on the curve itself).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] for a curve that is not an `SmBSplineCurve` (upstream's
|
||||
/// UV trim curves always are); those of [`BSplineCurve::assert_no_periodic_jump`].
|
||||
pub fn assert_no_periodic_jump_of(
|
||||
curve: &Curve,
|
||||
surface_closed_u: bool,
|
||||
surface_closed_v: bool,
|
||||
surface_uv_domain: &Extent2d,
|
||||
) -> Result<bool> {
|
||||
use crate::curve::curve::p2::CurveKind;
|
||||
let base = match curve.kind() {
|
||||
CurveKind::BSpline(c) => std::borrow::Cow::Borrowed(c),
|
||||
CurveKind::Line(c) => c.base_with_nurb()?,
|
||||
CurveKind::Ellipse(c) => std::borrow::Cow::Borrowed(c.base()),
|
||||
CurveKind::Parabola(c) => std::borrow::Cow::Borrowed(c.base()),
|
||||
CurveKind::Hyperbola(c) => std::borrow::Cow::Borrowed(c.base()),
|
||||
_ => {
|
||||
return Err(SolidError::invalid_input(
|
||||
"nurbs::b_spline_curve::p3::assert_no_periodic_jump",
|
||||
"the curve is not an SmBSplineCurve",
|
||||
))
|
||||
}
|
||||
};
|
||||
base.assert_no_periodic_jump(surface_closed_u, surface_closed_v, surface_uv_domain)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
//! | [`rebuild_uv_trim_curve`] | `SmEdgeuse::RebuildUVTrimCurve` |
|
||||
//! | [`check_and_set_uv_trim_curve`], [`CheckAndSetUvTrimCurve`] | `SmEdgeuse::CheckAndSetUVTrimCurve` |
|
||||
//! | [`notify`] | `SmEdgeuse::Notify` |
|
||||
//! | [`is_edgeuse_side_in_surface`], [`EdgeuseSide`] | `SmEdgeuse::IsEdgeuseSideInSurface` |
|
||||
//! | [`has_dog_leg_near_pole`], [`DogLeg`] | `SmEdgeuse::HasDogLegNearPole` |
|
||||
//! | `is_contained` (private) | `SM_IS_CONTAINED` (`SmMath.h`) |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Object model.** As [`super::p1`]: an edge use is a record in the [`Brep`]'s
|
||||
@@ -70,15 +73,15 @@ use crate::geom::local_solve_nd::{BoundaryHandler, LocalSolveNd};
|
||||
use crate::geom::object::{NotifyOperation, ObjectRef};
|
||||
use crate::geom::solution_array::SolutionEnd;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::{Point2, Point3, Vec3, EFF_ZERO, EFF_ZERO_SQRT, PI};
|
||||
use crate::math::{Point2, Point3, Vec2, Vec3, EFF_ZERO, EFF_ZERO_SQRT, PI};
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use crate::nurbs::knots::Side;
|
||||
use crate::surface::surface::p2::DropCurveFail;
|
||||
use crate::surface::surface::p7::drop_points_to_tangent_plane;
|
||||
use crate::surface::surface::Surface;
|
||||
use crate::topology::edgeuse::p1::{
|
||||
brep_handle, drop_curve_fail, edge, edge_brep, edge_is_lamina, face, faceuse, mate, radial,
|
||||
set_uv_trim_curve, uv_trim_curve, FindStepOff, UvTrimCurveArg,
|
||||
brep_handle, drop_curve_fail, edge, edge_brep, edge_is_lamina, face, faceuse, mate,
|
||||
orientation, radial, set_uv_trim_curve, uv_trim_curve, FindStepOff, UvTrimCurveArg,
|
||||
};
|
||||
use crate::topology::edgeuse::p3::{create_uv_trim_curve, evaluate_binormal, BinormalOptions};
|
||||
use crate::topology::topology::{self as topo, TopologyRecords};
|
||||
@@ -866,6 +869,470 @@ pub fn notify<R: TopologyRecords + ?Sized>(
|
||||
)
|
||||
}
|
||||
|
||||
// ---- side and pole checks (SmEdgeuse::IsEdgeuseSideInSurface, HasDogLegNearPole) ----------
|
||||
|
||||
/// `SM_IS_CONTAINED(c, a, b)` (`SmMath.h`): `c` inside or on `[a, b]` (either order),
|
||||
/// widened by `SmTol::GetScaledZero(a, b)`.
|
||||
fn is_contained(c: f64, a: f64, b: f64) -> bool {
|
||||
let tz = crate::geom::tol::scaled_zero2(&a, &b);
|
||||
((a <= c + tz) && (c - tz <= b)) || ((b <= c + tz) && (c - tz <= a))
|
||||
}
|
||||
|
||||
/// `SM_DEG2RAD(adeg)` = `adeg * (SM_PI / 180.0)`.
|
||||
fn deg_to_rad(deg: f64) -> f64 {
|
||||
deg * (PI / 180.0)
|
||||
}
|
||||
|
||||
/// The result of [`is_edgeuse_side_in_surface`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct EdgeuseSide {
|
||||
/// The return value: `true` when the inside side of the edge use lies in the
|
||||
/// surface's domain.
|
||||
pub in_surface: bool,
|
||||
/// `*pOptOnNaturalBoundary`: `true` unless a sample point off every natural boundary
|
||||
/// was found.
|
||||
pub on_natural_boundary: bool,
|
||||
}
|
||||
|
||||
/// `SmEdgeuse::IsEdgeuseSideInSurface(lSmpCount, pOptPeriodicU, pOptPeriodicV,
|
||||
/// pOptOnNaturalBoundary)`: drops `smp_count` points of the edge curve (in edge use
|
||||
/// order) onto the face's surface and classifies them against the natural UV domain's
|
||||
/// sides (to twice the surface zone tolerance mapped to UV in U and in V). A sample off
|
||||
/// every side answers `true` at once. Otherwise the edge use runs along a side: a
|
||||
/// degenerate edge (its sample box below twice the zone tolerance) is `true`, and else
|
||||
/// the UV steps must run the way that side's inside requires (all but one of them;
|
||||
/// for a periodic side without UV trim curve either way). An edge use without face,
|
||||
/// surface or edge is `true`. `periodic_u` / `periodic_v` override
|
||||
/// `SmSurface::IsPeriodic`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] for a dangling id; [`SolidError::InvalidInput`] for an edge
|
||||
/// without curve (upstream dereferences null); those of `IsPeriodic`, the curve and
|
||||
/// surface evaluations and `MapTo2d` that upstream does not ignore. The statuses of
|
||||
/// the evaluations and of `DropPoint` are ignored, as upstream ([`SolidError::Unported`]
|
||||
/// still propagates).
|
||||
pub fn is_edgeuse_side_in_surface(
|
||||
k: &Kernel,
|
||||
b: &Brep,
|
||||
eu: EdgeuseId,
|
||||
smp_count: u64,
|
||||
periodic_u: Option<bool>,
|
||||
periodic_v: Option<bool>,
|
||||
) -> Result<EdgeuseSide> {
|
||||
use crate::geom::extent2d::{SS_UMAX, SS_UMIN, SS_VMAX, SS_VMIN};
|
||||
use crate::geom::extent3d::Extent3d;
|
||||
use crate::nurbs::b_spline_surface::SurfParamType;
|
||||
use crate::surface::surface::p1::{map_to_2d_dir, zone_tol_3d};
|
||||
use crate::surface::surface::p5::DropPointOptions;
|
||||
|
||||
// init return and ouput vals
|
||||
let mut rtn = true;
|
||||
let mut out = EdgeuseSide {
|
||||
in_surface: true,
|
||||
on_natural_boundary: true,
|
||||
};
|
||||
|
||||
// locals
|
||||
let f = face(b, eu)?;
|
||||
let surface = match f {
|
||||
Some(f) => b.face(f)?.surface.as_ref(),
|
||||
None => None,
|
||||
};
|
||||
let e = edge(b, eu)?;
|
||||
|
||||
// no work - missing Face, Surface, or Edge
|
||||
let (Some(surface), Some(e)) = (surface, e) else {
|
||||
return Ok(out);
|
||||
};
|
||||
|
||||
// classify periodic surface
|
||||
let uv_domain = surface.natural_uv_domain_with_nurb(k)?;
|
||||
let periodic_u = match periodic_u {
|
||||
Some(p) => p,
|
||||
None => surface.is_periodic(k, &uv_domain, SurfParamType::U)?,
|
||||
};
|
||||
let periodic_v = match periodic_v {
|
||||
Some(p) => p,
|
||||
None => surface.is_periodic(k, &uv_domain, SurfParamType::V)?,
|
||||
};
|
||||
|
||||
// locals
|
||||
let edge_rec = b.edge(e)?;
|
||||
let edge_curve = edge_rec
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| null("IsEdgeuseSideInSurface: edge without curve"))?;
|
||||
let edge_interval = edge_rec.interval;
|
||||
let same = orientation(b, eu)? == OrientType::Same;
|
||||
let mut uv_drop_points: Vec<Point2> = Vec::new();
|
||||
let mut uv_classify = SS_UMIN | SS_VMIN | SS_UMAX | SS_VMAX;
|
||||
// `SmPoint2d` / `SmPoint3d` default to undefined coordinates
|
||||
let mut uv_drop_point = Point2::UNDEF;
|
||||
let mut uv_trim_curve_3d = Point3::UNDEF;
|
||||
let mut edge_point = Point3::UNDEF;
|
||||
let u_dir = Vec2::new(1.0, 0.0);
|
||||
let v_dir = Vec2::new(0.0, 1.0);
|
||||
let (mut up_u, mut down_u, mut up_v, mut down_v) = (0_u64, 0_u64, 0_u64, 0_u64);
|
||||
let mut edge_box_3d = Extent3d::default();
|
||||
let uv_trim_curve = uv_trim_curve(b, eu)?.0;
|
||||
|
||||
// Tol = 3d Surface Zone Tol mapped for each sample point to 2d through the surface
|
||||
// function
|
||||
let surf_zone_tol_3d = zone_tol_3d(k, surface)?;
|
||||
|
||||
// for every sample point - check for Edgeuse point on Surface boundary whose inside
|
||||
// direction points out of the Surface domain
|
||||
for ii in 0..smp_count {
|
||||
// param
|
||||
let last = smp_count.wrapping_sub(1);
|
||||
let norm_param = if same {
|
||||
if ii == 0 {
|
||||
0.0
|
||||
} else if ii == last {
|
||||
1.0
|
||||
} else {
|
||||
ii as f64 / last as f64
|
||||
}
|
||||
} else if ii == 0 {
|
||||
1.0
|
||||
} else if ii == last {
|
||||
0.0
|
||||
} else {
|
||||
1.0 - (ii as f64 / last as f64)
|
||||
};
|
||||
let param = edge_interval.evaluate(norm_param);
|
||||
|
||||
// Edge->3d Point (status ignored: a failed evaluation leaves the last point)
|
||||
if let Some(p) = ignored(edge_curve.evaluate_point(k, param))? {
|
||||
edge_point = p;
|
||||
}
|
||||
|
||||
// next Drop GuessUV = UVTrimCurve(dParam) or last Drop point
|
||||
let mut guess = None;
|
||||
if let Some(trim) = uv_trim_curve {
|
||||
if let Some(p) = ignored(trim.evaluate_point(k, param))? {
|
||||
uv_trim_curve_3d = p;
|
||||
}
|
||||
guess = Some(Point2::new(uv_trim_curve_3d.x, uv_trim_curve_3d.y));
|
||||
} else if ii > 0 {
|
||||
guess = uv_drop_points.last().copied();
|
||||
}
|
||||
|
||||
// Drop Point (status ignored)
|
||||
let opts = DropPointOptions {
|
||||
guess_uv: guess,
|
||||
operation: SolverOperation::Minimize,
|
||||
..DropPointOptions::default()
|
||||
};
|
||||
let drop = surface
|
||||
.drop_point::<crate::surface::surface::p4::NoObject>(k, edge_point, &uv_domain, &opts);
|
||||
match ignored(drop)? {
|
||||
// with a guess `rResultUV` starts as the guess; without one it is written
|
||||
// only by a successful drop
|
||||
Some(d) if d.success || guess.is_some() => uv_drop_point = d.result_uv,
|
||||
Some(_) => (),
|
||||
None => {
|
||||
if let Some(g) = guess {
|
||||
uv_drop_point = g;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tol 2d mapping of Surface ZoneTol3d
|
||||
let tol_2d_u = map_to_2d_dir(k, surf_zone_tol_3d, uv_drop_point, u_dir, surface)?;
|
||||
let tol_2d_v = map_to_2d_dir(k, surf_zone_tol_3d, uv_drop_point, v_dir, surface)?;
|
||||
|
||||
// classify sUVPoint - to XSectTol3d numerical tolerances mapped to 2d (diff in U
|
||||
// and V directions)
|
||||
let uv_loc_u =
|
||||
uv_domain.point_boundaries(uv_drop_point, 2.0 * tol_2d_u) & (SS_UMIN | SS_UMAX);
|
||||
let uv_loc_v =
|
||||
uv_domain.point_boundaries(uv_drop_point, 2.0 * tol_2d_v) & (SS_VMIN | SS_VMAX);
|
||||
let uv_loc = uv_loc_u | uv_loc_v;
|
||||
|
||||
// save the drop and classify data per sample point
|
||||
edge_box_3d.add_point(edge_point);
|
||||
uv_drop_points.push(uv_drop_point);
|
||||
|
||||
// accumulate the classifications and the UV incremental step directions
|
||||
uv_classify &= uv_loc;
|
||||
if let (Some(prev), Some(cur)) = (uv_drop_points.iter().rev().nth(1), uv_drop_points.last())
|
||||
{
|
||||
if prev.x < cur.x {
|
||||
up_u += 1;
|
||||
}
|
||||
if prev.x > cur.x {
|
||||
down_u += 1;
|
||||
}
|
||||
if prev.y < cur.y {
|
||||
up_v += 1;
|
||||
}
|
||||
if prev.y > cur.y {
|
||||
down_v += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// low work - found a classify point not on a boundary
|
||||
if uv_classify == 0 {
|
||||
// both sides of this edgeuse are inside at this point, so return TRUE
|
||||
out.on_natural_boundary = false;
|
||||
return Ok(out);
|
||||
}
|
||||
}
|
||||
|
||||
// any Edgeuse with an Edge which is degenerate (a database problem to be healed) is
|
||||
// classified as having a side inside the surface
|
||||
if edge_box_3d.max_dimension() < 2.0 * surf_zone_tol_3d {
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
// check direction for edges running a natural boundary (all but one of the
|
||||
// increments in the right direction; either way for a periodic side without UV trim
|
||||
// curve, as upstream)
|
||||
let need = smp_count.wrapping_sub(2);
|
||||
let no_trim = uv_trim_curve.is_none();
|
||||
if uv_classify & SS_UMIN != 0 {
|
||||
rtn = down_v >= need || (no_trim && periodic_u && up_v >= need);
|
||||
} else if uv_classify & SS_UMAX != 0 {
|
||||
rtn = up_v >= need || (no_trim && periodic_u && down_v >= need);
|
||||
} else if uv_classify & SS_VMIN != 0 {
|
||||
rtn = up_u >= need || (no_trim && periodic_v && down_u >= need);
|
||||
} else if uv_classify & SS_VMAX != 0 {
|
||||
rtn = down_u >= need || (no_trim && periodic_v && up_u >= need);
|
||||
}
|
||||
|
||||
// all done
|
||||
out.in_surface = rtn;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The result of [`has_dog_leg_near_pole`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DogLeg {
|
||||
/// The return value: `true` (a problem) when the UV trim curve's control polygon
|
||||
/// turns to follow a singular side of the surface domain.
|
||||
pub has_dog_leg: bool,
|
||||
/// `*pOptIndx`: the control point index at the first turn (unset upstream when there
|
||||
/// is none).
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
|
||||
/// `SmEdgeuse::HasDogLegNearPole(pOptIndx)`: whether the UV trim curve of the edge use
|
||||
/// (its own or its mate's) has a control polygon that turns near a singular side of the
|
||||
/// face surface's natural domain (within a fiftieth of the domain). A curve of the
|
||||
/// intersectors' form (cubic, piecewise C0) is tested only at its first and last
|
||||
/// Bezier segments with `smgu_IsDogLeg` (35 / 45 degrees); any other curve at every
|
||||
/// interior control point near a singular side, by the polygon's bend there (over 50
|
||||
/// degrees) with one leg within 10 degrees of the U direction and the other at 35 to
|
||||
/// 145 degrees to it. No UV trim curve, no face surface or no singular side is `false`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] for a dangling id; those of `GetSingularities` and of the
|
||||
/// curve's NURB (`SM_ERR` without one; upstream dereferences `NULL`);
|
||||
/// [`SolidError::InvalidInput`] for an intersector-form polygon of fewer than four
|
||||
/// points (upstream reads before the array) or a UV trim curve that is not an
|
||||
/// `SmBSplineCurve`.
|
||||
pub fn has_dog_leg_near_pole(k: &Kernel, b: &Brep, eu: EdgeuseId) -> Result<DogLeg> {
|
||||
use crate::curve::curve::CurveKind;
|
||||
use crate::geom::extent2d::{SS_NONE, SS_UMAX, SS_UMIN, SS_VMAX, SS_VMIN};
|
||||
use crate::geom::geom_utility::p1::is_dog_leg;
|
||||
use crate::math::rad_to_deg;
|
||||
use std::borrow::Cow;
|
||||
|
||||
let mut out = DogLeg {
|
||||
has_dog_leg: false,
|
||||
index: None,
|
||||
};
|
||||
|
||||
// no work - no UVTrimCurve to check
|
||||
let Some(uv_trim_curve) = uv_trim_curve(b, eu)?.0 else {
|
||||
return Ok(out);
|
||||
};
|
||||
|
||||
// no work - no Surface to check
|
||||
let Some(f) = face(b, eu)? else {
|
||||
return Ok(out);
|
||||
};
|
||||
let Some(surface) = b.face(f)?.surface.as_ref() else {
|
||||
return Ok(out);
|
||||
};
|
||||
|
||||
// rtn: SM_SS_NONE or an orof: SM_SS_UMIN, SM_SS_VMIN, SM_SS_UMAX, SM_SS_VMAX
|
||||
let sing = surface
|
||||
.get_singularities_of_face(k, crate::topology::face::p2::tolerance(b, f)?, false, false)?
|
||||
.singular;
|
||||
|
||||
// no work - no Singularities to check
|
||||
if sing == SS_NONE {
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
// locals
|
||||
let uv_domain = surface.natural_uv_domain_with_nurb(k)?;
|
||||
let tol_dist_u = uv_domain.x_length() / 50.0;
|
||||
let tol_dist_v = uv_domain.y_length() / 50.0;
|
||||
let u_min = uv_domain.u_min();
|
||||
let u_max = uv_domain.u_max();
|
||||
let v_min = uv_domain.v_min();
|
||||
let v_max = uv_domain.v_max();
|
||||
|
||||
// BSpline Control Points (GetPolygon makes a missing NURB)
|
||||
let base: Cow<'_, crate::nurbs::b_spline_curve::BSplineCurve> = match uv_trim_curve.kind() {
|
||||
CurveKind::BSpline(c) => Cow::Borrowed(c),
|
||||
CurveKind::Line(c) => c.base_with_nurb()?,
|
||||
CurveKind::Ellipse(c) => Cow::Borrowed(c.base()),
|
||||
CurveKind::Parabola(c) => Cow::Borrowed(c.base()),
|
||||
CurveKind::Hyperbola(c) => Cow::Borrowed(c.base()),
|
||||
_ => {
|
||||
return Err(null(
|
||||
"HasDogLegNearPole: UV trim curve is not an SmBSplineCurve",
|
||||
))
|
||||
}
|
||||
};
|
||||
let control_points = base.polygon(&base.natural_interval())?;
|
||||
let num = control_points.len();
|
||||
let uv = |i: usize| -> Result<Vec2> {
|
||||
control_points
|
||||
.get(i)
|
||||
.map(|p| Vec2::new(p.x, p.y))
|
||||
.ok_or_else(|| null("HasDogLegNearPole: control point index out of range"))
|
||||
};
|
||||
let on_sing = |p: &Point3| {
|
||||
let on_u = ((sing & SS_UMIN) != 0 && (p.x - u_min).abs() < tol_dist_u)
|
||||
|| ((sing & SS_UMAX) != 0 && (p.x - u_max).abs() < tol_dist_u);
|
||||
let on_v = ((sing & SS_VMIN) != 0 && (p.y - v_min).abs() < tol_dist_v)
|
||||
|| ((sing & SS_VMAX) != 0 && (p.y - v_max).abs() < tol_dist_v);
|
||||
(on_u, on_v)
|
||||
};
|
||||
|
||||
// Special case curves built by the intersectors and DropCurve (expected to be the
|
||||
// common case for UVTrimCurves)
|
||||
if base.is_intersector_approx()? {
|
||||
// tunable locals
|
||||
let perp_vec_deg = 35.0;
|
||||
let parallel_vec_deg = 45.0;
|
||||
|
||||
// see if start/end is on a singularity
|
||||
for ii in 0..2usize {
|
||||
// iter 0=Start Seg, 1=End Seg (`lNum - 4` is unsigned upstream)
|
||||
let indx = if ii == 0 {
|
||||
0
|
||||
} else {
|
||||
num.checked_sub(4)
|
||||
.ok_or_else(|| null("HasDogLegNearPole: fewer than four control points"))?
|
||||
};
|
||||
|
||||
// beg/end ControlPoint
|
||||
let cpt = control_points
|
||||
.get(indx + ii * 3)
|
||||
.ok_or_else(|| null("HasDogLegNearPole: control point index out of range"))?;
|
||||
|
||||
// check for control points near constant U / V Singularity boundaries
|
||||
let (on_sing_u, on_sing_v) = on_sing(cpt);
|
||||
|
||||
// when start/end UVTrimCurve segment starts/ends on Pole and has a dog-leg shape
|
||||
if (on_sing_u || on_sing_v)
|
||||
&& is_dog_leg(
|
||||
uv(indx)?,
|
||||
uv(indx + 1)?,
|
||||
uv(indx + 2)?,
|
||||
uv(indx + 3)?,
|
||||
perp_vec_deg,
|
||||
parallel_vec_deg,
|
||||
)
|
||||
{
|
||||
// set optional output
|
||||
out.index = Some(indx + 1);
|
||||
out.has_dog_leg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// UVTrimCurve was not created by an intersector or dropCurve
|
||||
|
||||
// for every control point - see if its near the singularity boundary
|
||||
let mut ii = 0usize;
|
||||
while ii < num && !out.has_dog_leg {
|
||||
let cpt = *control_points
|
||||
.get(ii)
|
||||
.ok_or_else(|| null("HasDogLegNearPole: control point index out of range"))?;
|
||||
|
||||
// check for control points near constant U / V Singularity boundaries
|
||||
let (do_u_check, do_v_check) = on_sing(&cpt);
|
||||
|
||||
// when control point is near singularity boundary
|
||||
if do_u_check || do_v_check {
|
||||
// for U and V checks
|
||||
for jj in 0..2 {
|
||||
// skip check when control point is not near constant U / V poles
|
||||
if jj == 0 && !do_u_check {
|
||||
continue;
|
||||
}
|
||||
if jj == 1 && !do_v_check {
|
||||
continue;
|
||||
}
|
||||
|
||||
// when control point is an interior control point
|
||||
if ii != 0 && ii + 1 < num {
|
||||
let last_cpt = *control_points.get(ii - 1).ok_or_else(|| {
|
||||
null("HasDogLegNearPole: control point index out of range")
|
||||
})?;
|
||||
let next_cpt = *control_points.get(ii + 1).ok_or_else(|| {
|
||||
null("HasDogLegNearPole: control point index out of range")
|
||||
})?;
|
||||
let last_vec = cpt - last_cpt;
|
||||
let next_vec = next_cpt - cpt;
|
||||
|
||||
// Angle in Control Polygon at target ControlPoint (a zero vector
|
||||
// gives 0, as upstream)
|
||||
let ang_vec_rad = last_vec.angle_between(next_vec).unwrap_or(0.0);
|
||||
|
||||
// Angles between ControlPolygon Next/Last Vecs and the
|
||||
// singularity direction (upstream sets [1 0 0] for both checks)
|
||||
let sing_vec = Vec3::new(1.0, 0.0, 0.0);
|
||||
let ang_last_pole_rad = last_vec.angle_between(sing_vec).unwrap_or(0.0);
|
||||
let ang_next_pole_rad = next_vec.angle_between(sing_vec).unwrap_or(0.0);
|
||||
|
||||
let ang_last_pole_deg = rad_to_deg(ang_last_pole_rad);
|
||||
let ang_next_pole_deg = rad_to_deg(ang_next_pole_rad);
|
||||
|
||||
// when ControlPolygon turns and one CtrlPolygon segment runs along
|
||||
// the singularity and one does not
|
||||
if ang_vec_rad > deg_to_rad(50.0)
|
||||
&& (((is_contained(ang_last_pole_deg, 0.0, 10.0)
|
||||
|| is_contained(ang_last_pole_deg, 170.0, 180.0))
|
||||
&& is_contained(ang_next_pole_deg, 35.0, 145.0))
|
||||
|| ((is_contained(ang_next_pole_deg, 0.0, 10.0)
|
||||
|| is_contained(ang_next_pole_deg, 170.0, 180.0))
|
||||
&& is_contained(ang_last_pole_deg, 35.0, 145.0)))
|
||||
{
|
||||
// call it a turn and quit
|
||||
out.has_dog_leg = true;
|
||||
out.index = Some(ii);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ii += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// all done
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Ignores a status upstream ignores ([`SolidError::Unported`] still propagates).
|
||||
fn ignored<T>(r: Result<T>) -> Result<Option<T>> {
|
||||
match r {
|
||||
Ok(v) => Ok(Some(v)),
|
||||
Err(e) if e.is_unported() => Err(e),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
//! | [`has_seam_problem`], [`SeamCheck`], [`HasSeamProblemOptions`] | `SmFace::HasSeamProblem` |
|
||||
//! | [`BrepSeamObjects`], `merge_seam_intervals` (crate) | the `SmObject`s of `HasSeamProblem` / `SplitAtSeam`'s `MergeIntervals` |
|
||||
//! | [`ClassifyProblem`] | `SmClassifyProblem` (`SmCurveClass.h`) |
|
||||
//! | [`assert_test_face37`] | `sm_AssertTestFace37` (file static) |
|
||||
//! | `loop_sample_uv_point` (private) | `sm_GetLoopSampleUVPoint` (file static) |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Object model.** As [`super::p1`]: free functions over the [`Brep`]'s arenas,
|
||||
@@ -2603,6 +2605,104 @@ pub(crate) fn has_seam_problem_in_brep_cached<O: LoopObject>(
|
||||
has_seam_problem_cached(k, b, f, &objects, opts, cache)
|
||||
}
|
||||
|
||||
// ---- AssertValid helpers (sm_GetLoopSampleUVPoint, sm_AssertTestFace37) -------------------
|
||||
|
||||
/// `sm_GetLoopSampleUVPoint(pLoop, rUVPoint)` (file static of `SmFace.cpp`): the UV
|
||||
/// point at the middle (`NormalizedEvaluate(0.5, TRUE, ..)`) of the first edge use of
|
||||
/// the loop that evaluates; `None` for a vertex loop and a loop whose edge uses all
|
||||
/// fail (a missing UV trim curve is made, as upstream).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] for a dangling id; those of the loop's edge-use walk;
|
||||
/// [`SolidError::Unported`] from the evaluation (its other failures move on to the next
|
||||
/// edge use, as upstream).
|
||||
fn loop_sample_uv_point(k: &Kernel, b: &mut Brep, l: LoopId) -> Result<Option<Point2>> {
|
||||
let edgeuses = match b.loop_(l)?.loopuse {
|
||||
Some(lu) => eu1::loopuse_edgeuses(b, lu)?,
|
||||
None => Vec::new(),
|
||||
};
|
||||
for eu in edgeuses {
|
||||
match eu3::normalized_evaluate(k, b, eu, 0.5, true, false, true) {
|
||||
Ok(e) => return Ok(Some(Point2::new(e.point.x, e.point.y))),
|
||||
Err(e) if e.is_unported() => return Err(e),
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
// arrive here for VertexLoops and Loops with no usable UVTrimCurve
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// `sm_AssertTestFace37(pFace)` (`SmFace::AssertValid` test 37): whether every inner
|
||||
/// loop of the face lies inside its outer loop (the first loop) and outside every other
|
||||
/// inner loop, judged at one sample UV point per inner loop ([`loop_sample_uv_point`])
|
||||
/// with `SmLoop::ContainsUVPoint` to the face tolerance mapped to UV. An inner loop
|
||||
/// outside or on the boundary of the outer loop, or inside another inner loop, fails;
|
||||
/// a face with fewer than two loops, an inner loop without a sample point and an
|
||||
/// unknown containment pass.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] for a dangling id; [`SolidError::InvalidInput`] for a face
|
||||
/// without surface that has a sampled inner loop (upstream dereferences null); those
|
||||
/// of [`crate::query::loop::contains_uv_point`] and `MapTo2d`.
|
||||
pub fn assert_test_face37(k: &Kernel, b: &mut Brep, f: FaceId) -> Result<bool> {
|
||||
use crate::geom::point_set::PointContainment;
|
||||
use crate::query::r#loop::contains_uv_point;
|
||||
|
||||
// locals
|
||||
let loops = super::p2::loops(b, f, false, None)?;
|
||||
let num_loops = loops.len();
|
||||
|
||||
// no work - a Face with fewer than 2 Loops has no nesting to check
|
||||
if num_loops < 2 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// the OuterLoop is the first Loop in the Faceuse Loopuse list
|
||||
let Some(&outer_loop) = loops.first() else {
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
let zone_tol_3d = super::p2::tolerance(b, f)?;
|
||||
|
||||
// for every InnerLoop
|
||||
for (ii, &inner_loop) in loops.iter().enumerate().skip(1) {
|
||||
let Some(sample_uv) = loop_sample_uv_point(k, b, inner_loop)? else {
|
||||
continue; // inconclusive - nothing to classify
|
||||
};
|
||||
|
||||
let uv_tol = {
|
||||
let surface = b.face(f)?.surface.as_ref().ok_or_else(|| {
|
||||
SolidError::invalid_input(OP, "AssertTestFace37: face without surface")
|
||||
})?;
|
||||
crate::surface::surface::p1::map_to_2d(k, zone_tol_3d, sample_uv, surface)?
|
||||
};
|
||||
|
||||
// every InnerLoop must lie inside the OuterLoop
|
||||
let containment = contains_uv_point(k, b, outer_loop, None, sample_uv, uv_tol)?.containment;
|
||||
if containment == PointContainment::Outside || containment == PointContainment::OnBoundary {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// no InnerLoop may lie inside another InnerLoop
|
||||
for (jj, &other) in loops.iter().enumerate().skip(1) {
|
||||
if jj == ii {
|
||||
continue;
|
||||
}
|
||||
if contains_uv_point(k, b, other, None, sample_uv, uv_tol)?.containment
|
||||
== PointContainment::Inside
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// arrive here when all InnerLoops are properly nested
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
//! | [`loop_is_connected_to`] | `SmLoop::IsConnectedTo` |
|
||||
//! | [`uv_corner_boundaries`], [`UvCorners`] | `SmLoop::GetUVCornerBoundaries` |
|
||||
//! | [`calc_quad_num`] | `sm_CalcQuadNum` (static) |
|
||||
//! | [`is_loopuse_side_in_surface`], [`LoopuseSide`] | `SmLoopuse::IsLoopuseSideInSurface` |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Object model.** Loops and loop uses are records in the [`Brep`]'s arenas; the
|
||||
@@ -89,7 +90,7 @@
|
||||
//! unsuccessful drop returns `SM_ERR`.
|
||||
//! - The `std::unordered_set` overloads of `GetEdges` / `GetVertices`,
|
||||
//! `GetEdgeusesAndVertexuses`, `CalculateBoundingBox`, `GetTurningAngDeg`,
|
||||
//! `IsLoopuseSideInSurface`, `SmLoop::GetMaxSameDimGap3d`, `IsLamina`, `IsManifold`,
|
||||
//! `SmLoop::GetMaxSameDimGap3d`, `IsLamina`, `IsManifold`,
|
||||
//! `IsEmbedded`, `ContainsUVPoint`, `RebuildFromEdgeCurves`, `AssertValid`, `Draw`
|
||||
//! and `Dump` are not part of this unit.
|
||||
|
||||
@@ -1460,6 +1461,67 @@ pub fn calc_quad_num(origin: Point3, test: Point3) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of [`is_loopuse_side_in_surface`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct LoopuseSide {
|
||||
/// The return value: `true` when every edge use's inside side lies in the surface.
|
||||
pub in_surface: bool,
|
||||
/// `*pOptOnNaturalBoundary`: some edge use tested runs on a natural boundary of the
|
||||
/// surface.
|
||||
pub on_natural_boundary: bool,
|
||||
/// `*pOptEdgeuseIndx`: the index of the first edge use that fails (`0` otherwise).
|
||||
pub edgeuse_index: usize,
|
||||
}
|
||||
|
||||
/// `SmLoopuse::IsLoopuseSideInSurface(pOptPeriodicU, pOptPeriodicV,
|
||||
/// pOptOnNaturalBoundary, pOptEdgeuseIndx)`: runs
|
||||
/// [`is_edgeuse_side_in_surface`](crate::topology::edgeuse::p2::is_edgeuse_side_in_surface)
|
||||
/// with 6 samples on the loop use's edge uses in order, stopping at the first that
|
||||
/// fails. A vertex loop use is `true`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`edgeuses`] and of the edge use test.
|
||||
pub fn is_loopuse_side_in_surface(
|
||||
k: &Kernel,
|
||||
b: &Brep,
|
||||
lu: LoopuseId,
|
||||
periodic_u: Option<bool>,
|
||||
periodic_v: Option<bool>,
|
||||
) -> Result<LoopuseSide> {
|
||||
// init return value
|
||||
let mut out = LoopuseSide {
|
||||
in_surface: true,
|
||||
on_natural_boundary: false,
|
||||
edgeuse_index: 0,
|
||||
};
|
||||
|
||||
// locals
|
||||
let smp_count = 6;
|
||||
let edgeuses = edgeuses(b, lu, None, None)?;
|
||||
|
||||
// for every edgeuse - check Bounding Edge Side status
|
||||
for (ii, &eu) in edgeuses.iter().enumerate() {
|
||||
// check and remember Edgeuse Side status
|
||||
let side = crate::topology::edgeuse::p2::is_edgeuse_side_in_surface(
|
||||
k, b, eu, smp_count, periodic_u, periodic_v,
|
||||
)?;
|
||||
if side.on_natural_boundary {
|
||||
out.on_natural_boundary = true;
|
||||
}
|
||||
out.in_surface &= side.in_surface;
|
||||
|
||||
// inform the public
|
||||
if !side.in_surface {
|
||||
out.edgeuse_index = ii;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// all done
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
//! | [`region_post_insert_shell`] | `SmOwningTopology::PostInsert` of a shell into a region (CO-PORTED, `topology.brep_p3`) |
|
||||
//! | [`shell_remove_faceuse`], [`shell_post_insert_faceuse`] | `SmOwningTopology::Remove` / `PostInsert` of one face use (CO-PORTED, `topology.brep_p3`) |
|
||||
//! | [`shell_list_size`] | `SmShell::GetSize` (`m_lListSize`; CO-PORTED, `topology.brep_p3`) |
|
||||
//! | [`is_inner_shell_of`], [`SM_TRUE`], [`SM_FALSE`], [`SM_UNSURE`] | `SmShell::IsInnerShellOf` (its `SmBoolean`) |
|
||||
//!
|
||||
//! Deviations: circular lists are typed vectors in head-first order, as in the
|
||||
//! shared Brep arena model. `GetWireEdges`, `GetMatedShells` and `RayFire` claim and
|
||||
@@ -1351,6 +1352,570 @@ pub trait RegionLookup {
|
||||
fn resolve_region(&self, object: crate::geom::attribute::ObjectKey) -> Result<RegionRef<'_>>;
|
||||
}
|
||||
|
||||
// ---- SmShell::IsInnerShellOf ------------------------------------------------------------
|
||||
|
||||
/// `TRUE` of an `SmBoolean`.
|
||||
pub const SM_TRUE: i32 = 1;
|
||||
/// `FALSE` of an `SmBoolean`.
|
||||
pub const SM_FALSE: i32 = 0;
|
||||
/// `UNSURE` (`2`) of an `SmBoolean`.
|
||||
pub const SM_UNSURE: i32 = 2;
|
||||
|
||||
/// The geometry behind the handles of [`is_inner_shell_of`]'s ray classification: the
|
||||
/// ray line (`CacheObject::Other`) and the B-rep's edges, faces and vertices.
|
||||
struct RayGeometry<'a> {
|
||||
b: &'a Brep,
|
||||
line: &'a crate::curve::curve::Curve,
|
||||
}
|
||||
|
||||
impl crate::intersect::point_class::ClassGeometry<crate::topology::brep_cache::CacheObject>
|
||||
for RayGeometry<'_>
|
||||
{
|
||||
fn curve(
|
||||
&self,
|
||||
o: crate::topology::brep_cache::CacheObject,
|
||||
) -> Option<&crate::curve::curve::Curve> {
|
||||
use crate::topology::brep_cache::CacheObject;
|
||||
match o {
|
||||
CacheObject::Other(_) => Some(self.line),
|
||||
CacheObject::Edge(e) | CacheObject::Curve(e) => self.b.edge(e).ok()?.curve.as_ref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn surface(
|
||||
&self,
|
||||
o: crate::topology::brep_cache::CacheObject,
|
||||
) -> Option<&crate::surface::surface::Surface> {
|
||||
use crate::topology::brep_cache::CacheObject;
|
||||
match o {
|
||||
CacheObject::Face(f) | CacheObject::Surface(f) => self.b.face(f).ok()?.surface.as_ref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn volume(
|
||||
&self,
|
||||
_o: crate::topology::brep_cache::CacheObject,
|
||||
) -> Option<&crate::surface::volume::Volume> {
|
||||
None
|
||||
}
|
||||
fn point(&self, o: crate::topology::brep_cache::CacheObject) -> Option<crate::math::Point3> {
|
||||
match o {
|
||||
crate::topology::brep_cache::CacheObject::Vertex(v) => {
|
||||
Some(self.b.vertex(v).ok()?.point)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn classification_vertex_point(
|
||||
&self,
|
||||
vertex: crate::topology::brep_cache::CacheObject,
|
||||
) -> Option<crate::math::Point3> {
|
||||
self.point(vertex)
|
||||
}
|
||||
fn topology_zone_tol_3d(
|
||||
&self,
|
||||
object: crate::topology::brep_cache::CacheObject,
|
||||
) -> Option<f64> {
|
||||
use crate::topology::brep_cache::CacheObject;
|
||||
match object {
|
||||
CacheObject::Vertex(v) => Some(self.b.vertex(v).ok()?.tolerance),
|
||||
CacheObject::Edge(e) => Some(self.b.edge(e).ok()?.tolerance),
|
||||
CacheObject::Face(f) => Some(self.b.face(f).ok()?.tolerance),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn topology_entity(
|
||||
&self,
|
||||
object: crate::topology::brep_cache::CacheObject,
|
||||
) -> Option<(&Brep, crate::error::EntityRef)> {
|
||||
use crate::error::EntityRef;
|
||||
use crate::topology::brep_cache::CacheObject;
|
||||
match object {
|
||||
CacheObject::Vertex(v) => Some((self.b, EntityRef::Vertex(v))),
|
||||
CacheObject::Edge(e) => Some((self.b, EntityRef::Edge(e))),
|
||||
CacheObject::Face(f) => Some((self.b, EntityRef::Face(f))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn topology_handle(
|
||||
&self,
|
||||
entity: crate::error::EntityRef,
|
||||
) -> Option<crate::topology::brep_cache::CacheObject> {
|
||||
use crate::error::EntityRef;
|
||||
use crate::topology::brep_cache::CacheObject;
|
||||
match entity {
|
||||
EntityRef::Vertex(v) => Some(CacheObject::Vertex(v)),
|
||||
EntityRef::Edge(e) => Some(CacheObject::Edge(e)),
|
||||
EntityRef::Face(f) => Some(CacheObject::Face(f)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `SmTopologySolver::BrepCurveSolve(pBrep, line, line.GetNaturalInterval(),
|
||||
/// SM_SO_INTERSECT, SM_SR_ALL, tol, 0.0, NULL, solutions)` on the B-rep's own cache.
|
||||
fn ray_brep_solve(
|
||||
k: &Kernel,
|
||||
b: &mut Brep,
|
||||
line: &crate::curve::curve::Curve,
|
||||
tol: f64,
|
||||
) -> Result<crate::geom::solution_array::SolutionArray<crate::topology::brep_cache::CacheObject>> {
|
||||
use crate::geom::attribute::ObjectKey;
|
||||
use crate::geom::global_solver::{SolutionRequested, SolverOperation};
|
||||
use crate::geom::object::ObjectRef;
|
||||
use crate::intersect::topology_solver::p1::{CurveRef, TopologySolver};
|
||||
use crate::intersect::topology_solver::p2::TopologySolveOptions;
|
||||
use crate::topology::brep_cache::CacheObject;
|
||||
let query = crate::query::brep::BrepQuery::of_brep(b);
|
||||
let cache = query.cache(k, b)?;
|
||||
let identity = query.effective_identity();
|
||||
let objects = crate::query::brep::LiveBrepObjects::new(b, &identity, Some(cache))?;
|
||||
TopologySolver::brep_curve_solve(
|
||||
k,
|
||||
&objects,
|
||||
Some(CacheObject::Brep),
|
||||
CurveRef {
|
||||
curve: line,
|
||||
handle: CacheObject::Other(ObjectRef {
|
||||
type_code: crate::curve::line::Line::TYPE,
|
||||
key: ObjectKey(0),
|
||||
}),
|
||||
},
|
||||
&line.natural_interval(),
|
||||
TopologySolveOptions {
|
||||
operation: SolverOperation::Intersect,
|
||||
requested: SolutionRequested::All,
|
||||
tolerance_3d: tol,
|
||||
best_answer_so_far_sq: 0.0,
|
||||
opt_vectors: &[],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// The outcome of a call whose status upstream ignores (see [`checked`]).
|
||||
enum Checked<T> {
|
||||
/// It succeeded.
|
||||
Value(T),
|
||||
/// It failed and the status is ignored, as upstream.
|
||||
Ignored,
|
||||
/// It failed and, with
|
||||
/// [`crate::kernel::UpstreamFixes::shell_inner_shell_of_statuses`], decides.
|
||||
Decides,
|
||||
}
|
||||
|
||||
/// The status-checked variant of an ignored upstream call: with
|
||||
/// [`crate::kernel::UpstreamFixes::shell_inner_shell_of_statuses`] an `Err` decides;
|
||||
/// without it the error is ignored, except [`SolidError::Unported`].
|
||||
fn checked<T>(k: &Kernel, r: Result<T>) -> Result<Checked<T>> {
|
||||
match r {
|
||||
Ok(v) => Ok(Checked::Value(v)),
|
||||
Err(e) if e.is_unported() => Err(e),
|
||||
Err(_) if k.fixes.shell_inner_shell_of_statuses => Ok(Checked::Decides),
|
||||
Err(_) => Ok(Checked::Ignored),
|
||||
}
|
||||
}
|
||||
|
||||
/// `SmShell::IsInnerShellOf(pOptOuterShell)`: whether shell `id` lies directly inside
|
||||
/// `outer` (simple parent-child containment, not nested deeper), or with `outer`
|
||||
/// `None` is an inner shell of the infinite region. `outer` must be a closed face-use
|
||||
/// shell (`ClosureTraversal` of its first face use; else [`SM_FALSE`]). Rays are cast
|
||||
/// from three sample points of each face use of the shell (along its normal), or from
|
||||
/// a wire shell's first edge or a vertex shell's vertex in random directions
|
||||
/// (`kernel.rng`, ND-1), against the whole B-rep; the first ray whose crossings are
|
||||
/// all simple face crossings decides, by the stack of shells it exits and enters.
|
||||
/// Returns [`SM_TRUE`], [`SM_FALSE`] or [`SM_UNSURE`] (no ray decided).
|
||||
///
|
||||
/// Upstream ignores the statuses of the closure traversal, the bounding box, the
|
||||
/// sample points, the ray solve, the classification insert and the crossing face's
|
||||
/// evaluations; the opt-in
|
||||
/// [`crate::kernel::UpstreamFixes::shell_inner_shell_of_statuses`] (fork 4038552)
|
||||
/// returns [`SM_UNSURE`] when the closure, the box, the sample points or a crossing
|
||||
/// face's point or normal cannot be computed, and goes on to the next ray when the ray
|
||||
/// cannot be solved or classified.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] for a dangling id; [`SolidError::InvalidInput`] where
|
||||
/// upstream dereferences null (a wire shell without edge or edge curve, a vertex shell
|
||||
/// without vertex, a crossing face without surface or face uses); the errors upstream
|
||||
/// does not ignore (default) or every error of the classification walk
|
||||
/// ([`SolidError::Unported`] always propagates).
|
||||
#[allow(clippy::too_many_lines)] // one upstream routine
|
||||
pub fn is_inner_shell_of(
|
||||
k: &mut Kernel,
|
||||
b: &mut Brep,
|
||||
id: ShellId,
|
||||
outer: Option<ShellId>,
|
||||
) -> Result<i32> {
|
||||
use crate::curve::curve::Curve;
|
||||
use crate::curve::line::{BoundedLineOptions, Line};
|
||||
use crate::geom::extent1d::Extent1d;
|
||||
use crate::geom::extent3d::Extent3d;
|
||||
use crate::geom::solution_array::{ObjectKind, SolutionObject, SolutionType};
|
||||
use crate::intersect::curve_class::p1::CurveClassification;
|
||||
use crate::intersect::curve_class::p4::ClassificationInit;
|
||||
use crate::math::{Point3, Vec3, EFF_ZERO};
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use crate::nurbs::knots::Side;
|
||||
use crate::topology::brep_cache::CacheObject;
|
||||
const OP_ISO: &str = "topology::shell::is_inner_shell_of";
|
||||
let null = |reason: &'static str| SolidError::invalid_input(OP_ISO, reason);
|
||||
|
||||
// check state - if given pOptOuterShell needs to be a Faceuse_TYPE shell that is
|
||||
// closed
|
||||
if let Some(outer) = outer {
|
||||
let mut is_closed = false;
|
||||
let outer_shell = b.shell(outer)?;
|
||||
if outer_shell.is_faceuse_shell() {
|
||||
let first = outer_shell.first_faceuse()?;
|
||||
// increment and lock any unlocked mark
|
||||
let lock = crate::geom::context::MarkLock::new(
|
||||
std::slice::from_mut(&mut k.context),
|
||||
0,
|
||||
ALL_MARKS,
|
||||
)?;
|
||||
let mut marks = Marks::default();
|
||||
let r = match first {
|
||||
Some(fu) => crate::topology::topology_traverser::closure_traversal(
|
||||
k, b, &mut marks, fu, &lock, None,
|
||||
),
|
||||
None => Err(SolidError::null_pointer(OP_ISO)),
|
||||
};
|
||||
let mut lock = lock;
|
||||
lock.release(std::slice::from_mut(&mut k.context))?;
|
||||
match checked(k, r)? {
|
||||
Checked::Value(c) => is_closed = c,
|
||||
Checked::Ignored => (),
|
||||
Checked::Decides => return Ok(SM_UNSURE),
|
||||
}
|
||||
}
|
||||
if !is_closed {
|
||||
return Ok(SM_FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
// get size based on Approx Brep bounding box
|
||||
let bbox = checked(
|
||||
k,
|
||||
qbrep::calculate_bounding_box(
|
||||
k,
|
||||
&mut *b,
|
||||
BoundingBoxOptions {
|
||||
approx: true,
|
||||
..BoundingBoxOptions::default()
|
||||
},
|
||||
),
|
||||
)?;
|
||||
let mut bbox = match bbox {
|
||||
Checked::Value(x) => x,
|
||||
Checked::Ignored => Extent3d::new(),
|
||||
Checked::Decides => return Ok(SM_UNSURE),
|
||||
};
|
||||
bbox.expand_absolute(1.0);
|
||||
let size = 2.0 * bbox.size().length();
|
||||
let tol = EFF_ZERO * (1.0 + size);
|
||||
|
||||
// search the faceuses of the shells for likely ray casts (those that hit only faces)
|
||||
let smp_cnt = 3u64;
|
||||
let shell = b.shell(id)?.clone();
|
||||
let inner_vertex = shell.vertex(b)?;
|
||||
let inner_faceuses = shell.get_faceuses()?;
|
||||
let inner_wire_edges = wire_edges(k, b, id)?;
|
||||
let is_fu_shell = shell.is_faceuse_shell();
|
||||
let is_wire_shell = shell.is_wire_shell();
|
||||
let is_vertex_shell = shell.is_vertex_shell();
|
||||
|
||||
let mut found_solution = false;
|
||||
let cnt = if is_fu_shell { inner_faceuses.len() } else { 1 };
|
||||
let mut smp_pv: Vec<Vec3> = Vec::new();
|
||||
let mut ii = 0;
|
||||
while ii < cnt && !found_solution {
|
||||
// sPV[0,2,4,...] = point to start ray for each SmpPoint,
|
||||
// sPV[1,3,5,...] = direction to run ray for each SmpPoint
|
||||
if is_fu_shell {
|
||||
let fu = *inner_faceuses
|
||||
.get(ii)
|
||||
.ok_or_else(|| null("face use index"))?;
|
||||
let fu_rec = b.faceuse(fu)?;
|
||||
let (f, orientation) = (
|
||||
fu_rec.face.ok_or_else(|| null("face use without face"))?,
|
||||
fu_rec.orientation,
|
||||
);
|
||||
// SmFaceuse::GetPointsInFace (which resets its outputs first; a failure
|
||||
// here leaves them empty)
|
||||
let r = crate::topology::face::p2::points_in_face(k, b, f, smp_cnt);
|
||||
match checked(k, r)? {
|
||||
Checked::Value(points) => {
|
||||
let mut pv = points.pv;
|
||||
if orientation == OrientType::Opposite {
|
||||
for n in pv.iter_mut().skip(1).step_by(2) {
|
||||
*n = -*n;
|
||||
}
|
||||
}
|
||||
smp_pv = pv;
|
||||
}
|
||||
Checked::Ignored => smp_pv.clear(),
|
||||
Checked::Decides => return Ok(SM_UNSURE),
|
||||
}
|
||||
} else if is_wire_shell {
|
||||
let e = *inner_wire_edges
|
||||
.first()
|
||||
.ok_or_else(|| null("wire shell without edge"))?;
|
||||
let edge = b.edge(e)?;
|
||||
let ivl = edge.interval;
|
||||
let curve = edge
|
||||
.curve
|
||||
.clone()
|
||||
.ok_or_else(|| null("wire edge without curve"))?;
|
||||
let mut point = Point3::UNDEF;
|
||||
for jj in 0..smp_cnt {
|
||||
// sample the curve and pick varying directions for the vector
|
||||
match checked(
|
||||
k,
|
||||
curve.evaluate_point(k, ivl.evaluate((jj + 1) as f64 / (smp_cnt + 1) as f64)),
|
||||
)? {
|
||||
Checked::Value(p) => point = p,
|
||||
Checked::Ignored => (),
|
||||
Checked::Decides => return Ok(SM_UNSURE),
|
||||
}
|
||||
let mut dir = Vec3::new(
|
||||
k.rng.next_signed_unit(),
|
||||
k.rng.next_signed_unit(),
|
||||
k.rng.next_signed_unit(),
|
||||
);
|
||||
dir = dir.unitize_or_z();
|
||||
smp_pv.push(point);
|
||||
smp_pv.push(dir);
|
||||
}
|
||||
} else if is_vertex_shell {
|
||||
let v = inner_vertex.ok_or_else(|| null("vertex shell without vertex"))?;
|
||||
let point = b.vertex(v)?.point;
|
||||
for _ in 0..smp_cnt {
|
||||
// pick varying directions for the vector
|
||||
let mut dir = Vec3::new(
|
||||
k.rng.next_signed_unit(),
|
||||
k.rng.next_signed_unit(),
|
||||
k.rng.next_signed_unit(),
|
||||
);
|
||||
dir = dir.unitize_or_z();
|
||||
smp_pv.push(point);
|
||||
smp_pv.push(dir);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
// for every sample point (a position and a direction vector)
|
||||
let mut jj = 0;
|
||||
while jj < smp_pv.len() {
|
||||
found_solution = true;
|
||||
let line_pnt = *smp_pv.get(jj).ok_or_else(|| null("sample point"))?;
|
||||
let line_vec = *smp_pv.get(jj + 1).ok_or_else(|| null("sample direction"))?;
|
||||
jj += 2;
|
||||
|
||||
// build a ray line
|
||||
let line = Curve::from(Line::bounded(
|
||||
line_pnt,
|
||||
line_vec,
|
||||
&Extent1d::from_min_max(0.0, size),
|
||||
&BoundedLineOptions {
|
||||
scale: 1.0,
|
||||
dim: 3,
|
||||
..BoundedLineOptions::default()
|
||||
},
|
||||
));
|
||||
|
||||
// intersect line with Brep
|
||||
let mut solutions = match checked(k, ray_brep_solve(k, b, &line, tol))? {
|
||||
Checked::Value(s) => s,
|
||||
Checked::Ignored => crate::geom::solution_array::SolutionArray::new(),
|
||||
Checked::Decides => continue,
|
||||
};
|
||||
|
||||
// keep things simple - only use classifications which have point
|
||||
// intersections with faces
|
||||
let mut kk = 0;
|
||||
while kk < solutions.len() {
|
||||
let sol = *solutions.get(kk).ok_or_else(|| null("solution index"))?;
|
||||
let object = sol.objects.get(1).copied().flatten();
|
||||
let is_kind = |kind: ObjectKind| object.is_some_and(|o| o.kind() == kind);
|
||||
|
||||
// keep the first solution - but ignore (by removing from the solutions
|
||||
// array) any subseqent wireedge or vertexshell intersections
|
||||
let ignore = sol.solution_type == SolutionType::SingleValue
|
||||
&& match object {
|
||||
Some(CacheObject::Edge(e)) => crate::topology::edge::is_wire(b, e)?,
|
||||
Some(CacheObject::Vertex(v)) => {
|
||||
crate::topology::vertex::is_shell_vertex(b, v)?
|
||||
|| crate::topology::vertex::is_wire_vertex(b, v)?
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if ignore {
|
||||
// remove this solution (upstream's `continue` then also steps past
|
||||
// the solution that moves into its slot)
|
||||
solutions.remove_at(kk, 1);
|
||||
kk += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip range solutions and intersections with anything but faces
|
||||
if sol.solution_type == SolutionType::RangeOfValues
|
||||
|| (kk == 0 && is_fu_shell && !is_kind(ObjectKind::Face))
|
||||
|| (kk == 0 && is_wire_shell && !is_kind(ObjectKind::Edge))
|
||||
|| (kk == 0 && is_vertex_shell && !is_kind(ObjectKind::Vertex))
|
||||
|| (kk > 0 && !is_kind(ObjectKind::Face))
|
||||
{
|
||||
// skip this solution
|
||||
found_solution = false;
|
||||
break;
|
||||
}
|
||||
kk += 1;
|
||||
}
|
||||
|
||||
// try again
|
||||
if !found_solution {
|
||||
continue;
|
||||
}
|
||||
|
||||
// load solution into a CurveClassification
|
||||
let line_handle = CacheObject::Other(crate::geom::object::ObjectRef {
|
||||
type_code: Line::TYPE,
|
||||
key: crate::geom::attribute::ObjectKey(0),
|
||||
});
|
||||
let mut curve_class = CurveClassification::with_curve(
|
||||
k,
|
||||
&ClassificationInit {
|
||||
curve: Some(line_handle),
|
||||
interval: line.natural_interval(),
|
||||
uv_curve: None,
|
||||
src_zone_tol_3d: crate::geom::tol::zone_tol3d(k, tol),
|
||||
opt_data_size: 0,
|
||||
is_curve_owned: false,
|
||||
is_curve_current: true,
|
||||
surface1: None,
|
||||
surface2: None,
|
||||
uv_curve2: None,
|
||||
ti: None,
|
||||
},
|
||||
)?;
|
||||
{
|
||||
let geometry = RayGeometry { b, line: &line };
|
||||
let r = curve_class.insert_topology_intersections(
|
||||
k,
|
||||
&geometry,
|
||||
solutions.as_slice(),
|
||||
0,
|
||||
);
|
||||
if matches!(checked(k, r)?, Checked::Decides) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// when the CurveClassification has only one interval then the ray moved into
|
||||
// the infinite region and sailed on out into space - we're done
|
||||
if curve_class.size() == 1 {
|
||||
// shell is an inner shell of the infinite region
|
||||
return Ok(if outer.is_none() { SM_TRUE } else { SM_FALSE });
|
||||
}
|
||||
|
||||
// classify by the unbalanced ray/shell crossings
|
||||
let mut shell_stack: Vec<Option<ShellId>> = Vec::new();
|
||||
|
||||
// for every classification but the first - it starts on the current faceuse
|
||||
for i in 1..curve_class.size() {
|
||||
let interval = curve_class.at(i)?;
|
||||
|
||||
// get the face intersection point
|
||||
let face = match interval.start.face_object() {
|
||||
Some(CacheObject::Face(f) | CacheObject::Surface(f)) => Some(f),
|
||||
_ => None,
|
||||
};
|
||||
let uv = interval.start.uv_param();
|
||||
let Some(face) = face else {
|
||||
// pFace->GetUpwardFaceuse() on NULL
|
||||
return Err(null("crossing without face"));
|
||||
};
|
||||
let up = b
|
||||
.face(face)?
|
||||
.faceuse
|
||||
.ok_or_else(|| null("face without face use"))?;
|
||||
let down = b
|
||||
.faceuse(up)?
|
||||
.mate
|
||||
.ok_or_else(|| null("face use without mate"))?;
|
||||
let up_shell = b.faceuse(up)?.shell;
|
||||
let down_shell = b.faceuse(down)?.shell;
|
||||
|
||||
// skip faces which are interior to a single shell
|
||||
if up_shell == down_shell {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Evaluate the pFace->Surface
|
||||
let surface = b
|
||||
.face(face)?
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or_else(|| null("crossing face without surface"))?;
|
||||
let pv0 = checked(k, surface.evaluate_point(k, uv))?;
|
||||
if matches!(pv0, Checked::Decides) {
|
||||
return Ok(SM_UNSURE);
|
||||
}
|
||||
let normal =
|
||||
match checked(k, surface.evaluate_normal(k, uv, Side::Left, Side::Left))? {
|
||||
Checked::Value(n) => n,
|
||||
Checked::Ignored => Vec3::UNDEF,
|
||||
Checked::Decides => return Ok(SM_UNSURE),
|
||||
};
|
||||
|
||||
// figure out which shell is being exited and which shell is being
|
||||
// entered
|
||||
let ordered = if normal.dot(line_vec) < 0.0 {
|
||||
[up_shell, down_shell]
|
||||
} else {
|
||||
[down_shell, up_shell]
|
||||
};
|
||||
|
||||
// parse the ordered shells into the shell stack
|
||||
for s in ordered {
|
||||
if shell_stack.last().is_some_and(|last| *last == s) {
|
||||
// this is a balanced intersection - remove the last stack member
|
||||
shell_stack.pop();
|
||||
} else {
|
||||
// this is an unbalanced intersection - add it to the stack
|
||||
shell_stack.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check the stack to pick the return value
|
||||
if shell_stack.is_empty() {
|
||||
// this is an inner shell of the infinite region
|
||||
return Ok(if outer.is_none() { SM_TRUE } else { SM_FALSE });
|
||||
}
|
||||
|
||||
// when pOptOuterShell == NULL we were testing for an infinite region inner
|
||||
// shell
|
||||
let Some(outer) = outer else {
|
||||
return Ok(SM_FALSE);
|
||||
};
|
||||
|
||||
// when pOptOuterShell == sShellStack[0] then this was an inner shell of the
|
||||
// given outer shell
|
||||
return Ok(if shell_stack.first().copied().flatten() == Some(outer) {
|
||||
SM_TRUE
|
||||
} else {
|
||||
SM_FALSE
|
||||
});
|
||||
}
|
||||
ii += 1;
|
||||
}
|
||||
|
||||
// arrive here when no candidate ray produced a simple set of intersections
|
||||
Ok(SM_UNSURE)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
//! | [`is_shell_vertex`], [`is_loop_vertex`] | `SmVertex::IsShellVertex`, `IsLoopVertex` |
|
||||
//! | [`max_up_dim_gap_3d`], [`max_gap_3d`], [`VertexGap`], [`HeldGap`] | `SmVertex::GetMaxUpDimGap3d`, `GetMaxGap3d` (the `SmGap*` results) |
|
||||
//! | [`notify`] | `SmVertex::Notify` |
|
||||
//! | [`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`) |
|
||||
//!
|
||||
@@ -80,7 +81,7 @@
|
||||
//! - `CalcCornerFromFaces`, `IsZoneTol3dConsistent`, `GetLoopOfFace`,
|
||||
//! `GetVertexuseOfEdge`, `GetVertexNumberInBrep`, the `Has*` / `IsEdgeVertex` /
|
||||
//! `IsOnSeam` / `IsConnectedToEdge` / `IsConnectedToFace` predicates,
|
||||
//! `IsWithinXSectTol3dOf*`, `IsOnFaceCorner`, `HasParallelFaces`, `CheckPointers`,
|
||||
//! `IsWithinXSectTol3dOf*`, `IsOnFaceCorner`, `HasParallelFaces`,
|
||||
//! `IsComplexCornerMember`, `RefineGeometry`, `FindAndRemoveVertexuseByEdge`,
|
||||
//! `GetMemoryUsed`, `AssertValid`, the graphics and the `Dump`s are not part of this
|
||||
//! unit.
|
||||
@@ -1255,6 +1256,349 @@ pub fn set_point(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- pointer check (SmVertex::CheckPointers) ------------------------------------------------
|
||||
|
||||
/// `SmVertex::CheckPointers()`: whether the topology around `v` is linked consistently,
|
||||
/// through each of its vertex uses in list order (stopping at the first failure): a
|
||||
/// shell vertex use is the vertex's only use and its shell's sole list member; a loop
|
||||
/// vertex use and the one of its mate loop use name this vertex, one loop, one face
|
||||
/// and mated loop uses and face uses that point back; an edge vertex use of a wire
|
||||
/// edge has mated shell edge uses of one shell; one of a loop edge has, with its mate
|
||||
/// and the corner neighbour at this vertex and its mate, one loop, one face, mated
|
||||
/// edge, loop and face uses that point back and consistent use types. Any other vertex
|
||||
/// use or edge use type fails.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] for a dangling id; [`SolidError::InvalidInput`] where the
|
||||
/// accessors upstream calls dereference null (an edge use without edge or primary edge
|
||||
/// use in `GetMate`, an edge use without vertex use or mate in
|
||||
/// `GetCornerMateEdgeuse`).
|
||||
pub fn check_pointers(b: &Brep, v: VertexId) -> Result<bool> {
|
||||
use crate::topology::shell::ShellMembers;
|
||||
use crate::topology::EdgeuseOwner;
|
||||
|
||||
// locals
|
||||
let mut rtn = true;
|
||||
let vertexuses = vertexuses(b, v)?;
|
||||
let lu_mate = |lu: LoopuseId| -> Result<Option<LoopuseId>> { Ok(b.loopuse(lu)?.mate) };
|
||||
let lu_vertexuse = |lu: LoopuseId| -> Result<Option<VertexuseId>> {
|
||||
Ok(match b.loopuse(lu)?.start {
|
||||
LoopuseStart::Vertexuse(vu) => vu,
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let lu_loop = |lu: LoopuseId| -> Result<Option<LoopId>> { Ok(b.loopuse(lu)?.loop_) };
|
||||
let lu_faceuse = |lu: LoopuseId| -> Result<Option<FaceuseId>> { Ok(b.loopuse(lu)?.faceuse) };
|
||||
let lu_is = |lu: LoopuseId, edge_type: bool| -> Result<bool> {
|
||||
Ok(match b.loopuse(lu)?.start {
|
||||
LoopuseStart::Edgeuse(_) => edge_type,
|
||||
LoopuseStart::Vertexuse(_) => !edge_type,
|
||||
LoopuseStart::Unknown => false,
|
||||
})
|
||||
};
|
||||
let fu_face = |fu: FaceuseId| -> Result<Option<FaceId>> { Ok(b.faceuse(fu)?.face) };
|
||||
let fu_mate = |fu: FaceuseId| -> Result<Option<FaceuseId>> { Ok(b.faceuse(fu)?.mate) };
|
||||
let fu_has = |fu: FaceuseId, lu: LoopuseId| -> Result<bool> {
|
||||
Ok(b.faceuse(fu)?.loopuses.contains(&lu))
|
||||
};
|
||||
let face_fu = |f: FaceId| -> Result<Option<FaceuseId>> { Ok(b.face(f)?.faceuse) };
|
||||
let loop_lu = |l: LoopId| -> Result<Option<LoopuseId>> { Ok(b.loop_(l)?.loopuse) };
|
||||
let vu_vertex = |vu: VertexuseId| -> Result<Option<VertexId>> { Ok(b.vertexuse(vu)?.vertex) };
|
||||
let vu_is_edge = |vu: VertexuseId| -> Result<bool> {
|
||||
Ok(matches!(b.vertexuse(vu)?.owner, VertexuseOwner::Edgeuse(_)))
|
||||
};
|
||||
let eu_is_loop = |eu: EdgeuseId| -> Result<bool> {
|
||||
Ok(matches!(b.edgeuse(eu)?.owner, EdgeuseOwner::Loopuse(_)))
|
||||
};
|
||||
let eu_is_shell = |eu: EdgeuseId| -> Result<bool> {
|
||||
Ok(matches!(b.edgeuse(eu)?.owner, EdgeuseOwner::Shell(_)))
|
||||
};
|
||||
let some_eq = |a: Option<VertexId>| a == Some(v);
|
||||
|
||||
// For every Face - find all the Vertexuses
|
||||
let mut ii = 0;
|
||||
while rtn && ii < vertexuses.len() {
|
||||
let Some(&vui) = vertexuses.get(ii) else {
|
||||
break;
|
||||
};
|
||||
ii += 1;
|
||||
|
||||
// switch on Vertexuse Type
|
||||
match b.vertexuse(vui)?.owner {
|
||||
VertexuseOwner::Shell(shell) => {
|
||||
// ShellVertex checks
|
||||
rtn &= shell.is_some();
|
||||
if let (true, Some(shell)) = (rtn, shell) {
|
||||
let members = &b.shell(shell)?.members;
|
||||
/* 1 */
|
||||
rtn &= vertexuses.len() == 1;
|
||||
/* 2 */
|
||||
rtn &= *members == ShellMembers::Vertexuse(vui);
|
||||
/* 3 (GetSize() == 1: the list of a vertex shell holds its one use) */
|
||||
rtn &= matches!(members, ShellMembers::Vertexuse(_));
|
||||
}
|
||||
}
|
||||
VertexuseOwner::Loopuse(lu1) => {
|
||||
let lu2 = match lu1 {
|
||||
Some(l) => lu_mate(l)?,
|
||||
None => None,
|
||||
};
|
||||
let vuj = match lu2 {
|
||||
Some(l) => lu_vertexuse(l)?,
|
||||
None => None,
|
||||
};
|
||||
let v1 = b.vertexuse(vui)?.vertex;
|
||||
let v2 = match vuj {
|
||||
Some(u) => vu_vertex(u)?,
|
||||
None => None,
|
||||
};
|
||||
let loop1 = match lu1 {
|
||||
Some(l) => lu_loop(l)?,
|
||||
None => None,
|
||||
};
|
||||
let loop2 = match lu2 {
|
||||
Some(l) => lu_loop(l)?,
|
||||
None => None,
|
||||
};
|
||||
let fu1 = match lu1 {
|
||||
Some(l) => lu_faceuse(l)?,
|
||||
None => None,
|
||||
};
|
||||
let fu2 = match lu2 {
|
||||
Some(l) => lu_faceuse(l)?,
|
||||
None => None,
|
||||
};
|
||||
let face1 = match fu1 {
|
||||
Some(f) => fu_face(f)?,
|
||||
None => None,
|
||||
};
|
||||
let face2 = match fu2 {
|
||||
Some(f) => fu_face(f)?,
|
||||
None => None,
|
||||
};
|
||||
|
||||
// LoopVertex checks
|
||||
let all = (lu1, lu2, vuj, v1, v2, loop1, loop2, fu1, fu2, face1, face2);
|
||||
rtn &= matches!(
|
||||
all,
|
||||
(
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(_)
|
||||
)
|
||||
);
|
||||
if let (
|
||||
true,
|
||||
(
|
||||
Some(lu1),
|
||||
Some(lu2),
|
||||
Some(vuj),
|
||||
Some(_),
|
||||
Some(_),
|
||||
Some(loop1),
|
||||
Some(_),
|
||||
Some(fu1),
|
||||
Some(fu2),
|
||||
Some(face1),
|
||||
Some(_),
|
||||
),
|
||||
) = (rtn, all)
|
||||
{
|
||||
/* 1 */
|
||||
rtn &= some_eq(v1) && some_eq(v2);
|
||||
/* 2 */
|
||||
rtn &= all.5 == all.6;
|
||||
/* 3 */
|
||||
rtn &= all.9 == all.10;
|
||||
/* 4 */
|
||||
rtn &= lu_mate(lu1)? == Some(lu2) && lu_mate(lu2)? == Some(lu1);
|
||||
/* 5 */
|
||||
rtn &= fu_mate(fu1)? == Some(fu2) && fu_mate(fu2)? == Some(fu1);
|
||||
/* 6 */
|
||||
let up = face_fu(face1)?;
|
||||
rtn &= up == Some(fu1) || up == Some(fu2);
|
||||
/* 7 */
|
||||
let l = loop_lu(loop1)?;
|
||||
rtn &= l == Some(lu1) || l == Some(lu2);
|
||||
/* 8 */
|
||||
rtn &= fu_has(fu1, lu1)?;
|
||||
/* 9 */
|
||||
rtn &= fu_has(fu2, lu2)?;
|
||||
/* 10 */
|
||||
rtn &= lu_vertexuse(lu1)? == Some(vui);
|
||||
/* 11 */
|
||||
rtn &= lu_vertexuse(lu2)? == Some(vuj);
|
||||
/* 12 */
|
||||
rtn &= some_eq(vu_vertex(vui)?) && some_eq(vu_vertex(vuj)?);
|
||||
/* 13 */
|
||||
rtn &= lu_is(lu1, false)? && lu_is(lu2, false)?;
|
||||
}
|
||||
}
|
||||
VertexuseOwner::Edgeuse(eu1_) => {
|
||||
let eu2_ = match eu1_ {
|
||||
Some(e) => eu1::mate(b, e)?,
|
||||
None => None,
|
||||
};
|
||||
let eu1_owner = match eu1_ {
|
||||
Some(e) => Some(b.edgeuse(e)?.owner),
|
||||
None => None,
|
||||
};
|
||||
|
||||
// switch on Edgeuse TYpe
|
||||
match eu1_owner {
|
||||
Some(EdgeuseOwner::Shell(_)) => {
|
||||
// EdgeVertex Part of a Shell
|
||||
let shell = match eu1_ {
|
||||
Some(e) => eu1::shell(b, e)?,
|
||||
None => None,
|
||||
};
|
||||
rtn &= eu1_.is_some() && eu2_.is_some() && shell.is_some();
|
||||
if let (true, Some(e1), Some(e2), Some(_)) = (rtn, eu1_, eu2_, shell) {
|
||||
/* 1 */
|
||||
rtn &= shell == eu1::shell(b, e1)? && shell == eu1::shell(b, e2)?;
|
||||
/* 2 */
|
||||
rtn &= eu1::mate(b, e1)? == Some(e2) && eu1::mate(b, e2)? == Some(e1);
|
||||
/* 3 */
|
||||
rtn &= eu1::vertexuse(b, e1)? == Some(vui);
|
||||
/* 4 */
|
||||
rtn &= eu_is_shell(e1)? && eu_is_shell(e2)?;
|
||||
}
|
||||
}
|
||||
Some(EdgeuseOwner::Loopuse(_)) => {
|
||||
// EdgeVertex Part of an Loopuse
|
||||
let eu3_ = match eu1_ {
|
||||
Some(e) => eu1::corner_mate_edgeuse(b, e, Some(v))?,
|
||||
None => None,
|
||||
};
|
||||
let eu4_ = match eu3_ {
|
||||
Some(e) => eu1::mate(b, e)?,
|
||||
None => None,
|
||||
};
|
||||
let eus = [eu1_, eu2_, eu3_, eu4_];
|
||||
let mut lus = [None; 4];
|
||||
let mut loops = [None; 4];
|
||||
let mut fus = [None; 4];
|
||||
let mut faces = [None; 4];
|
||||
let mut vus = [Some(vui), None, None, None];
|
||||
let mut vs = [None; 4];
|
||||
for (i, eu) in eus.iter().enumerate() {
|
||||
if let Some(e) = *eu {
|
||||
if let Some(slot) = lus.get_mut(i) {
|
||||
*slot = eu1::loopuse(b, e)?;
|
||||
}
|
||||
if let Some(slot) = fus.get_mut(i) {
|
||||
*slot = eu1::faceuse(b, e)?;
|
||||
}
|
||||
if i > 0 {
|
||||
if let Some(slot) = vus.get_mut(i) {
|
||||
*slot = eu1::vertexuse(b, e)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..4 {
|
||||
if let (Some(slot), Some(Some(l))) = (loops.get_mut(i), lus.get(i)) {
|
||||
*slot = lu_loop(*l)?;
|
||||
}
|
||||
if let (Some(slot), Some(Some(f))) = (faces.get_mut(i), fus.get(i)) {
|
||||
*slot = fu_face(*f)?;
|
||||
}
|
||||
if let (Some(slot), Some(Some(u))) = (vs.get_mut(i), vus.get(i)) {
|
||||
*slot = vu_vertex(*u)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Vertex->LoopEdge checks
|
||||
let present = eus.iter().all(Option::is_some)
|
||||
&& lus.iter().all(Option::is_some)
|
||||
&& loops.iter().all(Option::is_some)
|
||||
&& fus.iter().all(Option::is_some)
|
||||
&& faces.iter().all(Option::is_some)
|
||||
&& vus.iter().all(Option::is_some)
|
||||
&& vs.iter().all(Option::is_some);
|
||||
rtn &= present;
|
||||
if let (
|
||||
true,
|
||||
[Some(e1), Some(e2), Some(e3), Some(e4)],
|
||||
[Some(l1), Some(l2), Some(l3), Some(l4)],
|
||||
[Some(f1), Some(f2), _, _],
|
||||
[Some(loop1), _, _, _],
|
||||
[Some(face1), _, _, _],
|
||||
[Some(u1), Some(u2), Some(u3), Some(u4)],
|
||||
) = (rtn, eus, lus, fus, loops, faces, vus)
|
||||
{
|
||||
let [vi, _, _, vl] = vs;
|
||||
/* 1 */
|
||||
rtn &= some_eq(vi) && some_eq(vl);
|
||||
/* 2 */
|
||||
rtn &= loops.first() == loops.get(1)
|
||||
&& loops.first() == loops.get(2)
|
||||
&& loops.first() == loops.get(3);
|
||||
/* 3 */
|
||||
rtn &= faces.first() == faces.get(1)
|
||||
&& faces.first() == faces.get(2)
|
||||
&& faces.first() == faces.get(3);
|
||||
/* 4 */
|
||||
rtn &= eu1::mate(b, e1)? == Some(e2)
|
||||
&& eu1::mate(b, e2)? == Some(e1)
|
||||
&& eu1::mate(b, e3)? == Some(e4)
|
||||
&& eu1::mate(b, e4)? == Some(e3);
|
||||
/* 5 */
|
||||
rtn &= lu_mate(l1)? == Some(l2) && lu_mate(l2)? == Some(l1);
|
||||
/* 6 */
|
||||
rtn &= fu_mate(f1)? == Some(f2) && fu_mate(f2)? == Some(f1);
|
||||
/* 7 */
|
||||
let up = face_fu(face1)?;
|
||||
rtn &= up == Some(f1) || up == Some(f2);
|
||||
/* 8 */
|
||||
let l = loop_lu(loop1)?;
|
||||
rtn &= l == Some(l1) || l == Some(l2);
|
||||
/* 9 */
|
||||
rtn &= fu_has(f1, l1)?;
|
||||
/* 10 */
|
||||
rtn &= fu_has(f2, l2)?;
|
||||
/* 11 */
|
||||
rtn &= eu1::vertexuse(b, e1)? == Some(u1)
|
||||
&& eu1::vertexuse(b, e2)? == Some(u2)
|
||||
&& eu1::vertexuse(b, e3)? == Some(u3)
|
||||
&& eu1::vertexuse(b, e4)? == Some(u4);
|
||||
/* 12 */
|
||||
rtn &= vu_is_edge(u1)?
|
||||
&& vu_is_edge(u2)?
|
||||
&& vu_is_edge(u3)?
|
||||
&& vu_is_edge(u4)?;
|
||||
/* 13 */
|
||||
rtn &= eu_is_loop(e1)?
|
||||
&& eu_is_loop(e2)?
|
||||
&& eu_is_loop(e3)?
|
||||
&& eu_is_loop(e4)?;
|
||||
/* 14 */
|
||||
rtn &= lu_is(l1, true)?
|
||||
&& lu_is(l2, true)?
|
||||
&& lu_is(l3, true)?
|
||||
&& lu_is(l4, true)?;
|
||||
}
|
||||
}
|
||||
_ => rtn = false,
|
||||
}
|
||||
}
|
||||
VertexuseOwner::Unknown => rtn = false,
|
||||
}
|
||||
}
|
||||
|
||||
// all done
|
||||
Ok(rtn)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
//! The level-0 helpers of the topology `AssertValid` walk (#1419, WP2) on concrete
|
||||
//! bodies: `SmBSplineCurve::AssertNoPeriodicJump`, `SmEdgeuse::IsEdgeuseSideInSurface`
|
||||
//! and `HasDogLegNearPole`, `SmLoopuse::IsLoopuseSideInSurface`,
|
||||
//! `SmVertex::CheckPointers` and `sm_AssertTestFace37`.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use solidcore::boolean::api_brep as sm;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::a_object::BooleanOperation;
|
||||
use solidcore::geom::extent2d::{Extent2d, SS_NONE, SS_UMAX, SS_UMIN, SS_VMAX, SS_VMIN};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::math::Point3;
|
||||
use solidcore::nurbs::b_spline_curve::p1::{BSplineCurveForm, CanonicalData, KnotType};
|
||||
use solidcore::nurbs::b_spline_curve::p3::assert_no_periodic_jump_of;
|
||||
use solidcore::nurbs::b_spline_curve::BSplineCurve;
|
||||
use solidcore::primitive::api_primitives::{create_box, create_cylinder, create_sphere};
|
||||
use solidcore::topology::edgeuse::p1 as eu1;
|
||||
use solidcore::topology::edgeuse::p2::{has_dog_leg_near_pole, is_edgeuse_side_in_surface};
|
||||
use solidcore::topology::face::p4::assert_test_face37;
|
||||
use solidcore::topology::loopuse::is_loopuse_side_in_surface;
|
||||
use solidcore::topology::vertex::check_pointers;
|
||||
use solidcore::topology::{Brep, EdgeuseOwner, VertexuseOwner};
|
||||
|
||||
/// A degree-1 UV polyline through `pts`.
|
||||
fn polyline(pts: &[[f64; 2]]) -> BSplineCurve {
|
||||
let cps: Vec<Point3> = pts.iter().map(|p| Point3::new(p[0], p[1], 0.0)).collect();
|
||||
let n = cps.len();
|
||||
let knots: Vec<f64> = (0..n).map(|i| i as f64).collect();
|
||||
let mut mults = vec![1; n];
|
||||
mults[0] = 2;
|
||||
mults[n - 1] = 2;
|
||||
BSplineCurve::create_canonical(&CanonicalData {
|
||||
dim: 2,
|
||||
degree: 1,
|
||||
control_points: &cps,
|
||||
form: BSplineCurveForm::PolylineForm,
|
||||
knot_multiplicities: &mults,
|
||||
knots: &knots,
|
||||
knot_type: KnotType::Unspecified,
|
||||
weights: None,
|
||||
trim: None,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_no_periodic_jump_flags_a_control_polygon_crossing_the_period() {
|
||||
let dom = Extent2d::from_bounds(0.0, 0.0, 1.0, 1.0);
|
||||
let ok = Curve::from(polyline(&[[0.1, 0.0], [0.95, 0.5], [0.9, 1.0]]));
|
||||
let jump = Curve::from(polyline(&[[0.0, 0.0], [0.95, 0.5], [0.9, 1.0]]));
|
||||
let jump_v = Curve::from(polyline(&[[0.5, 0.0], [0.5, 0.95], [0.4, 1.0]]));
|
||||
let two = Curve::from(polyline(&[[0.0, 0.0], [1.0, 1.0]]));
|
||||
assert!(assert_no_periodic_jump_of(&ok, true, true, &dom).unwrap());
|
||||
assert!(!assert_no_periodic_jump_of(&jump, true, false, &dom).unwrap());
|
||||
// the jump is in U: not closed in U is no jump
|
||||
assert!(assert_no_periodic_jump_of(&jump, false, true, &dom).unwrap());
|
||||
assert!(!assert_no_periodic_jump_of(&jump_v, false, true, &dom).unwrap());
|
||||
assert!(assert_no_periodic_jump_of(&jump, false, false, &dom).unwrap());
|
||||
// two control points are assumed not to jump
|
||||
assert!(assert_no_periodic_jump_of(&two, true, true, &dom).unwrap());
|
||||
// scaled domain: the same polygon in a 1e3-scaled domain
|
||||
let big = Extent2d::from_bounds(0.0, 0.0, 1e3, 1e3);
|
||||
let jump_big = Curve::from(polyline(&[[0.0, 0.0], [950.0, 500.0], [900.0, 1e3]]));
|
||||
assert!(!assert_no_periodic_jump_of(&jump_big, true, false, &big).unwrap());
|
||||
// a 3D curve is never checked
|
||||
let line3 = Curve::from(
|
||||
BSplineCurve::create_line_segment(3, Point3::ZERO, Point3::new(1.0, 0.0, 0.0)).unwrap(),
|
||||
);
|
||||
assert!(assert_no_periodic_jump_of(&line3, true, true, &dom).unwrap());
|
||||
}
|
||||
|
||||
/// Whether the edge use lies on its face's upward face use (its loops run
|
||||
/// counter-clockwise around the face in UV).
|
||||
fn on_upward_faceuse(b: &Brep, eu: solidcore::topology::EdgeuseId) -> bool {
|
||||
let fu = eu1::faceuse(b, eu).unwrap().unwrap();
|
||||
let f = b.faceuse(fu).unwrap().face.unwrap();
|
||||
b.face(f).unwrap().faceuse == Some(fu)
|
||||
}
|
||||
|
||||
fn all_checks_pass(k: &mut Kernel, b: &mut Brep) {
|
||||
for v in b.vertices().ids().collect::<Vec<_>>() {
|
||||
assert!(check_pointers(b, v).unwrap(), "vertex {v:?}");
|
||||
}
|
||||
for eu in b.edgeuses().ids().collect::<Vec<_>>() {
|
||||
if !matches!(b.edgeuse(eu).unwrap().owner, EdgeuseOwner::Loopuse(_)) {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
!has_dog_leg_near_pole(k, b, eu).unwrap().has_dog_leg,
|
||||
"edgeuse {eu:?}"
|
||||
);
|
||||
if !on_upward_faceuse(b, eu) {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
is_edgeuse_side_in_surface(k, b, eu, 6, None, None)
|
||||
.unwrap()
|
||||
.in_surface,
|
||||
"edgeuse {eu:?}"
|
||||
);
|
||||
}
|
||||
// the loops' primary loop uses (what SmLoop::AssertValid test 2 checks)
|
||||
for l in b.loops().ids().collect::<Vec<_>>() {
|
||||
let lu = b.loop_(l).unwrap().loopuse.unwrap();
|
||||
assert!(
|
||||
is_loopuse_side_in_surface(k, b, lu, None, None)
|
||||
.unwrap()
|
||||
.in_surface
|
||||
);
|
||||
}
|
||||
for f in b.faces().ids().collect::<Vec<_>>() {
|
||||
assert!(assert_test_face37(k, b, f).unwrap(), "face {f:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_primitives_pass_every_level0_helper() {
|
||||
let mut k = Kernel::default();
|
||||
for scale in [1e-3, 1.0, 1e3] {
|
||||
let mut boxed =
|
||||
create_box(&mut k, Point3::ZERO, 2.0 * scale, 3.0 * scale, 4.0 * scale).unwrap();
|
||||
all_checks_pass(&mut k, &mut boxed);
|
||||
let mut sphere = create_sphere(&mut k, Point3::new(1.0, 2.0, 3.0), 5.0 * scale).unwrap();
|
||||
all_checks_pass(&mut k, &mut sphere);
|
||||
let mut cyl = create_cylinder(&mut k, Point3::ZERO, 2.0 * scale, 7.0 * scale).unwrap();
|
||||
all_checks_pass(&mut k, &mut cyl);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seam_edgeuses_run_on_a_natural_boundary() {
|
||||
let mut k = Kernel::default();
|
||||
let b = create_cylinder(&mut k, Point3::ZERO, 2.0, 7.0).unwrap();
|
||||
let mut on_boundary = 0;
|
||||
for eu in b.edgeuses().ids() {
|
||||
if !matches!(b.edgeuse(eu).unwrap().owner, EdgeuseOwner::Loopuse(_))
|
||||
|| !on_upward_faceuse(&b, eu)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let side = is_edgeuse_side_in_surface(&k, &b, eu, 6, None, None).unwrap();
|
||||
assert!(side.in_surface);
|
||||
on_boundary += usize::from(side.on_natural_boundary);
|
||||
// forcing the periodic flags changes nothing for a valid body
|
||||
let forced = is_edgeuse_side_in_surface(&k, &b, eu, 6, Some(true), Some(false)).unwrap();
|
||||
assert!(forced.in_surface);
|
||||
}
|
||||
assert!(
|
||||
on_boundary > 0,
|
||||
"no edge use of the cylinder runs on its domain boundary"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_uv_trim_curve_turning_along_a_pole_is_a_dog_leg() {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = create_sphere(&mut k, Point3::ZERO, 5.0).unwrap();
|
||||
// an edge use of the sphere face with its UV trim curve holder
|
||||
let (eu, holder) = b
|
||||
.edgeuses()
|
||||
.ids()
|
||||
.find_map(|eu| match eu1::uv_trim_curve(&b, eu).unwrap() {
|
||||
(Some(_), Some(h)) => Some((eu, h)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("a sphere edge use with a UV trim curve");
|
||||
let f = eu1::face(&b, eu).unwrap().unwrap();
|
||||
let surface = b.face(f).unwrap().surface.clone().unwrap();
|
||||
let tol = b.face(f).unwrap().tolerance;
|
||||
let sing = surface
|
||||
.get_singularities_of_face(&k, tol, false, false)
|
||||
.unwrap()
|
||||
.singular;
|
||||
assert_ne!(sing, SS_NONE);
|
||||
let d = surface.natural_uv_domain_with_nurb(&k).unwrap();
|
||||
let (u0, u1, v0, v1) = (d.u_min(), d.u_max(), d.v_min(), d.v_max());
|
||||
let (um, vm) = ((u0 + u1) / 2.0, (v0 + v1) / 2.0);
|
||||
let (lu, lv) = ((u1 - u0) / 10.0, (v1 - v0) / 10.0);
|
||||
// down to the pole, along it in U, back up: a turn at the pole
|
||||
let pts = if sing & SS_VMIN != 0 {
|
||||
[[um, vm], [um, v0], [um + lu, v0], [um + lu, vm]]
|
||||
} else if sing & SS_VMAX != 0 {
|
||||
[[um, vm], [um, v1], [um + lu, v1], [um + lu, vm]]
|
||||
} else if sing & SS_UMIN != 0 {
|
||||
[[um, vm], [u0, vm], [u0, vm + lv], [um, vm + lv]]
|
||||
} else {
|
||||
assert_ne!(sing & SS_UMAX, 0);
|
||||
[[um, vm], [u1, vm], [u1, vm + lv], [um, vm + lv]]
|
||||
};
|
||||
b.edgeuse_mut(holder).unwrap().uv_trim_curve = Some(Curve::from(polyline(&pts)));
|
||||
let dog = has_dog_leg_near_pole(&k, &b, eu).unwrap();
|
||||
// a U-direction pole run (VMIN/VMAX) turns at control point 1; a U pole is a V run,
|
||||
// which the U-direction test does not see
|
||||
if sing & (SS_VMIN | SS_VMAX) != 0 {
|
||||
assert!(dog.has_dog_leg);
|
||||
assert_eq!(dog.index, Some(1));
|
||||
}
|
||||
// a straight trim across the domain away from the poles is no dog leg
|
||||
b.edgeuse_mut(holder).unwrap().uv_trim_curve = Some(Curve::from(polyline(&[
|
||||
[um, vm],
|
||||
[um + lu, vm + lv],
|
||||
[um + 2.0 * lu, vm],
|
||||
])));
|
||||
assert!(!has_dog_leg_near_pole(&k, &b, eu).unwrap().has_dog_leg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_pointers_fails_on_a_broken_mate_link() {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = create_box(&mut k, Point3::ZERO, 1.0, 1.0, 1.0).unwrap();
|
||||
let v = b.vertices().ids().next().unwrap();
|
||||
assert!(check_pointers(&b, v).unwrap());
|
||||
let vu = b.vertex(v).unwrap().vertexuses[0];
|
||||
let VertexuseOwner::Edgeuse(Some(eu)) = b.vertexuse(vu).unwrap().owner else {
|
||||
panic!("box vertex use is not an edge vertex use");
|
||||
};
|
||||
let lu = eu1::loopuse(&b, eu).unwrap().unwrap();
|
||||
b.loopuse_mut(lu).unwrap().mate = None;
|
||||
assert!(!check_pointers(&b, v).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn face37_fails_when_the_outer_loop_is_not_first() {
|
||||
let mut k = Kernel::default();
|
||||
let block = create_box(&mut k, Point3::ZERO, 20.0, 20.0, 20.0).unwrap();
|
||||
let pin = create_cylinder(&mut k, Point3::new(10.0, 10.0, -5.0), 3.0, 30.0).unwrap();
|
||||
let mut b = sm::boolean(&mut k, block, pin, BooleanOperation::Difference)
|
||||
.unwrap()
|
||||
.result;
|
||||
let holed: Vec<_> = b
|
||||
.faces()
|
||||
.ids()
|
||||
.filter(|&f| {
|
||||
solidcore::topology::face::p2::loops(&b, f, false, None)
|
||||
.unwrap()
|
||||
.len()
|
||||
== 2
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(holed.len(), 2, "the pierced box has two faces with a hole");
|
||||
for &f in &holed {
|
||||
assert!(assert_test_face37(&k, &mut b, f).unwrap());
|
||||
}
|
||||
// the hole listed first: the square is then "inside" the circle, which it is not
|
||||
let f = holed[0];
|
||||
let fu = b.face(f).unwrap().faceuse.unwrap();
|
||||
b.faceuse_mut(fu).unwrap().loopuses.reverse();
|
||||
assert!(!assert_test_face37(&k, &mut b, f).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_downward_face_use_of_a_box_face_has_its_side_outside_the_surface() {
|
||||
// the mate face use's loops run clockwise in UV, so an edge use along a plane's
|
||||
// domain side has its inside side out of the domain
|
||||
let mut k = Kernel::default();
|
||||
let b = create_box(&mut k, Point3::ZERO, 2.0, 3.0, 4.0).unwrap();
|
||||
let mut outside = 0;
|
||||
for eu in b.edgeuses().ids() {
|
||||
if on_upward_faceuse(&b, eu) {
|
||||
continue;
|
||||
}
|
||||
let side = is_edgeuse_side_in_surface(&k, &b, eu, 6, None, None).unwrap();
|
||||
if !side.in_surface {
|
||||
assert!(side.on_natural_boundary);
|
||||
outside += 1;
|
||||
}
|
||||
}
|
||||
assert!(outside > 0);
|
||||
// the loop use reports the first failing edge use
|
||||
let lu = b
|
||||
.loopuses()
|
||||
.ids()
|
||||
.find(|&lu| {
|
||||
!is_loopuse_side_in_surface(&k, &b, lu, None, None)
|
||||
.unwrap()
|
||||
.in_surface
|
||||
})
|
||||
.expect("a box loop use with its side outside");
|
||||
let side = is_loopuse_side_in_surface(&k, &b, lu, None, None).unwrap();
|
||||
let eus = solidcore::topology::loopuse::edgeuses(&b, lu, None, None).unwrap();
|
||||
let first_bad = eus
|
||||
.iter()
|
||||
.position(|&eu| {
|
||||
!is_edgeuse_side_in_surface(&k, &b, eu, 6, None, None)
|
||||
.unwrap()
|
||||
.in_surface
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(side.edgeuse_index, first_bad);
|
||||
assert!(side.on_natural_boundary);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//! Regression test for #1473: `SmShell::IsInnerShellOf` on a box with a sealed internal
|
||||
//! void (the issue's repro: a 20 mm cube minus a 10 mm cube at (5, 5, 5)).
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use solidcore::boolean::api_brep as sm;
|
||||
use solidcore::geom::a_object::BooleanOperation;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::math::Vec3;
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::topology::shell::{is_inner_shell_of, ShellMembers, SM_FALSE, SM_TRUE, SM_UNSURE};
|
||||
use solidcore::topology::{brep::p1, Brep, ShellId};
|
||||
|
||||
/// The largest |coordinate| of the shell's face vertices: 20 for the outer wall, 15 for
|
||||
/// the cavity's.
|
||||
fn shell_extent(b: &Brep, s: ShellId) -> f64 {
|
||||
let mut m: f64 = 0.0;
|
||||
for &f in &b.shell(s).unwrap().faces {
|
||||
for &l in &b.face(f).unwrap().loops {
|
||||
for le in &b.loop_(l).unwrap().edges {
|
||||
for v in b.edge(le.edge).unwrap().vertices {
|
||||
let p = b.vertex(v).unwrap().point;
|
||||
m = m.max(p.x.abs()).max(p.y.abs()).max(p.z.abs());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
fn void_body(k: &mut Kernel) -> Brep {
|
||||
let outer = create_box(k, Vec3::new(0.0, 0.0, 0.0), 20.0, 20.0, 20.0).unwrap();
|
||||
let cavity = create_box(k, Vec3::new(5.0, 5.0, 5.0), 10.0, 10.0, 10.0).unwrap();
|
||||
sm::boolean(k, outer, cavity, BooleanOperation::Difference)
|
||||
.unwrap()
|
||||
.result
|
||||
}
|
||||
|
||||
fn check(k: &mut Kernel) {
|
||||
let mut b = void_body(k);
|
||||
let regions = p1::regions(&b, None).unwrap();
|
||||
let infinite = b.infinite_region();
|
||||
let mut solid_checked = false;
|
||||
for r in regions {
|
||||
let shells = b.region(r).unwrap().shells.clone();
|
||||
if Some(r) == infinite {
|
||||
// the infinite region's one shell (the 20 mm cube's outside) is an inner
|
||||
// shell of the infinite region
|
||||
assert_eq!(shells.len(), 1);
|
||||
assert_eq!(
|
||||
is_inner_shell_of(k, &mut b, shells[0], None).unwrap(),
|
||||
SM_TRUE
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if shells.len() == 1 {
|
||||
// the void: another shell contains it
|
||||
assert_eq!(shell_extent(&b, shells[0]), 15.0);
|
||||
assert_eq!(
|
||||
is_inner_shell_of(k, &mut b, shells[0], None).unwrap(),
|
||||
SM_FALSE
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// the solid region: the outer wall encloses the cavity's wall
|
||||
assert_eq!(shells.len(), 2);
|
||||
let (outer, cavity) = if shell_extent(&b, shells[0]) > shell_extent(&b, shells[1]) {
|
||||
(shells[0], shells[1])
|
||||
} else {
|
||||
(shells[1], shells[0])
|
||||
};
|
||||
assert_eq!(shell_extent(&b, outer), 20.0);
|
||||
assert_eq!(
|
||||
is_inner_shell_of(k, &mut b, cavity, Some(outer)).unwrap(),
|
||||
SM_TRUE
|
||||
);
|
||||
assert_eq!(
|
||||
is_inner_shell_of(k, &mut b, outer, Some(cavity)).unwrap(),
|
||||
SM_FALSE
|
||||
);
|
||||
solid_checked = true;
|
||||
}
|
||||
assert!(solid_checked, "no region with two shells");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1473_box_with_sealed_void_upstream_statuses() {
|
||||
let mut k = Kernel::default();
|
||||
check(&mut k);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1473_box_with_sealed_void_fork_statuses() {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.shell_inner_shell_of_statuses = true;
|
||||
check(&mut k);
|
||||
}
|
||||
|
||||
/// WR-497: an outer face-use shell whose closure traversal fails (its face-use list is
|
||||
/// empty, so there is no first face use). Upstream ignores the status and reads the
|
||||
/// shell as not closed (FALSE); with the opt-in fix the failure is UNSURE.
|
||||
fn failed_closure(k: &mut Kernel) -> i32 {
|
||||
let mut b = void_body(k);
|
||||
let solid = p1::regions(&b, None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|&r| b.region(r).unwrap().shells.len() == 2)
|
||||
.unwrap();
|
||||
let shells = b.region(solid).unwrap().shells.clone();
|
||||
let (outer, cavity) = if shell_extent(&b, shells[0]) > shell_extent(&b, shells[1]) {
|
||||
(shells[0], shells[1])
|
||||
} else {
|
||||
(shells[1], shells[0])
|
||||
};
|
||||
b.shell_mut(outer).unwrap().members = ShellMembers::Faceuses(Vec::new());
|
||||
is_inner_shell_of(k, &mut b, cavity, Some(outer)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1473_failed_closure_traversal_upstream_is_false() {
|
||||
let mut k = Kernel::default();
|
||||
assert_eq!(failed_closure(&mut k), SM_FALSE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1473_failed_closure_traversal_fix_is_unsure() {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.shell_inner_shell_of_statuses = true;
|
||||
assert_eq!(failed_closure(&mut k), SM_UNSURE);
|
||||
}
|
||||
@@ -26505,6 +26505,13 @@
|
||||
"lines": 500,
|
||||
"name": "SmBSplineCurve::AssertValid",
|
||||
"usr": "c:@S@SmBSplineCurve@F@AssertValid#*$@S@SmAssertArray#$@E@SmAssertTestLevel#$@E@SmAssertWalking#*$@S@SmTArray>#l#1"
|
||||
},
|
||||
{
|
||||
"file": "source/SMLib/src/SmBSplineCurve.cpp",
|
||||
"line": 8231,
|
||||
"lines": 69,
|
||||
"name": "SmBSplineCurve::AssertNoPeriodicJump",
|
||||
"usr": "c:@S@SmBSplineCurve@F@AssertNoPeriodicJump#c#c#&$@S@SmExtent2d#"
|
||||
}
|
||||
],
|
||||
"module": "nurbs",
|
||||
@@ -40027,6 +40034,20 @@
|
||||
"lines": 60,
|
||||
"name": "SmEdgeuse::Notify",
|
||||
"usr": "c:@S@SmEdgeuse@F@Notify#$@E@SmNotifyOperation#*$@S@SmObject#S1_#S1_#"
|
||||
},
|
||||
{
|
||||
"file": "source/SMLib/src/SmEdgeuse.cpp",
|
||||
"line": 871,
|
||||
"lines": 180,
|
||||
"name": "SmEdgeuse::IsEdgeuseSideInSurface",
|
||||
"usr": "c:@S@SmEdgeuse@F@IsEdgeuseSideInSurface#l#*c#S1_#S1_#1"
|
||||
},
|
||||
{
|
||||
"file": "source/SMLib/src/SmEdgeuse.cpp",
|
||||
"line": 3663,
|
||||
"lines": 233,
|
||||
"name": "SmEdgeuse::HasDogLegNearPole",
|
||||
"usr": "c:@S@SmEdgeuse@F@HasDogLegNearPole#*c#1"
|
||||
}
|
||||
],
|
||||
"module": "topology",
|
||||
@@ -40598,6 +40619,20 @@
|
||||
"lines": 865,
|
||||
"name": "SmFace::HasSeamProblem",
|
||||
"usr": "c:@S@SmFace@F@HasSeamProblem#&$@S@SmCurveClassification#S0_#&$@E@SmSurfParamType#S2_#S2_#I#*I#S4_#*$@S@SmFaceProps#1"
|
||||
},
|
||||
{
|
||||
"file": "source/SMLib/src/SmFace.cpp",
|
||||
"line": 20175,
|
||||
"lines": 25,
|
||||
"name": "sm_GetLoopSampleUVPoint",
|
||||
"usr": "c:SmFace.cpp@F@sm_GetLoopSampleUVPoint#*1$@S@SmLoop#&$@S@SmVector2d#"
|
||||
},
|
||||
{
|
||||
"file": "source/SMLib/src/SmFace.cpp",
|
||||
"line": 20226,
|
||||
"lines": 55,
|
||||
"name": "sm_AssertTestFace37",
|
||||
"usr": "c:@F@sm_AssertTestFace37#*1$@S@SmFace#"
|
||||
}
|
||||
],
|
||||
"module": "topology",
|
||||
@@ -40943,6 +40978,13 @@
|
||||
"lines": 4,
|
||||
"name": "SmLoopuse::IsKindOf",
|
||||
"usr": "c:@S@SmLoopuse@F@IsKindOf#L#1"
|
||||
},
|
||||
{
|
||||
"file": "source/SMLib/src/SmLoopuse.cpp",
|
||||
"line": 1034,
|
||||
"lines": 66,
|
||||
"name": "SmLoopuse::IsLoopuseSideInSurface",
|
||||
"usr": "c:@S@SmLoopuse@F@IsLoopuseSideInSurface#*c#S0_#S0_#*l#1"
|
||||
}
|
||||
],
|
||||
"module": "topology",
|
||||
@@ -42700,6 +42742,13 @@
|
||||
"lines": 4,
|
||||
"name": "SmShell::IsKindOf",
|
||||
"usr": "c:@S@SmShell@F@IsKindOf#L#1"
|
||||
},
|
||||
{
|
||||
"file": "source/SMLib/src/SmShell.cpp",
|
||||
"line": 827,
|
||||
"lines": 335,
|
||||
"name": "SmShell::IsInnerShellOf",
|
||||
"usr": "c:@S@SmShell@F@IsInnerShellOf#*1$@S@SmShell#1"
|
||||
}
|
||||
],
|
||||
"module": "topology",
|
||||
@@ -43675,6 +43724,13 @@
|
||||
"lines": 4,
|
||||
"name": "SmVertex::IsKindOf",
|
||||
"usr": "c:@S@SmVertex@F@IsKindOf#L#1"
|
||||
},
|
||||
{
|
||||
"file": "source/SMLib/src/SmVertex.cpp",
|
||||
"line": 1446,
|
||||
"lines": 262,
|
||||
"name": "SmVertex::CheckPointers",
|
||||
"usr": "c:@S@SmVertex@F@CheckPointers#1"
|
||||
}
|
||||
],
|
||||
"module": "topology",
|
||||
@@ -43905,4 +43961,4 @@
|
||||
"source/SMLib/src/SmVertexuse.cpp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// difftest shim for unit `nurbs.b_spline_curve_p3`.
|
||||
// Wraps the SmBSplineCurve members owned by the unit (source/SMLib/src/SmBSplineCurve.cpp,
|
||||
// source/SMLib/inc/SmBSplineCurve.h): CreateByScaleTransRot, HasInternalPole, IsSegmentLine,
|
||||
// GetControlPoint, IsOnPlane, JoinWith, SubdivideAtDiscontinuity, TrimGap and AssertValid.
|
||||
// GetControlPoint, IsOnPlane, JoinWith, SubdivideAtDiscontinuity, TrimGap, AssertValid and
|
||||
// AssertNoPeriodicJump.
|
||||
//
|
||||
// One entry point, sc_nurbs_b_spline_curve_p3_eval(op, in, n_in, out, cap, out_len). Layouts are
|
||||
// documented in crates/solidcore-ffi/src/units/nurbs_b_spline_curve_p3.rs, which validates every
|
||||
@@ -48,6 +49,7 @@ enum : int32_t
|
||||
OP_SUBDIVIDE = 6,
|
||||
OP_GAP = 7,
|
||||
OP_ASSERT = 8,
|
||||
OP_JUMP = 9,
|
||||
};
|
||||
|
||||
const double NaN = std::numeric_limits<double>::quiet_NaN();
|
||||
@@ -241,6 +243,20 @@ void curve_op(int32_t op, Rd& rd, const SmContext& ctx, SmBSplineCurve* c, std::
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OP_JUMP:
|
||||
{
|
||||
bool closed_u = rd.flag();
|
||||
bool closed_v = rd.flag();
|
||||
double u0 = rd.num();
|
||||
double v0 = rd.num();
|
||||
double u1 = rd.num();
|
||||
double v1 = rd.num();
|
||||
SmExtent2d dom(SmPoint2d(u0, v0), SmPoint2d(u1, v1));
|
||||
SmBoolean ok = c->AssertNoPeriodicJump(closed_u ? TRUE : FALSE, closed_v ? TRUE : FALSE, dom);
|
||||
o.push_back(0.0);
|
||||
o.push_back(ok ? 1.0 : 0.0);
|
||||
break;
|
||||
}
|
||||
default: o.push_back((double)SC_ERR_ARG); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// difftest shim for unit `topology.edgeuse_p2`: SmEdgeuse::HasDogLegNearPole and
|
||||
// SmEdgeuse::IsEdgeuseSideInSurface (source/SMLib/src/SmEdgeuse.cpp) on every edge use of a
|
||||
// real SmBrep.
|
||||
//
|
||||
// One entry point, sc_topology_edgeuse_p2_pole(path, in, n_in, out, cap, out_len). The input
|
||||
// is `smp_count periodic_u periodic_v` (`-1`: NULL, else the SmBoolean given) then the B-rep
|
||||
// block of unit topology.vertexuse (sc_topology_fixture.h; validated by crates/solidcore-ffi/
|
||||
// src/units/topology_edgeuse_p2.rs). Output: `0 n (dog_leg index side on_boundary)*n` (the
|
||||
// edge uses in the fixture's traversal order; `index` is `-1` when HasDogLegNearPole leaves
|
||||
// it unset), or `-1 status` when the B-rep cannot be built. `-2` marks a call skipped because
|
||||
// upstream reads out of bounds or dereferences null: HasDogLegNearPole of an
|
||||
// intersector-form UV trim curve with fewer than four control points (it reads
|
||||
// `lNum - 4`), IsEdgeuseSideInSurface of an edge without curve on a face with a surface.
|
||||
#include "sc_topology_fixture.h"
|
||||
|
||||
#include <SmEdgeuse.h>
|
||||
|
||||
SC_EXPORT int32_t sc_topology_edgeuse_p2_pole(const char* path, const double* in, int64_t n_in,
|
||||
double* out, int64_t cap, int64_t* out_len)
|
||||
{
|
||||
if (in == NULL || n_in < 4 || out_len == NULL || path == NULL)
|
||||
return SC_ERR_ARG;
|
||||
SC_TRY
|
||||
Rd rd{in, 0};
|
||||
ULONG smp = (ULONG)rd.idx();
|
||||
double pu = rd.num(), pv = rd.num();
|
||||
SmBoolean bu = pu == 1.0 ? TRUE : FALSE, bv = pv == 1.0 ? TRUE : FALSE;
|
||||
std::vector<double> o;
|
||||
SmStatus status = SM_SUCCESS;
|
||||
Built built;
|
||||
built.brep = build(rd, path, status);
|
||||
if (built.brep == nullptr)
|
||||
{
|
||||
o.push_back(-1.0);
|
||||
o.push_back(st(status));
|
||||
}
|
||||
else
|
||||
{
|
||||
o.push_back(0.0);
|
||||
Graph g;
|
||||
walk(built.brep, g);
|
||||
o.push_back((double)g.edgeuses.size());
|
||||
for (SmEdgeuse* eu : g.edgeuses)
|
||||
{
|
||||
SmBSplineCurve* trim = eu->GetUVTrimCurve();
|
||||
SmFace* f = eu->GetFace();
|
||||
SmSurface* s = f ? f->GetSurface() : NULL;
|
||||
bool dog_guard = false;
|
||||
if (trim != NULL && s != NULL && s->GetSingularities() != SM_SS_NONE && trim->IsIntersectorApprox())
|
||||
{
|
||||
SmTArray<SmPoint3d> poly;
|
||||
trim->GetPolygon(trim->GetNaturalInterval(), poly);
|
||||
dog_guard = poly.GetSize() < 4;
|
||||
}
|
||||
if (dog_guard)
|
||||
{
|
||||
o.push_back(-2.0);
|
||||
o.push_back(-2.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
SmBoolean idx = -1;
|
||||
SmBoolean dog = eu->HasDogLegNearPole(&idx);
|
||||
o.push_back(flg(dog));
|
||||
o.push_back((double)idx);
|
||||
}
|
||||
SmEdge* e = eu->GetEdge();
|
||||
if (s != NULL && e != NULL && e->GetCurve() == NULL)
|
||||
{
|
||||
o.push_back(-2.0);
|
||||
o.push_back(-2.0);
|
||||
continue;
|
||||
}
|
||||
SmBoolean on = FALSE;
|
||||
SmBoolean side = eu->IsEdgeuseSideInSurface(smp, pu < 0 ? NULL : &bu, pv < 0 ? NULL : &bv, &on);
|
||||
o.push_back(flg(side));
|
||||
o.push_back(flg(on));
|
||||
}
|
||||
}
|
||||
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
|
||||
SC_CATCH
|
||||
}
|
||||
@@ -12,12 +12,16 @@
|
||||
//
|
||||
// Output: `0` (or `-1 status` when the B-rep build failed), then the op record. A slot of
|
||||
// `-2` marks a call the shim skips because upstream would dereference null.
|
||||
// Op 11 (`Face37`) runs sm_AssertTestFace37 on the face.
|
||||
#include "sc_topology_fixture.h"
|
||||
|
||||
#include <SmCurveClass.h>
|
||||
#include <SmFaceProps.h>
|
||||
#include <SmSolutionArray.h>
|
||||
|
||||
// SmFace.cpp: the AssertValid rule 37 helper (external linkage, not declared in a header).
|
||||
SmBoolean sm_AssertTestFace37(const SmFace* pFace);
|
||||
|
||||
namespace
|
||||
{
|
||||
enum : int32_t
|
||||
@@ -32,6 +36,7 @@ enum : int32_t
|
||||
FP4_OP_LINE = 8,
|
||||
FP4_OP_SECTOR = 9,
|
||||
FP4_OP_SEAM = 10,
|
||||
FP4_OP_FACE37 = 11,
|
||||
};
|
||||
|
||||
const double FP4_NULL_DEREF = -2.0;
|
||||
@@ -309,6 +314,20 @@ void fp4_brep_op(int32_t op, Rd& rd, const char* path, std::vector<double>& o)
|
||||
fp4_put_class(o, cv);
|
||||
break;
|
||||
}
|
||||
case FP4_OP_FACE37:
|
||||
{
|
||||
// sm_AssertTestFace37 dereferences the face's surface once an inner loop has a
|
||||
// sample point
|
||||
SmTArray<SmLoop*> loops;
|
||||
f->GetLoops(loops);
|
||||
if (f->GetSurface() == NULL && loops.GetSize() >= 2)
|
||||
{
|
||||
o.push_back(FP4_NULL_DEREF);
|
||||
break;
|
||||
}
|
||||
o.push_back(flg(sm_AssertTestFace37(f)));
|
||||
break;
|
||||
}
|
||||
default: o.push_back((double)SC_ERR_ARG); break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// difftest shim for unit `topology.loopuse`: SmLoopuse::IsLoopuseSideInSurface (source/SMLib/
|
||||
// src/SmLoopuse.cpp) on every loop use of a real SmBrep.
|
||||
//
|
||||
// One entry point, sc_topology_loopuse_side_in_surface(path, in, n_in, out, cap, out_len). The
|
||||
// input is `periodic_u periodic_v` (`-1`: NULL, else the SmBoolean given) then the B-rep block
|
||||
// of unit topology.vertexuse (sc_topology_fixture.h; validated by crates/solidcore-ffi/src/
|
||||
// units/topology_loopuse.rs). Output: `0 n (side on_boundary index)*n` (the loop uses in the
|
||||
// fixture's traversal order; `-2 -2 -2` where an edge use's edge has no curve, which upstream
|
||||
// dereferences), or `-1 status` when the B-rep cannot be built.
|
||||
#include "sc_topology_fixture.h"
|
||||
|
||||
#include <SmLoopuse.h>
|
||||
|
||||
SC_EXPORT int32_t sc_topology_loopuse_side_in_surface(const char* path, const double* in, int64_t n_in,
|
||||
double* out, int64_t cap, int64_t* out_len)
|
||||
{
|
||||
if (in == NULL || n_in < 3 || out_len == NULL || path == NULL)
|
||||
return SC_ERR_ARG;
|
||||
SC_TRY
|
||||
Rd rd{in, 0};
|
||||
double pu = rd.num(), pv = rd.num();
|
||||
SmBoolean bu = pu == 1.0 ? TRUE : FALSE, bv = pv == 1.0 ? TRUE : FALSE;
|
||||
std::vector<double> o;
|
||||
SmStatus status = SM_SUCCESS;
|
||||
Built built;
|
||||
built.brep = build(rd, path, status);
|
||||
if (built.brep == nullptr)
|
||||
{
|
||||
o.push_back(-1.0);
|
||||
o.push_back(st(status));
|
||||
}
|
||||
else
|
||||
{
|
||||
o.push_back(0.0);
|
||||
Graph g;
|
||||
walk(built.brep, g);
|
||||
o.push_back((double)g.loopuses.size());
|
||||
for (SmLoopuse* lu : g.loopuses)
|
||||
{
|
||||
SmTArray<SmEdgeuse*> eus;
|
||||
lu->GetEdgeuses(eus);
|
||||
bool guard = false;
|
||||
for (ULONG i = 0; i < eus.GetSize(); i++)
|
||||
{
|
||||
SmFace* f = eus[i]->GetFace();
|
||||
SmEdge* e = eus[i]->GetEdge();
|
||||
if (f != NULL && f->GetSurface() != NULL && e != NULL && e->GetCurve() == NULL)
|
||||
guard = true;
|
||||
}
|
||||
if (guard)
|
||||
{
|
||||
o.insert(o.end(), {-2.0, -2.0, -2.0});
|
||||
continue;
|
||||
}
|
||||
SmBoolean on = FALSE;
|
||||
ULONG idx = 0;
|
||||
SmBoolean r = lu->IsLoopuseSideInSurface(pu < 0 ? NULL : &bu, pv < 0 ? NULL : &bv, &on, &idx);
|
||||
o.push_back(flg(r));
|
||||
o.push_back(flg(on));
|
||||
o.push_back((double)idx);
|
||||
}
|
||||
}
|
||||
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
|
||||
SC_CATCH
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// difftest shim for unit `topology.shell`: SmShell::IsInnerShellOf (source/SMLib/src/
|
||||
// SmShell.cpp) on every pair of shells of a B-rep with internal voids.
|
||||
//
|
||||
// One entry point, sc_topology_shell_inner_shell_of(in, n_in, out, cap, out_len). The input
|
||||
// is `n_steps (op kind x y z a b c)*n_steps`: step 0 makes the body (`op` ignored), each
|
||||
// later step builds a primitive and combines it into the body with
|
||||
// SmApiBoolean(body, primitive, op) (`op` an SmBooleanOperationType). `kind` 1 is
|
||||
// SmApiCreateBox(origin (x, y, z), a, b, c), 2 SmApiCreateSphere(center (x, y, z), a).
|
||||
// A failing constructor or Boolean is SC_ERR_ARG (validated inputs avoid them).
|
||||
// Output: `n_regions (n_shells)*n_regions` (SmBrep::GetRegions order, each region's shell
|
||||
// list order), then for every shell i in that order `IsInnerShellOf(NULL)` followed by
|
||||
// `IsInnerShellOf(shell j)` for every other shell j in order (the SmBoolean, 2 = UNSURE).
|
||||
#include "sc_shim.h"
|
||||
|
||||
#include "StdAfx.h"
|
||||
#include <SmApiBrep.h>
|
||||
#include <SmApiPrimitives.h>
|
||||
#include <SmBrep.h>
|
||||
#include <SmRegion.h>
|
||||
#include <SmShell.h>
|
||||
#include <SmTArray.h>
|
||||
#include <SmVector3d.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Bad
|
||||
{
|
||||
};
|
||||
|
||||
SmBrep* primitive(const double* s)
|
||||
{
|
||||
SmBrep* r = NULL;
|
||||
SmPoint3d o(s[2], s[3], s[4]);
|
||||
SmApiStatus stat = SM_ERR;
|
||||
if ((int)s[1] == 1)
|
||||
stat = SmApiCreateBox(o, s[5], s[6], s[7], r);
|
||||
else if ((int)s[1] == 2)
|
||||
stat = SmApiCreateSphere(o, s[5], r);
|
||||
if (stat != SM_SUCCESS || r == NULL)
|
||||
{
|
||||
delete r;
|
||||
throw Bad();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SC_EXPORT int32_t sc_topology_shell_inner_shell_of(const double* in, int64_t n_in, double* out, int64_t cap,
|
||||
int64_t* out_len)
|
||||
{
|
||||
if (in == NULL || n_in < 1 || out_len == NULL)
|
||||
return SC_ERR_ARG;
|
||||
int64_t n = (int64_t)in[0];
|
||||
if (n < 1 || n_in != 1 + 8 * n)
|
||||
return SC_ERR_ARG;
|
||||
SC_TRY
|
||||
SmBrep* body = NULL;
|
||||
try
|
||||
{
|
||||
body = primitive(in + 1);
|
||||
for (int64_t i = 1; i < n; i++)
|
||||
{
|
||||
const double* s = in + 1 + 8 * i;
|
||||
SmBrep* p = primitive(s);
|
||||
SmBrep* r = NULL;
|
||||
SmApiStatus stat = SmApiBoolean(body, p, (SmBooleanOperationType)(int32_t)s[0], r);
|
||||
if (stat != SM_SUCCESS || r != body)
|
||||
{
|
||||
delete p;
|
||||
throw Bad();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Bad&)
|
||||
{
|
||||
delete body;
|
||||
return SC_ERR_ARG;
|
||||
}
|
||||
std::vector<double> o;
|
||||
std::vector<SmShell*> shells;
|
||||
SmTArray<SmRegion*> regs;
|
||||
body->GetRegions(regs);
|
||||
o.push_back((double)regs.GetSize());
|
||||
for (ULONG r = 0; r < regs.GetSize(); r++)
|
||||
{
|
||||
SmTArray<SmShell*> sh;
|
||||
regs[r]->GetShells(sh);
|
||||
o.push_back((double)sh.GetSize());
|
||||
for (ULONG s = 0; s < sh.GetSize(); s++)
|
||||
shells.push_back(sh[s]);
|
||||
}
|
||||
for (size_t i = 0; i < shells.size(); i++)
|
||||
{
|
||||
o.push_back((double)shells[i]->IsInnerShellOf(NULL));
|
||||
for (size_t j = 0; j < shells.size(); j++)
|
||||
if (j != i)
|
||||
o.push_back((double)shells[i]->IsInnerShellOf(shells[j]));
|
||||
}
|
||||
delete body;
|
||||
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
|
||||
SC_CATCH
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// difftest shim for unit `topology.vertex`: SmVertex::CheckPointers (source/SMLib/src/
|
||||
// SmVertex.cpp) on every vertex of a real SmBrep.
|
||||
//
|
||||
// One entry point, sc_topology_vertex_check_pointers(path, in, n_in, out, cap, out_len). The
|
||||
// input is the B-rep block of unit topology.vertexuse (built by the shared
|
||||
// sc_topology_fixture.h; validated by crates/solidcore-ffi/src/units/topology_vertex.rs).
|
||||
// Output: `0 n flag*n` (the vertices in the fixture's traversal order), or `-1 status` when
|
||||
// the B-rep cannot be built.
|
||||
#include "sc_topology_fixture.h"
|
||||
|
||||
SC_EXPORT int32_t sc_topology_vertex_check_pointers(const char* path, const double* in, int64_t n_in,
|
||||
double* out, int64_t cap, int64_t* out_len)
|
||||
{
|
||||
if (in == NULL || n_in < 1 || out_len == NULL || path == NULL)
|
||||
return SC_ERR_ARG;
|
||||
SC_TRY
|
||||
Rd rd{in, 0};
|
||||
std::vector<double> o;
|
||||
SmStatus status = SM_SUCCESS;
|
||||
Built built;
|
||||
built.brep = build(rd, path, status);
|
||||
if (built.brep == nullptr)
|
||||
{
|
||||
o.push_back(-1.0);
|
||||
o.push_back(st(status));
|
||||
}
|
||||
else
|
||||
{
|
||||
o.push_back(0.0);
|
||||
Graph g;
|
||||
walk(built.brep, g);
|
||||
o.push_back((double)g.vertices.size());
|
||||
for (SmVertex* v : g.vertices)
|
||||
o.push_back(flg(v->CheckPointers()));
|
||||
}
|
||||
return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len);
|
||||
SC_CATCH
|
||||
}
|
||||
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin
|
||||
status = "green"
|
||||
claimed_by = "swarm-nurbs.b_spline_curve_p3-a1"
|
||||
claimed_at = "2026-10-13T00:13:51Z"
|
||||
notes = "GREEN: difftest 2195 cases, 2195 passed, 0 deferred: the 6 cases deferred on SmCurve::GetNaturalInterval (TrimGap's GlobalCurveIntersect named curves whose handles reported no natural interval) pass since the handles carry the curves' natural intervals; difftest partial: 2195 cases, 2036 passed, 159 deferred (curve.curve_p5 159) after curve.curve_p8 wired SmCurve::GlobalCurveIntersect (TrimGap); difftest partial: 2195 cases, 1995 passed, 200 deferred (curve.curve_p7 98, curve.curve_p8 56, curve.curve_p4 46) after curve.curve_cache co-ported SmCurve::IsLine; difftest partial: 2195 cases, 1836 passed, 359 deferred (curve.curve_p5 188, curve.curve_p7 69, curve.curve_p8 56, curve.curve_p4 46) after curve.curve_p2 wired its stubs (was 966 deferred, curve.curve_p2 734); 9 fns; largest: SmBSplineCurve::AssertValid, SmBSplineCurve::JoinWith, SmBSplineCurve::TrimGap, SmBSplineCurve::IsSegmentLine; cycle back-edges (call into later units; stub or co-port): curve.curve_p2(6), curve.line(3), curve.curve_p4(1), curve.curve_p7(1), curve.curve_p8(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit; after curve.curve_p4 (SmCurve::AssertValid wired): 2195 cases, 2023 pass, 172 deferred (curve.curve_p7 116, curve.curve_p8 56); after curve.curve_p7 (GlobalPropertyAnalysis wired): 2195 cases, 2023 pass, 172 deferred (curve.curve_p5 116, curve.curve_p8 56)"
|
||||
notes = "#1419 WP2 (PR #1472): SmBSplineCurve::AssertNoPeriodicJump -> BSplineCurve::assert_no_periodic_jump; difftest 2435 cases, 2435 passed, 0 deferred (240 new jump cases). GREEN: difftest 2195 cases, 2195 passed, 0 deferred: the 6 cases deferred on SmCurve::GetNaturalInterval (TrimGap's GlobalCurveIntersect named curves whose handles reported no natural interval) pass since the handles carry the curves' natural intervals; difftest partial: 2195 cases, 2036 passed, 159 deferred (curve.curve_p5 159) after curve.curve_p8 wired SmCurve::GlobalCurveIntersect (TrimGap); difftest partial: 2195 cases, 1995 passed, 200 deferred (curve.curve_p7 98, curve.curve_p8 56, curve.curve_p4 46) after curve.curve_cache co-ported SmCurve::IsLine; difftest partial: 2195 cases, 1836 passed, 359 deferred (curve.curve_p5 188, curve.curve_p7 69, curve.curve_p8 56, curve.curve_p4 46) after curve.curve_p2 wired its stubs (was 966 deferred, curve.curve_p2 734); 9 fns; largest: SmBSplineCurve::AssertValid, SmBSplineCurve::JoinWith, SmBSplineCurve::TrimGap, SmBSplineCurve::IsSegmentLine; cycle back-edges (call into later units; stub or co-port): curve.curve_p2(6), curve.line(3), curve.curve_p4(1), curve.curve_p7(1), curve.curve_p8(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit; after curve.curve_p4 (SmCurve::AssertValid wired): 2195 cases, 2023 pass, 172 deferred (curve.curve_p7 116, curve.curve_p8 56); after curve.curve_p7 (GlobalPropertyAnalysis wired): 2195 cases, 2023 pass, 172 deferred (curve.curve_p5 116, curve.curve_p8 56)"
|
||||
|
||||
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin
|
||||
status = "green"
|
||||
claimed_by = ""
|
||||
claimed_at = ""
|
||||
notes = "GREEN, PR #563 on main b65597902: 4583 native cases, 4583 bit-identical passes, 0 failed, 0 deferred; all 4193 main passes retained and 390 deferrals now pass. Verified by x86_64 CI 39928207576 and full ARM64 audit. Coverage: 2360 cases on 15 upstream .smb B-reps with rebuild/validate-only variants, 485 constructed B-rep cases, 1722 adversarial fixtures at scales 1e-4..1e4 with rational/pinched/sub-domain surfaces and tolerance-limit UV shifts, plus 16 generated cases. Shared graph export/import is topology.edgeuse_p1 (sc_edgeuse_graph.h), including PR #304's exact revolution export; historical inexact analytic imports and dependency deferrals are documented in findings.md. Historical perturbation was sensitive. WR-84 (IsConvexRadialSector SER as TRUE) and WR-85 (CheckAndSetUVTrimCurve deletes caller curve) retained. Eight functions, largest EvaluateBinormalStepOff, CheckAndSetUVTrimCurve, IsTangentSector, IsConvexRadialSector; cycle back-edges topology.edge(9), heal.gap(5), topology.edgeuse_p3(4), blend.fillet_geom_p1(1), topology.brep_p1(1); unit-SCC #22 (220 units); 317 upstream test entry points."
|
||||
notes = "#1419 WP2 (PR #1472): SmEdgeuse::HasDogLegNearPole and IsEdgeuseSideInSurface -> topology::edgeuse::p2::{has_dog_leg_near_pole, is_edgeuse_side_in_surface}; difftest 4795 cases, 4795 passed, 0 deferred (212 new level0 cases over every edge use; shim skips (-2) the upstream out-of-bounds read for intersector polygons of < 4 points and the null-curve dereference). GREEN, PR #563 on main b65597902: 4583 native cases, 4583 bit-identical passes, 0 failed, 0 deferred; all 4193 main passes retained and 390 deferrals now pass. Verified by x86_64 CI 39928207576 and full ARM64 audit. Coverage: 2360 cases on 15 upstream .smb B-reps with rebuild/validate-only variants, 485 constructed B-rep cases, 1722 adversarial fixtures at scales 1e-4..1e4 with rational/pinched/sub-domain surfaces and tolerance-limit UV shifts, plus 16 generated cases. Shared graph export/import is topology.edgeuse_p1 (sc_edgeuse_graph.h), including PR #304's exact revolution export; historical inexact analytic imports and dependency deferrals are documented in findings.md. Historical perturbation was sensitive. WR-84 (IsConvexRadialSector SER as TRUE) and WR-85 (CheckAndSetUVTrimCurve deletes caller curve) retained. Eight functions, largest EvaluateBinormalStepOff, CheckAndSetUVTrimCurve, IsTangentSector, IsConvexRadialSector; cycle back-edges topology.edge(9), heal.gap(5), topology.edgeuse_p3(4), blend.fillet_geom_p1(1), topology.brep_p1(1); unit-SCC #22 (220 units); 317 upstream test entry points."
|
||||
|
||||
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
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
|
||||
#### WR-497. `SmShell::IsInnerShellOf` decides from statuses it ignores
|
||||
- **Severity:** Wrong result
|
||||
- **Area:** Topology (shells, containment)
|
||||
- **Location:** `source/SMLib/src/SmShell.cpp`, `SmShell::IsInnerShellOf` (`:827-1161`): `ClosureTraversal` of the outer shell, `SmBrep::CalculateBoundingBox`, `SmFaceuse::GetPointsInFace`, `SmCurve::EvaluatePoint` of a wire edge, `SmTopologySolver::BrepCurveSolve`, `SmCurveClassification::InsertTopologyIntersections`, and the crossing face's `EvaluatePoint` / `EvaluateNormal`.
|
||||
- **What happens:** every status is ignored. A failed closure traversal leaves `bIsClosed` FALSE (the shell is "not inside"); a failed box leaves the ray length from an empty extent; failed sample points leave the rays of the previous attempt or none; a ray the solver could not cast, or whose crossings could not be classified, is still parsed (an empty classification reads as "sailed out into space", an inner shell of the infinite region); a crossing face whose normal could not be evaluated orders the shells by an unset vector. Each gives a definite TRUE or FALSE that is not evidence.
|
||||
- **Trigger:** any failure of those callees; none is known on valid closed bodies (the #1473 box with a sealed void classifies correctly either way).
|
||||
- **How found:** port of the function for tangency-labs/solidcore#1419 (WP2) and #1473; the fork's status pass (tangency-labs/smlib 4038552).
|
||||
- **Confirmed:** no (read only).
|
||||
- **Fix (fork 4038552):** `UNSURE` when the closure, the box, the sample points or a crossing face's point or normal cannot be computed; a ray that cannot be solved or classified decides nothing and the next ray is tried.
|
||||
- **Rust port:** opt-in `Kernel::fixes.shell_inner_shell_of_statuses` (default `false`, upstream's behaviour) in `topology::shell::is_inner_shell_of`. Test: `crates/solidcore/tests/issue_1473_inner_shell.rs` (both settings).
|
||||
Reference in New Issue
Block a user