diff --git a/crates/difftest/src/units/query_poly.rs b/crates/difftest/src/units/query_poly.rs index 44997358e..672d26efc 100644 --- a/crates/difftest/src/units/query_poly.rs +++ b/crates/difftest/src/units/query_poly.rs @@ -14,24 +14,34 @@ //! //! Deferred cases: `IsDegenerate` of an edge whose face has an original face while the //! back pointers are valid (`SmFace::GetSurface`, `topology.face_p2`). +//! `owner_degenerate` is a separate opt-in operation: explicit face records resolve +//! only their own labels. It compares real native `IsDegenerate` calls and independent +//! surface samples; it never supplies geometry for the two legacy opaque-owner cases. use serde::{Deserialize, Serialize}; use serde_json::Value; -use solidcore::geom::attribute::{AttributeStore, ObjectKey}; use solidcore::geom::a_object::CacheStore; +use solidcore::geom::attribute::{AttributeStore, ObjectKey}; use solidcore::geom::axis2_placement::Axis2Placement; +use solidcore::geom::extent2d::Extent2d; use solidcore::geom::extent3d::Extent3d; use solidcore::geom::object::ObjectRef; use solidcore::geom::tol; use solidcore::geom::vector2d::Vec2; use solidcore::geom::vector3d::Vec3; use solidcore::kernel::Kernel; +use solidcore::nurbs::b_spline_surface::BSplineSurface; +use solidcore::nurbs::math::{CPoint, NOW}; +use solidcore::nurbs::srf_basic::Surface as NurbSurface; use solidcore::query::poly::{ - PolyAuxId, PolyBrep, PolyEdgeId, PolyFaceId, PolyLoopId, PolyNotifyEnv, - PolyObjectKind, PolyRegionId, PolyShellId, PolyVertexId, + PolyAuxId, PolyBrep, PolyEdgeId, PolyFaceId, PolyLoopId, PolyNotifyEnv, PolyObjectKind, + PolyRegionId, PolyShellId, PolyVertexId, }; +use solidcore::surface::plane::Plane; +use solidcore::surface::surface::Surface; use solidcore::topology::topology::Topology; +use solidcore::topology::{Brep, Face, FaceId, Shell}; use solidcore::SolidError; use solidcore_ffi::units::query_poly as ffi; @@ -138,6 +148,10 @@ enum OpIn { Degenerate { calls: Vec<(usize, Option)>, }, + OwnerDegenerate { + owners: Vec, + calls: Vec<(usize, Option)>, + }, Transform { origin: [f64; 3], x: [f64; 3], @@ -158,6 +172,49 @@ struct Inputs { op: OpIn, } +#[derive(Debug, Clone, Serialize, Deserialize)] +struct OwnerIn { + label: i64, + surface: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +enum OwnerSurfaceIn { + Plane { + origin: [f64; 3], + x: [f64; 3], + y: [f64; 3], + scale: [f64; 2], + domain: [f64; 4], + }, + Bezier { + net: Vec>, + }, +} + +fn owner_to_ffi(owner: &OwnerIn) -> ffi::OwnerFace { + ffi::OwnerFace { + label: owner.label, + surface: owner.surface.as_ref().map(|s| match s { + OwnerSurfaceIn::Plane { + origin, + x, + y, + scale, + domain, + } => ffi::OwnerSurface::Plane { + origin: *origin, + x: *x, + y: *y, + scale: *scale, + domain: *domain, + }, + OwnerSurfaceIn::Bezier { net } => ffi::OwnerSurface::Bezier { net: net.clone() }, + }), + } +} + fn to_ffi(b: &BrepIn) -> ffi::PolyDesc { ffi::PolyDesc { tol: b.tol, @@ -221,6 +278,10 @@ fn op_to_ffi(op: &OpIn) -> ffi::Op { .collect(), ), OpIn::Degenerate { calls } => ffi::Op::Degenerate(calls.clone()), + OpIn::OwnerDegenerate { owners, calls } => ffi::Op::OwnerDegenerate { + owners: owners.iter().map(owner_to_ffi).collect(), + calls: calls.clone(), + }, OpIn::Transform { origin, x, @@ -243,9 +304,7 @@ fn op_to_ffi(op: &OpIn) -> ffi::Op { MutIn::SetNormalUninit { f } => ffi::Mutation::SetNormalUninit(f), MutIn::SetEdgeTolerance { e, t } => ffi::Mutation::SetEdgeTolerance(e, t), MutIn::SetFaceTolerance { f, t } => ffi::Mutation::SetFaceTolerance(f, t), - MutIn::SetOriginalFace { f, label } => { - ffi::Mutation::SetOriginalFace(f, label) - } + MutIn::SetOriginalFace { f, label } => ffi::Mutation::SetOriginalFace(f, label), MutIn::ClearAux { e } => ffi::Mutation::ClearAux(e), }) .collect(), @@ -281,6 +340,8 @@ const K_OBJ_BOXES: i32 = 22; const K_NORMALS: i32 = 23; const K_SPHERES: i32 = 25; const K_RESULTS: i32 = 26; +const K_OWNER_POINTS: i32 = 27; +const K_OWNER_TOPO: i32 = 28; fn key_name(key: i32) -> &'static str { match key { @@ -309,6 +370,8 @@ fn key_name(key: i32) -> &'static str { K_NORMALS => "normals", K_SPHERES => "spheres", K_RESULTS => "results", + K_OWNER_POINTS => "owner_points", + K_OWNER_TOPO => "owner_topo", _ => "unknown", } } @@ -321,7 +384,10 @@ fn to_output(secs: &[ffi::Section]) -> Result { return Err(harness(format!("unknown section {}", s.key))); } out = if s.ints { - out.ints(name, s.values.iter().map(|&x| x as i64).collect::>()) + out.ints( + name, + s.values.iter().map(|&x| x as i64).collect::>(), + ) } else { out.values(name, s.values.clone()) }; @@ -440,7 +506,11 @@ fn build(k: &Kernel, b: &BrepIn) -> Result { let ne = if l.mode == 2 { kk - 1 } else { kk }; for j in 0..ne { let v0 = pick(*l.verts.get(j).ok_or(Fail::Harness(harness("loop")))?)?; - let v1 = pick(*l.verts.get((j + 1) % kk).ok_or(Fail::Harness(harness("loop")))?)?; + let v1 = pick( + *l.verts + .get((j + 1) % kk) + .ok_or(Fail::Harness(harness("loop")))?, + )?; let p0 = rs(p.vertex_point(v0))?; let p1 = rs(p.vertex_point(v1))?; edges.push(rs(p.add_poly_edge(k, lp, l.edge_tol, p0, p1, v0))?); @@ -466,7 +536,10 @@ fn build(k: &Kernel, b: &BrepIn) -> Result { } let mut aux = Vec::new(); for a in &b.aux { - aux.push(rs(p.add_aux_data(Vec3::new(a[0], a[1], a[2]), Vec2::new(a[3], a[4])))?); + aux.push(rs(p.add_aux_data( + Vec3::new(a[0], a[1], a[2]), + Vec2::new(a[3], a[4]), + ))?); } for &(e, a) in &b.edge_aux { let a = *aux.get(a).ok_or(Fail::Harness(harness("aux index")))?; @@ -487,7 +560,11 @@ fn build(k: &Kernel, b: &BrepIn) -> Result { } } for &(f, l) in &b.orig_faces { - rs(p.face_set_original_face(PolyFaceId::from_index(f as u32), Some(ObjectKey(l as u64)), false))?; + rs(p.face_set_original_face( + PolyFaceId::from_index(f as u32), + Some(ObjectKey(l as u64)), + false, + ))?; } for &(e, l) in &b.orig_edgeuses { rs(p.edge_set_original_edgeuse(edge(e)?, Some(ObjectKey(l as u64))))?; @@ -520,6 +597,135 @@ fn vid(i: usize) -> PolyVertexId { PolyVertexId::from_index(i as u32) } +/// Real owner faces in their own arena, independent of the poly B-rep's labels. +struct OwnerWorld { + brep: Brep, + faces: std::collections::BTreeMap, +} + +fn build_owners(owners: &[OwnerIn]) -> Result { + let descs: Vec<_> = owners.iter().map(owner_to_ffi).collect(); + ffi::check_owner_faces(&descs).map_err(|e| Fail::Harness(harness(e.to_string())))?; + let mut brep = Brep::new(); + let shell = rs(brep.add_shell_record(Shell::default()))?; + let mut faces = std::collections::BTreeMap::new(); + for owner in owners { + let mut face = Face::new(shell); + face.surface = match &owner.surface { + None => None, + Some(OwnerSurfaceIn::Plane { + origin, + x, + y, + scale, + domain, + }) => Some(Surface::from(Plane::new( + v3(*origin), + v3(*x), + v3(*y), + Vec2::from(*scale), + &Extent2d::from_bounds(domain[0], domain[1], domain[2], domain[3]), + ))), + Some(OwnerSurfaceIn::Bezier { net }) => { + // Dimensions were checked above; each knot vector is at most 8 entries. + let nu = net.len(); + let nv = net + .first() + .ok_or_else(|| Fail::Harness(harness("empty owner Bezier net")))? + .len(); + let mut u = vec![0.0; nu]; + u.extend(vec![1.0; nu]); + let mut v = vec![0.0; nv]; + v.extend(vec![1.0; nv]); + let net = net + .iter() + .map(|row| { + row.iter() + .map(|p| CPoint::new(p[0], p[1], p[2], NOW)) + .collect() + }) + .collect(); + Some(Surface::from(BSplineSurface::from_nurb( + rs(NurbSurface::new(net, nu - 1, nv - 1, u, v))?, + false, + ))) + } + }; + if let Some(s) = &face.surface { + face.uv_domain = s.natural_uv_domain(); + } + let id = rs(brep.add_face_record(face))?; + rs(brep.shell_mut(shell))?.faces.push(id); + if let Some(surface) = rs(brep.face_mut(id))?.surface.as_mut() { + surface.set_owner(Some(ObjectRef { + type_code: solidcore::topology::face::p2::TYPE, + key: ObjectKey(owner.label as u64), + })); + surface.set_owner_zone_tol_3d(Some(0.0)); // SmFace() default + } + faces.insert(ObjectKey(owner.label as u64), id); + } + Ok(OwnerWorld { brep, faces }) +} + +/// Independent native-visible samples, not a substitute for `IsDegenerate`. +/// Per call: 10 integers (edge, label, resolved, surface, back-pointers, mapping, +/// four classifications); raw endpoints, then domain and three mapped samples +/// only when mapping applies. The midpoint is sampled even if the native +/// degeneracy implementation's seam branch would not need it. +fn owner_samples( + k: &Kernel, + p: &PolyBrep, + e: PolyEdgeId, + world: &OwnerWorld, + ints: &mut Vec, + points: &mut Vec, +) -> Result<(), Fail> { + let label = match rs(p.edge_poly_face(e))? { + Some(f) => rs(p.face_original_face(f))?, + None => None, + }; + let face = label.and_then(|key| world.faces.get(&key).copied()); + let surface = match face { + Some(f) => rs(solidcore::topology::face::p2::surface(&world.brep, f))?, + None => None, + }; + let back = rs(p.edge_ok_back_ptrs(e))?; + let mapping = back && surface.is_some() && rs(p.edge_is_2d_or_zero(e))?; + ints.extend([ + f64::from(e.index()), + label.map_or(0.0, |key| key.0 as f64), + f64::from(u8::from(face.is_some())), + f64::from(u8::from(surface.is_some())), + f64::from(u8::from(back)), + f64::from(u8::from(mapping)), + ]); + let start = rs(p.edge_start_point(e))?; + let end = rs(p.edge_end_point(e))?; + put(points, start); + put(points, end); + if let Some(surface) = surface.filter(|_| mapping) { + let domain = surface.natural_uv_domain(); + let a = Vec2::new(start.x, start.y); + let b = Vec2::new(end.x, end.y); + let (su, sv) = domain.classify_point(a, solidcore::math::EFF_ZERO_PARAM, None); + let (eu, ev) = domain.classify_point(b, solidcore::math::EFF_ZERO_PARAM, None); + ints.extend([su, sv, eu, ev].map(|pos| f64::from(pos.code()))); + points.extend([ + domain.min().x, + domain.min().y, + domain.max().x, + domain.max().y, + ]); + for uv in [a, b, (a + b) / 2.0] { + put(points, rs(surface.evaluate_point(k, uv))?); + } + } else { + ints.extend([-1.0; 4]); + } + Ok(()) +} + fn dump(b: &mut Built, out: &mut Secs) -> Result<(), Fail> { let p = &mut b.brep; let mut o = Vec::new(); @@ -569,7 +775,11 @@ fn dump(b: &mut Built, out: &mut Secs) -> Result<(), Fail> { } else { opt(rs(p.face_poly_shell(f))?, PolyShellId::index) }); - o.push(if rs(p.face_ok_back_ptrs(f))? { 1.0 } else { 0.0 }); + o.push(if rs(p.face_ok_back_ptrs(f))? { + 1.0 + } else { + 0.0 + }); o.push(rs(p.face_original_face(f))?.map_or(0.0, |k| k.0 as f64)); o.push(opt(rs(p.face_outer_poly_loop(f))?, PolyLoopId::index)); o.push(ls.len() as f64); @@ -622,8 +832,16 @@ fn dump(b: &mut Built, out: &mut Secs) -> Result<(), Fail> { o.push(rads.len() as f64); o.extend(rads.iter().map(|r| f64::from(r.index()))); o.push(opt(rs(p.edge_aux_data(e))?, PolyAuxId::index)); - o.push(if rs(p.edge_is_2d_or_zero(e))? { 1.0 } else { 0.0 }); - o.push(if rs(p.edge_ok_back_ptrs(e))? { 1.0 } else { 0.0 }); + o.push(if rs(p.edge_is_2d_or_zero(e))? { + 1.0 + } else { + 0.0 + }); + o.push(if rs(p.edge_ok_back_ptrs(e))? { + 1.0 + } else { + 0.0 + }); o.push(rs(p.edge_original_edgeuse(e))?.map_or(0.0, |k| k.0 as f64)); g.push(rs(p.edge_tolerance(e))?); put(&mut g, rs(p.edge_start_point(e))?); @@ -646,7 +864,11 @@ fn dump(b: &mut Built, out: &mut Secs) -> Result<(), Fail> { o.extend(pe.iter().map(|e| f64::from(e.index()))); o.push(pf.len() as f64); o.extend(pf.iter().map(|f| opt(*f, PolyFaceId::index))); - o.push(if rs(p.vertex_is_2d_or_zero(v))? { 1.0 } else { 0.0 }); + o.push(if rs(p.vertex_is_2d_or_zero(v))? { + 1.0 + } else { + 0.0 + }); put(&mut g, rs(p.vertex_point(v))?); g.push(rs(p.vertex_tolerance(v))?); let ns = rs(p.vertex_normals(v))?; @@ -706,7 +928,8 @@ fn run_op(k: &Kernel, b: &mut Built, op: &OpIn, out: &mut Secs) -> Result<(), Fa centroid, normal, } => { - let mut pts: Option> = points.as_ref().map(|v| v.iter().map(|&q| v3(q)).collect()); + let mut pts: Option> = + points.as_ref().map(|v| v.iter().map(|&q| v3(q)).collect()); let mut nrm = normal.map(v3); let r = rs(p.calculate_bounding_box(*pseudo, pts.as_mut(), *centroid, nrm.as_mut()))?; out.ints(K_STATUS, vec![0.0]); @@ -802,6 +1025,24 @@ fn run_op(k: &Kernel, b: &mut Built, op: &OpIn, out: &mut Secs) -> Result<(), Fa } out.ints(K_RESULTS, o); } + OpIn::OwnerDegenerate { owners, calls } => { + let world = build_owners(owners)?; + let (mut results, mut topo, mut points) = (Vec::new(), Vec::new(), Vec::new()); + for &(e, t) in calls { + let e = eid(e); + let d = rs(p.edge_is_degenerate_with_owner( + k, + e, + t.map(|t| tol::xsect_tol3d(k, t)), + |key| world.faces.get(&key).map(|&f| (&world.brep, f)), + ))?; + results.push(f64::from(u8::from(d))); + owner_samples(k, p, e, &world, &mut topo, &mut points)?; + } + out.ints(K_RESULTS, results); + out.ints(K_OWNER_TOPO, topo); + out.floats(K_OWNER_POINTS, points); + } OpIn::Transform { origin, x, @@ -855,11 +1096,9 @@ fn run_op(k: &Kernel, b: &mut Built, op: &OpIn, out: &mut Secs) -> Result<(), Fa MutIn::SetFaceTolerance { f, t } => { rs(p.face_set_tolerance(fid(f), tol::zone_tol3d(k, t)))?; } - MutIn::SetOriginalFace { f, label } => rs(p.face_set_original_face( - fid(f), - Some(ObjectKey(label as u64)), - false, - ))?, + MutIn::SetOriginalFace { f, label } => { + rs(p.face_set_original_face(fid(f), Some(ObjectKey(label as u64)), false))?; + } MutIn::ClearAux { e } => rs(p.edge_set_aux_data(eid(e), None))?, } } @@ -944,6 +1183,123 @@ impl UnitDiff for Runner { } } +#[cfg(test)] +mod owner_tests { + use super::*; + use crate::case::{repo_root, CaseSource}; + + fn owner_cases() -> Vec { + let file: Value = serde_json::from_str(include_str!( + "../../../../fixtures/query.poly/real_owners.json" + )) + .unwrap(); + file["cases"] + .as_array() + .unwrap() + .iter() + .map(|c| Case { + unit: "query.poly".into(), + name: c["name"].as_str().unwrap().into(), + source: CaseSource::Inline, + inputs: c["inputs"].clone(), + }) + .collect() + } + + #[test] + fn original_corpus_and_opaque_native_outputs_are_retained() { + let cases = Runner + .cases(&CaseContext { + root: repo_root(), + seeds: 16, + }) + .unwrap(); + assert_eq!(cases.len(), 671 + 16); + assert_eq!( + cases + .iter() + .filter(|c| !c.name.starts_with("owner_")) + .count(), + 671 + ); + assert_eq!( + cases + .iter() + .filter(|c| matches!(c.source, CaseSource::Generated { .. })) + .count(), + 600 + ); + for (name, seed) in [ + ("gen0378", 13_165_552_566_343_800_279), + ("gen0496", 13_165_552_566_343_800_397), + ] { + let case = cases.iter().find(|c| c.name == name).unwrap(); + assert_eq!(case.source, CaseSource::Generated { seed }); + assert_eq!(case.inputs, Runner.generate(&mut Rng::new(seed)).unwrap()); + let rust = Runner.run_rust(case).unwrap(); + let deferred = rust.deferred.unwrap(); + assert_eq!(deferred.unit, "topology.face_p2"); + assert_eq!(deferred.symbol, "SmFace::GetSurface"); + let cpp = Runner.run_cpp(case).unwrap(); + assert!(cpp.error.is_none()); + assert_eq!(cpp.ints["results"], vec![-9; 4]); + assert!(!cpp.values.contains_key("owner_points")); + assert!(!cpp.ints.contains_key("owner_topo")); + } + } + + #[test] + fn real_owner_native_mapping_poles_seams_and_controls() { + for case in owner_cases() { + let cpp = Runner.run_cpp(&case).unwrap(); + let rust = Runner.run_rust(&case).unwrap(); + assert!(cpp.error.is_none() && cpp.rejected.is_none() && cpp.deferred.is_none()); + assert!(rust.error.is_none() && rust.rejected.is_none() && rust.deferred.is_none()); + let diff = crate::compare::compare(&cpp, &rust, &[]); + assert!(diff.pass(), "{}: {diff:?}", case.name); + assert!(diff.values_compared > 0); + assert!(cpp.values["owner_points"] + .iter() + .any(|v| v.is_finite() && *v != 0.0)); + let expected: &[i64] = match case.name.as_str() { + "owner_plane_stretch" => &[0, 0], + "owner_plane_compress" | "owner_face_without_surface" => &[0, 1, 0], + "owner_u_pole" | "owner_shared_face_identity" => &[1, 1], + "owner_u_seam_midpoint" => &[0, 0, 1], + "owner_partial_pole" | "owner_v_pole" => &[1], + "owner_v_seam_midpoint" => &[0, 1], + "owner_distinct_face_identity" => &[1, 0], + _ => &[0], + }; + assert_eq!(cpp.ints["results"], expected, "{}", case.name); + if case.name == "owner_u_seam_midpoint" { + assert_eq!( + &cpp.values["owner_points"][10..19], + &[1.0, 0.0, 3.0, 1.0, 0.0, 3.0, 0.0, 0.0, 3.0] + ); + } + if case.name == "owner_plane_compress" { + assert_eq!( + &cpp.values["owner_points"][10..19], + &[8.0625, -1.0, 1.0, 8.125, -1.0, 1.0, 8.09375, -1.0, 1.0] + ); + } + if case.name == "owner_face_without_surface" { + assert_eq!(&cpp.ints["owner_topo"][1..6], &[3, 1, 0, 1, 0]); + // The same valid original face label, without its explicit record, + // must defer instead of becoming another surface-less face. + let mut unresolved = case.clone(); + unresolved.inputs["op"]["owners"] = serde_json::json!([]); + assert!(Runner.run_rust(&unresolved).unwrap().deferred.is_some()); + assert_eq!( + Runner.run_cpp(&unresolved).unwrap().ints["results"], + vec![-9; 3] + ); + } + } + } +} + // ---- generator ---------------------------------------------------------------------- mod gen { diff --git a/crates/solidcore-ffi/src/units/query_poly.rs b/crates/solidcore-ffi/src/units/query_poly.rs index 5c48d75b1..ed3f3d072 100644 --- a/crates/solidcore-ffi/src/units/query_poly.rs +++ b/crates/solidcore-ffi/src/units/query_poly.rs @@ -22,6 +22,13 @@ //! leave a dangling member), `CalculateBoundingBox` asked for a pseudo box without a //! point array (upstream dereferences the null pointer), labels outside `1..=3`, and //! mutations that touch an already deleted object. +//! +//! [`Op::OwnerDegenerate`] puts explicit real face records immediately after `op`, +//! before the otherwise unchanged poly B-rep block: +//! `n_owners`, then `label surface_kind` (`0` no surface, `1` plane, `2` Bezier). +//! A plane is `origin[3] x[3] y[3] scale[2] domain[4]`; a non-rational Bezier +//! patch is `nu nv` followed by `nu * nv` XYZ control points, u rows first. +//! This new op does not change any legacy input or output layout. use crate::validate::bad; use crate::{call_with_buffer, FfiError}; @@ -91,6 +98,131 @@ pub struct PolyDesc { pub ok_back_ptrs: bool, } +/// Explicit geometry attached to a real `SmFace` by the owner-aware op. +#[derive(Debug, Clone, PartialEq)] +pub enum OwnerSurface { + /// An actual `SmPlane`, with its axes, scales and natural UV domain. + Plane { + /// Placement origin. + origin: [f64; 3], + /// Placement x axis. + x: [f64; 3], + /// Placement y axis. + y: [f64; 3], + /// Parameter scales. + scale: [f64; 2], + /// `[u_min, v_min, u_max, v_max]`. + domain: [f64; 4], + }, + /// A non-rational single-span B-spline on `[0, 1]^2`, with 2..=4 points + /// in each direction. Every row is one u index; columns are v indices. + Bezier { + /// XYZ control net (not homogeneous coordinates). + net: Vec>, + }, +} + +/// A real original face, explicitly distinguished from an unresolved legacy label. +#[derive(Debug, Clone, PartialEq)] +pub struct OwnerFace { + /// Existing original-face label, in `1..=3`. + pub label: i64, + /// `None` constructs a real face with a null surface, not an unresolved owner. + pub surface: Option, +} + +/// Validates only the new explicit owner geometry, without calling C++. +/// +/// # Errors +/// +/// Rejects duplicate/out-of-range labels, non-finite geometry, reversed plane +/// domains, or a non-rectangular/unsupported Bezier net. No legacy guard changes. +pub fn check_owner_faces(owners: &[OwnerFace]) -> Result<(), FfiError> { + let finite = |xs: &[f64]| -> Result<(), FfiError> { + if xs.iter().all(|x| x.is_finite()) { + Ok(()) + } else { + Err(bad("non-finite owner geometry")) + } + }; + let mut seen = Vec::new(); + for owner in owners { + label(owner.label)?; + if seen.contains(&owner.label) { + return Err(bad("duplicate owner label")); + } + seen.push(owner.label); + match &owner.surface { + None => {} + Some(OwnerSurface::Plane { + origin, + x, + y, + scale, + domain, + }) => { + finite(origin)?; + finite(x)?; + finite(y)?; + finite(scale)?; + finite(domain)?; + if domain[0] > domain[2] || domain[1] > domain[3] { + return Err(bad("reversed owner plane domain")); + } + } + Some(OwnerSurface::Bezier { net }) => { + let nv = net.first().map_or(0, Vec::len); + if !(2..=4).contains(&net.len()) + || !(2..=4).contains(&nv) + || net.iter().any(|row| row.len() != nv) + { + return Err(bad( + "owner Bezier net must be rectangular, 2..=4 per direction", + )); + } + for point in net.iter().flatten() { + finite(point)?; + } + } + } + } + Ok(()) +} + +fn encode_owner_faces(v: &mut Vec, owners: &[OwnerFace]) -> Result<(), FfiError> { + check_owner_faces(owners)?; + v.push(owners.len() as f64); + for owner in owners { + v.push(label(owner.label)?); + match &owner.surface { + None => v.push(0.0), + Some(OwnerSurface::Plane { + origin, + x, + y, + scale, + domain, + }) => { + v.push(1.0); + v.extend_from_slice(origin); + v.extend_from_slice(x); + v.extend_from_slice(y); + v.extend_from_slice(scale); + v.extend_from_slice(domain); + } + Some(OwnerSurface::Bezier { net }) => { + v.push(2.0); + v.push(net.len() as f64); + v.push(net.first().map_or(0, Vec::len) as f64); + for point in net.iter().flatten() { + v.extend_from_slice(point); + } + } + } + } + Ok(()) +} + /// One contains query. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ContainsQuery { @@ -155,6 +287,14 @@ pub enum Op { Contains(Vec), /// `IsDegenerate(opt xsect tol)` calls. Degenerate(Vec<(usize, Option)>), + /// Real-face `IsDegenerate` calls plus independent raw/mapped endpoint and + /// midpoint samples, natural domains and parameter classifications. + OwnerDegenerate { + /// Explicit owner provenance; omitted labels stay unresolved. + owners: Vec, + /// `(edge, optional intersection tolerance)` calls. + calls: Vec<(usize, Option)>, + }, /// `Transform(SmAxis2Placement(o, x, y), opt scale)`. Transform { /// Origin. @@ -218,8 +358,17 @@ fn encode(d: &PolyDesc, op: &Op) -> Result, FfiError> { Op::Transform { .. } => 6.0, Op::Mutate(_) => 7.0, Op::Kinds(_) => 8.0, + Op::OwnerDegenerate { .. } => 9.0, }; v.push(op_code); + if let Op::OwnerDegenerate { owners, .. } = op { + // Construct genuine owners before poly faces, without transient fake + // pointers for any explicitly resolved label. Legacy bytes are unchanged. + encode_owner_faces(&mut v, owners)?; + if d.vertices.iter().flatten().any(|x| !x.is_finite()) { + return Err(bad("non-finite owner-op poly vertex")); + } + } v.push(d.tol); v.push(d.regions.len() as f64); let mut n_shells = 0usize; @@ -397,6 +546,14 @@ fn encode(d: &PolyDesc, op: &Op) -> Result, FfiError> { v.push(t.unwrap_or(0.0)); } } + Op::OwnerDegenerate { calls, .. } => { + v.push(calls.len() as f64); + for &(e, t) in calls { + v.push(idx(e, n_edges, "edge")?); + v.push(b(t.is_some())); + v.push(t.unwrap_or(0.0)); + } + } Op::Transform { origin, x, @@ -526,3 +683,41 @@ pub fn run(d: &PolyDesc, op: &Op) -> Result, FfiError> { } Ok(secs) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_owner_guards_are_bounded_and_do_not_invent_geometry() { + let bare = OwnerFace { + label: 1, + surface: None, + }; + assert!(check_owner_faces(&[]).is_ok()); // unresolved is not a null surface + assert!(check_owner_faces(std::slice::from_ref(&bare)).is_ok()); + assert!(check_owner_faces(&[bare.clone(), bare]).is_err()); + for label in [0, 4, i64::MAX] { + assert!(check_owner_faces(&[OwnerFace { + label, + surface: None + }]) + .is_err()); + } + let patch = |net| OwnerFace { + label: 2, + surface: Some(OwnerSurface::Bezier { net }), + }; + assert!(check_owner_faces(&[patch(vec![vec![[0.0; 3]; 2]; 3])]).is_ok()); + for net in [ + Vec::new(), + vec![vec![[0.0; 3]; 2]], + vec![vec![[0.0; 3]; 2]; 5], + vec![vec![[0.0; 3]; 5]; 2], + vec![vec![[0.0; 3]; 2], vec![[0.0; 3]; 3]], + vec![vec![[f64::NAN, 0.0, 0.0]; 2]; 2], + ] { + assert!(check_owner_faces(&[patch(net)]).is_err()); + } + } +} diff --git a/crates/solidcore/src/query/poly.rs b/crates/solidcore/src/query/poly.rs index 68ae69037..b5de19b36 100644 --- a/crates/solidcore/src/query/poly.rs +++ b/crates/solidcore/src/query/poly.rs @@ -36,6 +36,7 @@ //! | [`PolyBrep::edge_bounding_box`] | `SmPolyEdge::CalculateBoundingBox` | //! | [`PolyBrep::edge_is_2d_or_zero`] | `SmPolyEdge::Is2dOrZero` | //! | [`PolyBrep::edge_is_degenerate`] | `SmPolyEdge::IsDegenerate` | +//! | [`PolyBrep::edge_is_degenerate_with_owner`] | `SmPolyEdge::IsDegenerate` (real-face adapter) | //! | [`PolyBrep::delete_loop`] | `SmPolyLoop::~SmPolyLoop` | //! | [`PolyBrep::loop_poly_face`], [`PolyBrep::loop_first_poly_edge`], [`PolyBrep::loop_is_closed`], [`PolyBrep::loop_poly_edges`] | `SmPolyLoop::GetPolyFace`, `GetFirstPolyEdge`, `IsClosed`, `GetPolyEdges` (`GetPolyBrep` is `self`) | //! | [`PolyBrep::loop_contains_point`] | `SmPolyLoop::ContainsPoint` | @@ -104,8 +105,9 @@ //! the back pointers are valid and a face is set; `SetOriginalFace` with attribute //! propagation calls `SmFace::Notify` (unit `topology.face_p3`); the destructor of a //! poly B-rep with valid back pointers clears `SmVertex::m_pPolyVertex` -//! (`GetPolyVertex` / `SetPolyVertex`, unit `topology.vertex`). These return -//! [`SolidError::Unported`]. +//! (`GetPolyVertex` / `SetPolyVertex`, unit `topology.vertex`). The key-only APIs +//! return [`SolidError::Unported`]; `IsDegenerate` additionally offers the explicit +//! [`PolyBrep::edge_is_degenerate_with_owner`] adapter. //! - `SmPolyBrep::CalculateBoundingBox` asked for a pseudo box without a point array //! dereferences the null `pOptPolyPoints` (it builds `sPolyPoints` and then reads the //! caller's pointer); Rust returns [`SolidError::InvalidInput`] (see @@ -148,12 +150,16 @@ //! transformed in an ordered list instead of upstream's pointer-hashed //! `std::unordered_set`; only membership is used, so the order of work is the same. //! - `SM_DEBUG_CODE` blocks, `WARN`, `SE` messages and `SM_ASSERT*` are not ported. +//! - `IsDegenerate` can resolve an original face through a caller-supplied B-rep +//! adapter; the key-only API still defers. Surface evaluation errors propagate; +//! upstream ignores their statuses. use crate::error::{Result, SolidError}; use crate::geom::a_object::{self, AObject, CacheStore}; use crate::geom::attribute::{AttributeStore, ObjectKey}; use crate::geom::axis2_placement::Axis2Placement; use crate::geom::context::Context; +use crate::geom::extent1d::ExtentPointType; use crate::geom::extent3d::Extent3d; use crate::geom::geom_utility::p1::polygon_area; use crate::geom::geom_utility::p2::is_point_on_segment; @@ -163,8 +169,9 @@ use crate::geom::pseudo_box::PseudoBox; use crate::geom::tol; use crate::geom::vector2d::Vec2; use crate::kernel::Kernel; -use crate::math::{Point3, Vec3, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQ, UNDEF_DOUBLE}; +use crate::math::{Point3, Vec3, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_PARAM, EFF_ZERO_SQ, UNDEF_DOUBLE}; use crate::topology::topology::TopologyRecords; +use crate::topology::{Brep, FaceId}; /// `SM_UNDEF_ULONG` on LP64 (`0xFFFFFFFFFFFFFFFE`... upstream's `ULONG_MAX - 1`). pub const UNDEF_ULONG: u64 = u64::MAX - 1; @@ -1956,19 +1963,45 @@ impl PolyBrep { k: &Kernel, e: PolyEdgeId, xsect_tol: Option, + ) -> Result { + self.edge_is_degenerate_with_owner(k, e, xsect_tol, |_| None) + } + + /// `SmPolyEdge::IsDegenerate` with access to the original face's real B-rep. + /// + /// `resolve` maps the original face key to its owning B-rep and face id. `None` + /// means unresolved, not a face without a surface: the latter is represented by + /// a valid face whose `surface` is `None`. With valid back pointers, 2D endpoints + /// are evaluated on that surface, including upstream's pole/seam midpoint test. + /// A resolver is not consulted when the back pointers are invalid. + /// + /// # Errors + /// + /// [`SolidError::Unported`] for an unresolved original face; errors of the face + /// and edge accessors and of surface evaluation. Upstream ignores evaluation + /// statuses; this method propagates them. + pub fn edge_is_degenerate_with_owner<'a>( + &self, + k: &Kernel, + e: PolyEdgeId, + xsect_tol: Option, + resolve: impl FnOnce(ObjectKey) -> Option<(&'a Brep, FaceId)>, ) -> Result { let face = self.edge_poly_face(e)?; // a deleted face has cleared its back pointer let orig_face = face .and_then(|f| self.face_slots.get(f.0 as usize).and_then(Option::as_ref)) .and_then(|x| x.original_face); - if orig_face.is_some() && self.edge_ok_back_ptrs(e)? { - // UNPORTED(topology.face_p2): SmFace::GetSurface - return Err(SolidError::unported( - "SmFace::GetSurface", - "topology.face_p2", - )); - } + let surface = match orig_face { + Some(key) if self.edge_ok_back_ptrs(e)? => { + // DISPATCH GAP: an opaque key alone cannot identify an owning B-rep. + let (brep, face) = resolve(key).ok_or_else(|| { + SolidError::unported("SmFace::GetSurface", "topology.face_p2") + })?; + crate::topology::face::p2::surface(brep, face)? + } + _ => None, + }; // Use a local tolerance instead of SmScaledZero let xsect = if let Some(t) = xsect_tol { @@ -1998,15 +2031,59 @@ impl PolyBrep { // Get PolyEdge endPoints. let mut start = self.edge_start_point(e)?; let mut end = self.edge_end_point(e)?; + let start_uv = Vec2::new(start.x, start.y); + let end_uv = Vec2::new(end.x, end.y); + let mut mid = (start + end) / 2.0; + let (mut su, mut sv, mut eu, mut ev) = ( + ExtentPointType::Unknown, + ExtentPointType::Unknown, + ExtentPointType::Unknown, + ExtentPointType::Unknown, + ); + let mut check_pole = false; - // map or clean up chord pts as appropriate (no surface here) + // Match SmPolyEdge::IsDegenerate's imperfect UV/XYZ test, not a new one. if self.edge_is_2d_or_zero(e)? { - start.z = 0.0; - end.z = 0.0; + if let Some(surface) = surface { + let domain = surface.natural_uv_domain(); + (su, sv) = domain.classify_point(start_uv, EFF_ZERO_PARAM, None); + (eu, ev) = domain.classify_point(end_uv, EFF_ZERO_PARAM, None); + if su != ExtentPointType::Outside && sv != ExtentPointType::Outside { + check_pole = (su == eu + && matches!(su, ExtentPointType::Start | ExtentPointType::End)) + || (sv == ev + && matches!(sv, ExtentPointType::Start | ExtentPointType::End)); + } + start = surface.evaluate_point(k, start_uv)?; + end = surface.evaluate_point(k, end_uv)?; + } else { + start.z = 0.0; + end.z = 0.0; + mid.z = 0.0; + } } - // check the PolyEdge ChordLengths to scaled zero (the seam check needs a surface) - Ok(start.closer_than(xsect, end)) + let mut degenerate = start.closer_than(xsect, end); + if degenerate && self.edge_is_2d_or_zero(e)? { + if let Some(surface) = surface { + let opposite = |a, b| { + (a == ExtentPointType::Start && b == ExtentPointType::End) + || (a == ExtentPointType::End && b == ExtentPointType::Start) + }; + let mut check_seam = + opposite(su, eu) && (start_uv.y - end_uv.y).abs() < EFF_ZERO_PARAM; + if !check_seam { + check_seam = opposite(sv, ev) && (start_uv.x - end_uv.x).abs() < EFF_ZERO_PARAM; + } + if check_seam { + if check_pole { + mid = surface.evaluate_point(k, (start_uv + end_uv) / 2.0)?; + } + degenerate = start.closer_than(xsect, mid); + } + } + } + Ok(degenerate) } // ---- loops ---------------------------------------------------------------------- @@ -3390,6 +3467,56 @@ mod tests { assert!(b.face_normal(&k, c, true, true).is_ok()); } + #[test] + fn degeneracy_resolves_real_face_but_not_an_opaque_key() { + let k = Kernel::default(); + let (mut poly, face, _, edges) = square(&k); + let edge = edges[0]; + poly.face_set_original_face(face, Some(ObjectKey(7)), false) + .unwrap(); + poly.set_ok_back_ptrs(true); + assert!(poly + .edge_is_degenerate(&k, edge, Some(0.5)) + .is_err_and(|e| e.is_unported())); + + let mut owner = Brep::new(); + let shell = owner + .add_shell_record(crate::topology::Shell::default()) + .unwrap(); + let f = owner + .add_face_record(crate::topology::Face::new(shell)) + .unwrap(); + // A resolved face without geometry takes upstream's no-surface branch. + assert!(!poly + .edge_is_degenerate_with_owner(&k, edge, Some(0.5), |_| Some((&owner, f))) + .unwrap()); + owner.face_mut(f).unwrap().surface = Some(crate::surface::surface::Surface::from( + crate::surface::plane::Plane::new( + Vec3::ZERO, + Vec3::new(1.0, 0.0, 0.0), + Vec3::new(0.0, 1.0, 0.0), + Vec2::new(0.25, 1.0), + &crate::geom::extent2d::Extent2d::from_bounds(0.0, 0.0, 1.0, 1.0), + ), + )); + assert!(poly + .edge_is_degenerate_with_owner(&k, edge, Some(0.5), |_| Some((&owner, f))) + .unwrap()); + assert!(poly + .edge_is_degenerate_with_owner(&k, edge, Some(0.5), |_| Some(( + &owner, + FaceId::from_index(u32::MAX) + ))) + .is_err()); + + poly.set_ok_back_ptrs(false); + assert!(!poly + .edge_is_degenerate_with_owner(&k, edge, Some(0.5), |_| panic!( + "invalid back pointers must not resolve the owner" + )) + .unwrap()); + } + #[test] fn kinds_and_helpers() { assert!(PolyObjectKind::CPolyFace.is_kind_of(PolyObjectKind::Face.type_code())); diff --git a/fixtures/query.poly/real_owners.json b/fixtures/query.poly/real_owners.json new file mode 100644 index 000000000..b3d5095db --- /dev/null +++ b/fixtures/query.poly/real_owners.json @@ -0,0 +1,118 @@ +{ + "description": "Explicit real SmFace provenance. Bezier nets are non-rational single-span SmBSplineSurfaces on [0,1]^2, not analytic stand-ins. Native IsDegenerate decisions and independent raw/domain/mapped samples are compared. No geometry is supplied for legacy generated labels.", + "upstream_ref": "source/SMLib/src/SmPoly.cpp:21279-21419", + "cases": [ + { + "name": "owner_plane_stretch", + "inputs": { + "brep": {"tol": 0.00001, "regions": [1], "vertices": [[0.25,0.5,0,0.00001],[0.5,0.5,0,0.00001]], "faces": [{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}], "orig_faces":[[0,1]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"plane","origin":[8,-2,1],"x":[1,0,0],"y":[0,1,0],"scale":[4,2],"domain":[0,0,1,1]}}],"calls":[[0,0.5],[0,null]]} + } + }, + { + "name": "owner_plane_compress", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0.25,0.5,0,0.00001],[0.5,0.5,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,1]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"plane","origin":[8,-2,1],"x":[1,0,0],"y":[0,1,0],"scale":[0.25,2],"domain":[0,0,1,1]}}],"calls":[[0,0.03125],[0,0.125],[0,null]]} + } + }, + { + "name": "owner_face_without_surface", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0.25,0.5,0,0.00001],[0.5,0.5,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,3]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":3,"surface":null}],"calls":[[0,0.125],[0,0.5],[0,null]]} + } + }, + { + "name": "owner_invalid_back_pointers", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0.25,0.5,0,0.00001],[0.5,0.5,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,1]],"ok_back_ptrs":false}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"plane","origin":[8,-2,1],"x":[1,0,0],"y":[0,1,0],"scale":[0.25,2],"domain":[0,0,1,1]}}],"calls":[[0,0.125]]} + } + }, + { + "name": "owner_xyz_not_uv", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0.25,0.5,2,0.00001],[0.5,0.5,2,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,1]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"plane","origin":[8,-2,1],"x":[1,0,0],"y":[0,1,0],"scale":[0.25,2],"domain":[0,0,1,1]}}],"calls":[[0,0.125]]} + } + }, + { + "name": "owner_no_original_face", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0.25,0.5,0,0.00001],[0.5,0.5,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[],"calls":[[0,0.125]]} + } + }, + { + "name": "owner_u_pole", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0,0,0,0.00001],[1,0,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,1]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"bezier","net":[[[0,0,2],[1,0,3]],[[0,0,2],[-1,0,3]],[[0,0,2],[1,0,3]]]}}],"calls":[[0,null],[0,0.001]]} + } + }, + { + "name": "owner_u_seam_midpoint", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0,1,0,0.00001],[1,1,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,1]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"bezier","net":[[[0,0,2],[1,0,3]],[[0,0,2],[-1,0,3]],[[0,0,2],[1,0,3]]]}}],"calls":[[0,null],[0,0.001],[0,2]]} + } + }, + { + "name": "owner_u_seam_reversed", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[1,1,0,0.00001],[0,1,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,2]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":2,"surface":{"kind":"bezier","net":[[[0,0,2],[1,0,3]],[[0,0,2],[-1,0,3]],[[0,0,2],[1,0,3]]]}}],"calls":[[0,null]]} + } + }, + { + "name": "owner_u_seam_interior", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0,0.5,0,0.00001],[1,0.5,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,1]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"bezier","net":[[[0,0,2],[1,0,3]],[[0,0,2],[-1,0,3]],[[0,0,2],[1,0,3]]]}}],"calls":[[0,0.001]]} + } + }, + { + "name": "owner_partial_pole", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0.25,0,0,0.00001],[0.75,0,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,1]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"bezier","net":[[[0,0,2],[1,0,3]],[[0,0,2],[-1,0,3]],[[0,0,2],[1,0,3]]]}}],"calls":[[0,null]]} + } + }, + { + "name": "owner_v_pole", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0,0,0,0.00001],[0,1,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,3]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":3,"surface":{"kind":"bezier","net":[[[0,0,2],[0,0,2],[0,0,2]],[[1,0,3],[-1,0,3],[1,0,3]]]}}],"calls":[[0,null]]} + } + }, + { + "name": "owner_v_seam_midpoint", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[1,0,0,0.00001],[1,1,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,3]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":3,"surface":{"kind":"bezier","net":[[[0,0,2],[0,0,2],[0,0,2]],[[1,0,3],[-1,0,3],[1,0,3]]]}}],"calls":[[0,null],[0,2]]} + } + }, + { + "name": "owner_v_seam_reversed", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[1,1,0,0.00001],[1,0,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,2]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":2,"surface":{"kind":"bezier","net":[[[0,0,2],[0,0,2],[0,0,2]],[[1,0,3],[-1,0,3],[1,0,3]]]}}],"calls":[[0,null]]} + } + }, + { + "name": "owner_shared_face_identity", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0.25,0.5,0,0.00001],[0.5,0.5,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]},{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[1,0]}]}],"orig_faces":[[0,1],[1,1]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"plane","origin":[8,-2,1],"x":[1,0,0],"y":[0,1,0],"scale":[0.25,2],"domain":[0,0,1,1]}}],"calls":[[0,0.125],[1,0.125]]} + } + }, + { + "name": "owner_distinct_face_identity", + "inputs": { + "brep": {"tol":0.00001,"regions":[1],"vertices":[[0.25,0.5,0,0.00001],[0.5,0.5,0,0.00001]],"faces":[{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]},{"shell":0,"tol":0.00001,"loops":[{"mode":2,"edge_tol":0.00001,"verts":[0,1]}]}],"orig_faces":[[0,1],[1,3]],"ok_back_ptrs":true}, + "op": {"kind":"owner_degenerate","owners":[{"label":1,"surface":{"kind":"plane","origin":[8,-2,1],"x":[1,0,0],"y":[0,1,0],"scale":[0.25,2],"domain":[0,0,1,1]}},{"label":3,"surface":{"kind":"plane","origin":[-4,8,2],"x":[1,0,0],"y":[0,1,0],"scale":[4,2],"domain":[0,0,1,1]}}],"calls":[[0,0.125],[1,0.125]]} + } + } + ] +} diff --git a/harness/cpp/shim/query_poly.cpp b/harness/cpp/shim/query_poly.cpp index 6a0573730..1c249d29b 100644 --- a/harness/cpp/shim/query_poly.cpp +++ b/harness/cpp/shim/query_poly.cpp @@ -15,20 +15,26 @@ #include "StdAfx.h" #include +#include #include #include #include +#include #include #include +#include #include +#include #include #include +#include #include #include #include #include #include #include +#include // The poly classes are final; read their protected members (and set the radial rings, // the vertex list and the back pointer flag) by opening the access of this header only. @@ -39,6 +45,7 @@ #undef protected #include +#include #include #include "sc_sm.h" @@ -58,6 +65,7 @@ enum : int32_t OP_TRANSFORM = 6, OP_MUTATE = 7, OP_KINDS = 8, + OP_OWNER_DEGEN = 9, }; // Section keys (keep in sync with crates/difftest/src/units/query_poly.rs). @@ -88,6 +96,8 @@ enum : int32_t K_NORMALS = 23, K_SPHERES = 25, K_RESULTS = 26, + K_OWNER_POINTS = 27, + K_OWNER_TOPO = 28, }; struct Rd @@ -157,6 +167,32 @@ int64_t unfake(const void* p) return p == NULL ? 0 : (int64_t)((uintptr_t)p >> 4); } +// Exposes only the protected constructor/destructor of a real SmFace. No virtual +// geometry operation is replaced. SetSurface establishes the real surface owner. +struct OwnerFace : SmFace +{ + // SmTopology hides the context allocator; SmObject's full overload sets also + // contain private entries. Forward only the public, matching allocation pair. + static void* operator new(size_t size, const SmContext& ctx) + { + return SmObject::operator new(size, ctx); + } + static void operator delete(void* ptr) + { + SmObject::operator delete(ptr); + } + static void operator delete(void* ptr, const SmContext& ctx) + { + SmObject::operator delete(ptr, ctx); + } + explicit OwnerFace(SmSurface* surface) + { + if (surface != NULL) + SetSurface(surface); + } + ~OwnerFace() override = default; +}; + struct Model { SmPolyBrep* brep = NULL; @@ -169,6 +205,23 @@ struct Model std::vector edge_alive; std::vector vertices; std::vector aux; + std::vector owner_labels; + std::vector> owners; + + SmFace* owner(int64_t label) const + { + for (size_t i = 0; i < owner_labels.size(); ++i) + if (owner_labels[i] == label) + return owners[i].get(); + return NULL; + } + int64_t face_label(const SmFace* face) const + { + for (size_t i = 0; i < owners.size(); ++i) + if (owners[i].get() == face) + return owner_labels[i]; + return unfake(face); + } }; void build(Rd& rd, const SmContext& ctx, Model& m) @@ -298,7 +351,8 @@ void build(Rd& rd, const SmContext& ctx, Model& m) { int64_t f = rd.idx(); int64_t label = rd.idx(); - m.faces[f]->SetOriginalFace((SmFace*)fake(label), FALSE); + SmFace* owner = m.owner(label); + m.faces[f]->SetOriginalFace(owner ? owner : (SmFace*)fake(label), FALSE); } int64_t noe = rd.idx(); for (int64_t i = 0; i < noe; ++i) @@ -354,7 +408,8 @@ void dump(Model& m, Out& out) for (int64_t label = 1; label <= 3; ++label) { SmTArray fs; - b->GetPolyFaces(fs, (SmFace*)fake(label)); + SmFace* common = m.owner(label); + b->GetPolyFaces(fs, common ? common : (SmFace*)fake(label)); o.push_back((double)fs.GetSize()); for (ULONG j = 0; j < fs.GetSize(); ++j) o.push_back(ix(m.faces, fs[j])); @@ -380,7 +435,7 @@ void dump(Model& m, Out& out) o.push_back(ix(m.faces, f->m_pCPolyFace)); o.push_back(f->m_pCPolyFace == f ? -1.0 : ix(m.shells, f->GetPolyShell())); o.push_back(f->GetOKBackPtrs() ? 1.0 : 0.0); - o.push_back((double)unfake(f->GetOriginalFace())); + o.push_back((double)m.face_label(f->GetOriginalFace())); o.push_back(ix(m.loops, f->GetOuterPolyLoop())); o.push_back((double)ls.GetSize()); for (ULONG j = 0; j < ls.GetSize(); ++j) @@ -700,6 +755,110 @@ void op_degen(Rd& rd, Model& m, Out& out) out.ints(K_RESULTS, o); } +// Only OP_OWNER_DEGEN consumes these records. Legacy labels remain opaque and +// their old -9 output is untouched. The FFI bounds Bezier nets to 2..=4 per axis. +SmSurface* read_owner_surface(Rd& rd, const SmContext& ctx) +{ + int64_t kind = rd.idx(); + if (kind == 0) + return NULL; + if (kind == 1) + { + SmPoint3d origin = rd.vec(); + SmVector3d x = rd.vec(), y = rd.vec(); + double sx = rd.num(), sy = rd.num(); + double u0 = rd.num(), v0 = rd.num(), u1 = rd.num(), v1 = rd.num(); + return new (ctx) SmPlane(origin, x, y, SmVector2d(sx, sy), SmExtent2d(u0, v0, u1, v1), &ctx); + } + gw_INDEX nu = (gw_INDEX)rd.idx(), nv = (gw_INDEX)rd.idx(); + gw_DEGREE p = (gw_DEGREE)(nu - 1), q = (gw_DEGREE)(nv - 1); + gw_SURFACE* s = sm_AllocateNurbSurface(nu - 1, nv - 1, p, q, 2 * nu - 1, 2 * nv - 1); + for (gw_INDEX i = 0; i < nu; ++i) + for (gw_INDEX j = 0; j < nv; ++j) + { + SmVector3d point = rd.vec(); + s->net->Pw[i][j].x = point.x; + s->net->Pw[i][j].y = point.y; + s->net->Pw[i][j].z = point.z; + s->net->Pw[i][j].w = NL_NOW; + } + for (gw_INDEX i = 0; i < 2 * nu; ++i) + s->knu->U[i] = i < nu ? 0.0 : 1.0; + for (gw_INDEX i = 0; i < 2 * nv; ++i) + s->knv->U[i] = i < nv ? 0.0 : 1.0; + return new (ctx) SmBSplineSurface(s, FALSE, &ctx); +} + +void read_owners(Rd& rd, const SmContext& ctx, Model& m) +{ + int64_t no = rd.idx(); + for (int64_t i = 0; i < no; ++i) + { + int64_t label = rd.idx(); + std::unique_ptr face(new (ctx) OwnerFace(read_owner_surface(rd, ctx))); + m.owner_labels.push_back(label); + m.owners.push_back(std::move(face)); + } +} + +bool op_owner_degen(Rd& rd, Model& m, Out& out) +{ + int64_t n = rd.idx(); + std::vector results, topo, points; + for (int64_t i = 0; i < n; ++i) + { + int64_t e = rd.idx(); + bool has = rd.flag(); + double t = rd.num(); + SmPolyEdge* pe = m.edges[e]; + SmPolyFace* pf = pe->GetPolyFace(); + SmFace* original = pf ? pf->GetOriginalFace() : NULL; + int64_t label = m.face_label(original); + SmFace* face = m.owner(label); + SmSurface* surface = face ? face->GetSurface() : NULL; + bool back = pe->GetOKBackPtrs() != FALSE; + bool mapping = back && surface && pe->Is2dOrZero(); + topo.insert(topo.end(), {(double)e, (double)label, face ? 1.0 : 0.0, + surface ? 1.0 : 0.0, back ? 1.0 : 0.0, mapping ? 1.0 : 0.0}); + SmPoint3d start = pe->GetStartPoint(), end = pe->GetEndPoint(); + put(points, start); + put(points, end); + if (mapping) + { + SmPoint2d a(start.x, start.y), b(end.x, end.y); + SmExtent2d domain = surface->GetNaturalUVDomain(); + SmExtentPointType su, sv, eu, ev; + domain.ClassifyPoint2d(a, su, sv, SM_EFF_ZERO_PARAM); + domain.ClassifyPoint2d(b, eu, ev, SM_EFF_ZERO_PARAM); + topo.insert(topo.end(), {(double)su, (double)sv, (double)eu, (double)ev}); + points.insert(points.end(), {domain.GetMin().x, domain.GetMin().y, + domain.GetMax().x, domain.GetMax().y}); + // Independent probes of the declared face surface, not a hand-written + // replacement for the native IsDegenerate decision below. + for (const SmPoint2d& uv : {a, b, (a + b) / 2.0}) + { + SmPoint3d point; + if (surface->EvaluatePoint(uv, point) != SM_SUCCESS) + return false; // upstream IsDegenerate ignores this status + put(points, point); + } + } + else + topo.insert(topo.end(), 4, -1.0); + if (original && back && face == NULL) + results.push_back(-9.0); // unresolved, never dereference a legacy label + else + { + SmXSectTol3d xs(t); + results.push_back(pe->IsDegenerate(has ? &xs : NULL) ? 1.0 : 0.0); + } + } + out.ints(K_RESULTS, results); + out.ints(K_OWNER_TOPO, topo); + out.floats(K_OWNER_POINTS, points); + return true; +} + void op_transform(Rd& rd, Model& m, Out& out) { SmVector3d org = rd.vec(); @@ -808,6 +967,8 @@ SC_EXPORT int32_t sc_query_poly_eval(const double* in, int64_t n_in, double* out Rd rd{in, 0}; int32_t op = (int32_t)rd.idx(); Model m; + if (op == OP_OWNER_DEGEN) + read_owners(rd, ctx, m); build(rd, ctx, m); switch (op) { @@ -826,6 +987,13 @@ SC_EXPORT int32_t sc_query_poly_eval(const double* in, int64_t n_in, double* out case OP_DEGEN: op_degen(rd, m, o); break; + case OP_OWNER_DEGEN: + if (!op_owner_degen(rd, m, o)) + { + delete m.brep; + return SC_ERR_ARG; + } + break; case OP_TRANSFORM: op_transform(rd, m, o); break;