Merge pull request #1395 from brett19/wire/trim-srf-cache-face-tree

wire: surface.trim_srf_cache — face-owned tree build (blend.hcr own scene; stacked on #1390)
This commit is contained in:
bherbruck
2026-10-28 19:51:47 +00:00
committed by GitHub
14 changed files with 698 additions and 167 deletions
+268 -109
View File
@@ -7,11 +7,16 @@
//! vector or a view placement, seeds silhouette edges (the silhouette branch of
//! `ComputeSilhouettes`' edge loop) and back-facing edges, and runs one member. It
//! exports the scene B-rep (`GetSceneBrep()`, the copy transformed by the unported
//! `SmBrep` copy constructor and `Transform`) with its `CalculateBoundingBox` (unit
//! `query.brep`, unported); the Rust side imports that scene record by record (the
//! `topology.vertexuse` importer), builds its HCR with [`Hcr::from_scene`], seeds the same
//! edges and runs the same member. Both sides first build the scene (shim op 8) and check
//! the seeds and op arguments against it with the same `ffi::validate`.
//! `SmBrep` copy constructor and `Transform`) with its `CalculateBoundingBox`. The Rust
//! side makes everything itself, in the shim's call order, so no C++ state reaches it:
//! the B-rep block built by the Rust kernel ([`crate::own_builder`], `.smb` files by the
//! Rust reader), the extra wire edge (`create_wire_edge_from_curve`), the HCR from
//! [`Hcr::new`] / [`Hcr::with_transform`] (its own scene copy, transform, infinite
//! region and bounding box), the scene renumbered into the shim's traversal order
//! ([`crate::own_builder::renumber_with`], so edge and vertex selectors name the same
//! entities), then the same seeds and member. The C++ side checks the seeds and op
//! arguments against its scene with `ffi::validate` (shim op 8); the Rust side makes the
//! same checks against its own scene ([`validate_rust`]).
//!
//! Compared: the construction state (transforms, projection box, ray offset tolerance,
//! view vector, options), `IsSilhouetteEdge` / `IsBackFacingEdge`, the intervals and
@@ -31,26 +36,21 @@
//! generated.
//!
//! Status `-2` marks an upstream path that is undefined (UB-170, null topology): the
//! shim does not run it and the port returns `InvalidInput`. Ops whose edge curves (or,
//! for op 5, face surfaces) the importer cannot hold exactly (an `SmEllipse` edge curve
//! is imported as its B-spline part) report `inexact_geometry` on both sides, decided
//! from the same export.
//! shim does not run it and the port returns `InvalidInput`. The Rust scene holds its
//! geometry natively, so no case is skipped as inexact.
//!
//! The importer leaves the surfaces' owners unset; the runner sets each face surface's
//! owner to its face, as the upstream B-rep build does, so `SmBrep::GetSurfaces`
//! (`IsPointVisible`) selects the same surfaces on both sides.
//! The renumbered scene's geometry is owned under the kernel's arena identities (and the
//! B-rep records that naming), so `SmBrep::GetSurfaces` (`IsPointVisible`) finds each face
//! surface's owner, as upstream's copy constructor sets it.
//!
//! `ComputeSilhouettes` and `ComputeGlobalVisibility` are not run: their C++ path reaches
//! UB-170 on open silhouette chains.
use super::topology_vertexuse::{
gen_brep, import, import_raw, path_of, push_curve, push_surface, sel, BrepIn, CrvIn,
};
use crate::{Case, CaseContext, HarnessError, Output, Rng, UnitDiff};
use super::topology_vertexuse::{gen_brep, path_of, push_curve, push_surface, sel, BrepIn, CrvIn};
use crate::{own_reader, Case, CaseContext, HarnessError, Output, Rng, UnitDiff};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use solidcore::blend::hcr::{new_curve_classification, view_transform_for_vector, Hcr, HcrObject, HcrOptions};
use solidcore::curve::curve::p2::{BSPLINE_CURVE_TYPE, LINE_TYPE};
use solidcore::blend::hcr::{new_curve_classification, Hcr, HcrObject, HcrOptions};
use solidcore::curve::curve::Curve;
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::extent1d::Extent1d;
@@ -59,9 +59,11 @@ use solidcore::intersect::curve_class::p1::CurveClassification;
use solidcore::intersect::point_class::PointClassification;
use solidcore::kernel::Kernel;
use solidcore::math::Vec3;
use solidcore::topology::brep_cache::{arena_identity, CacheObject};
use solidcore::topology::{edge, Brep, EdgeId, FaceId, VertexId};
use solidcore::SolidError;
use solidcore::geom::attribute::ObjectKey;
use solidcore::geom::object::ObjectRef;
use solidcore::topology::brep::p2::ArenaNotifyState;
use solidcore::topology::{edge, EdgeId, VertexId};
use solidcore::{EntityRef, SolidError};
use solidcore_ffi::units::{blend_hcr as ffi, topology_vertexuse as fixture};
use std::collections::BTreeSet;
@@ -235,23 +237,29 @@ fn ops_block(r: &Resolved) -> (u8, Vec<f64>) {
(op, v)
}
/// What both sides compute before running: the scene (shim op 8), the resolved
/// selectors and the op block, validated against the scene.
/// What the C++ side computes before running: the scene (shim op 8), the resolved
/// selectors and the op block, validated against that scene.
struct Prepared {
path: String,
brep: Vec<f64>,
hcr: Vec<f64>,
scene: Vec<f64>,
r: Resolved,
op: u8,
ops: Vec<f64>,
}
/// [`Prepared`], or the label of a rejection both sides report identically.
/// [`Prepared`], or the label of a rejection.
fn prepare(i: &Inputs) -> Result<Result<Prepared, String>, HarnessError> {
let path = path_of(&i.brep)?;
let brep = brep_block(&i.brep)?;
let hcr = hcr_block(i)?;
// a failed build of the B-rep block (the shim's `build` returns NULL), reported with
// its status as the Rust side reports its own failed build
if let Ok(ex) = fixture::eval(fixture::Op::Export, &path, &brep) {
if ex.first() == Some(&-1.0) {
let status = ex.get(1).copied().unwrap_or(0.0);
return Ok(Err(format!("brep_build_status_{status}")));
}
}
let scene = match ffi::scene(&path, &brep, &hcr) {
Ok(s) => s,
Err(e) => return Ok(Err(format!("rejected: {e}"))),
@@ -269,27 +277,225 @@ fn prepare(i: &Inputs) -> Result<Result<Prepared, String>, HarnessError> {
path,
brep,
hcr,
scene,
r,
op,
ops,
}))
}
fn exact_curve(types: &[f64], e: i64) -> bool {
types
.get(e as usize)
.is_some_and(|&t| t == BSPLINE_CURVE_TYPE as f64 || t == LINE_TYPE as f64)
// ---- the Rust side's own scene and checks ---------------------------------------------------
/// The label of an input rejection (`ffi::scene` / `ffi::validate`'s `bad`).
fn bad(msg: &str) -> String {
format!("rejected: invalid argument: blend.hcr: {msg}")
}
/// Whether the op depends on geometry the import does not hold exactly.
fn inexact(r: &Resolved, types: &[f64], exact_faces: &[bool]) -> bool {
let curves = |e: i64| !exact_curve(types, e) || r.sil.iter().any(|&s| !exact_curve(types, s));
match r.op {
OpIn::Intersect { edge, .. } | OpIn::CurveVisibility { edge, .. } => curves(edge),
OpIn::EdgeVisibility { edge, .. } => curves(edge) || exact_faces.iter().any(|x| !x),
_ => false,
/// `ffi`'s `MAX_SCENE_MAGNITUDE` check on the inputs (the `±f64::MAX` markers aside).
fn check_magnitudes(v: &[f64]) -> Result<(), String> {
if v.iter().any(|x| {
let a = x.abs();
a > ffi::MAX_SCENE_MAGNITUDE && a < f64::MAX
}) {
return Err(bad("scene value above 1e15 (grid walk does not end)"));
}
Ok(())
}
/// `ffi`'s view checks on the inputs (UB-169: a view vector `SmVector3d::Unitize`
/// cannot unitize; the placement, tolerances and flags are typed inputs here).
fn check_view(i: &Inputs) -> Result<(), String> {
if i.placement.is_none() {
let [x, y, z] = i.view;
let l = x * x + y * y + z * z;
if !(l >= 1.0e-24 && l.is_finite()) {
return Err(bad("UB-169: view vector cannot be unitized"));
}
}
Ok(())
}
/// `ffi::validate`'s checks of the seeds and op arguments (`ops`, as [`ops_block`]
/// makes them) against the Rust scene `h` (its edge and vertex counts and which edges
/// have a curve).
fn validate_rust(op: u8, h: &Hcr, ops: &[f64]) -> Result<(), String> {
if op == 7 {
check_magnitudes(ops)?;
}
let b = h.scene_brep();
let (n_e, n_v) = (b.edges().len(), b.vertices().len());
let mut at = 0usize;
let mut num = || -> Result<f64, String> {
let x = *ops.get(at).ok_or_else(|| bad("short input"))?;
at += 1;
if !x.is_finite() {
return Err(bad("non-finite number"));
}
Ok(x)
};
let count = |x: f64| -> Result<usize, String> {
if x.fract() != 0.0 || !(0.0..=64.0).contains(&x) {
return Err(bad("count out of range"));
}
Ok(x as usize)
};
let index = |x: f64, n: usize, allow_none: bool| -> Result<i64, String> {
if x.fract() != 0.0 || x < if allow_none { -1.0 } else { 0.0 } || x >= n as f64 {
return Err(bad("index out of range"));
}
Ok(x as i64)
};
let flag = |x: f64| -> Result<(), String> {
if x != 0.0 && x != 1.0 {
return Err(bad("flag"));
}
Ok(())
};
let n_sil = count(num()?)?;
for _ in 0..n_sil {
let e = index(num()?, n_e, false)?;
let has_curve = b.edge(eid(e)).is_ok_and(|x| x.curve.is_some());
if !has_curve {
return Err(bad("seeded silhouette edge without curve"));
}
}
let n_back = count(num()?)?;
for _ in 0..n_back {
index(num()?, n_e, false)?;
}
match op {
1 | 2 => {
index(num()?, n_e, false)?;
}
3 | 4 => {
index(num()?, n_e, false)?;
let (fa, fb) = (num()?, num()?);
if !(0.0..=1.0).contains(&fa) || !(0.0..=1.0).contains(&fb) {
return Err(bad("interval fractions"));
}
for _ in 0..2 {
let h = num()?;
if !(-9.223_372_036_854_775_808e18..9.223_372_036_854_775_808e18).contains(&h) {
return Err(bad("hint"));
}
}
}
5 => {
index(num()?, n_e, false)?;
index(num()?, n_v, true)?;
flag(num()?)?;
flag(num()?)?;
}
7 => {
for _ in 0..3 {
num()?;
}
}
_ => {}
}
if at != ops.len() {
return Err(bad("trailing op input"));
}
Ok(())
}
/// The identity of an original B-rep object the scene copy notifies under: the
/// notification naming in a key namespace of its own (as the sweep's HCR scene copy).
fn source_key(e: EntityRef) -> ObjectRef {
let r = own_reader::read_key_of(e);
ObjectRef {
type_code: r.type_code,
key: ObjectKey(r.key.0 | 1 << 50),
}
}
/// The Rust side's HCR, as the shim makes it: the B-rep block built by the Rust kernel,
/// the extra wire edge, the HCR constructor (scene copy, transform, infinite region,
/// bounding box), the scene renumbered into the shim's `walk` order; or the output of a
/// rejected or failed build.
fn rust_hcr(k: &mut Kernel, i: &Inputs) -> Result<Result<Hcr, Output>, HarnessError> {
use crate::own_builder::{build_as, failed_output, renumber_with, Built, Conventions};
let conv = &Conventions::EDGEUSE_GRAPH;
// `ffi::scene`'s input checks
let (brep, hcr) = (brep_block(&i.brep)?, hcr_block(i)?);
if let Err(m) = check_magnitudes(&brep).and_then(|()| check_magnitudes(&hcr)) {
return Ok(Err(Output::err(m)));
}
// the fixture export's block checks (`fixture::eval`; pure Rust, no shim call)
if let Err(e) = fixture::validate_block(&path_of(&i.brep)?, &brep) {
return Ok(Err(Output::err(format!("rejected: {e}"))));
}
if let Err(m) = check_view(i) {
return Ok(Err(Output::err(m)));
}
// the shim's `build` (`sc_topology_fixture.h`, no validate-only, no gap walk)
let built = match &i.brep {
BrepIn::File { rebuild, .. } => {
own_reader::file_in_order(k, &path_of(&i.brep)?, *rebuild, false, false, conv)?
}
src => build_as(k, src, false, false, conv)?
.ok_or_else(|| harness("own builder: no build for the source"))?,
};
let mut b = match built {
Built::Brep(b) => *b,
Built::Failed(e) => return Ok(Err(failed_output(&e))),
};
// the extra wire edge (`CreateWireEdgeFromCurve`)
if let Some(x) = &i.extra {
let curve: super::topology_edgeuse_p1::CrvIn = serde_json::to_value(&x.curve)
.and_then(serde_json::from_value)
.map_err(|e| harness(format!("extra curve: {e}")))?;
let c = Curve::from(super::topology_edgeuse_p1::bspline_of(&curve)?);
if let Err(e) = solidcore::topology::brep::p5::create_wire_edge_from_curve(
k,
&mut b,
c,
Extent1d::from_min_max(x.t0, x.t1),
) {
return Ok(Err(Output::unported(&e)
.unwrap_or_else(|| Output::err(format!("extra_wire_failed: {e}")))));
}
}
let opts = HcrOptions {
approx_tol_3d: i.approx_tol,
ang_tol_rad: i.ang_tol,
display_seams: i.seams,
display_smooth_edges: i.smooth,
};
let mut state = ArenaNotifyState::default();
let made = match i.placement {
None => Hcr::new(
k,
&mut state.env(None),
&b,
&source_key,
Vec3::new(i.view[0], i.view[1], i.view[2]),
opts,
),
Some([z, x]) => {
let mut a = Axis2Placement::default();
a.set_step_canonical(
Vec3::new(i.view[0], i.view[1], i.view[2]),
Vec3::new(z[0], z[1], z[2]),
Vec3::new(x[0], x[1], x[2]),
)
.and_then(|()| Hcr::with_transform(k, &mut state.env(None), &b, &source_key, &a, opts))
}
};
let mut h = match made {
Ok(h) => h,
Err(e) => {
return Ok(Err(Output::unported(&e)
.unwrap_or_else(|| Output::err(format!("hcr_construct_failed: {e}")))))
}
};
// the shim's `walk(scene, g)` order: the scene's arena ids renumbered to it (nothing
// in the freshly constructed HCR refers to a scene id yet), its geometry owned under
// the arena identities the renumbering gives it
let mut scene = renumber_with(h.scene_brep(), conv)?;
scene
.use_arena_naming()
.map_err(|e| harness(format!("scene naming: {e}")))?;
*h.scene_brep_mut() = scene;
Ok(Ok(h))
}
fn output(status: f64, record: Vec<f64>) -> Output {
@@ -307,11 +513,6 @@ impl UnitDiff for Runner {
Ok(p) => p,
Err(msg) => return Ok(Output::err(msg)),
};
let s = ffi::split(&p.scene).ok_or_else(|| harness("short scene output"))?;
let (_, exact_faces) = import_raw(s.export)?;
if inexact(&p.r, s.curve_types, &exact_faces) {
return Ok(Output::err("inexact_geometry"));
}
let v = match ffi::eval(p.op, &p.path, &p.brep, &p.hcr, &p.ops) {
Ok(v) => v,
Err(e) => return Ok(Output::err(format!("rejected: {e}"))),
@@ -322,42 +523,20 @@ impl UnitDiff for Runner {
fn run_rust(&self, case: &Case) -> Result<Output, HarnessError> {
let i: Inputs = case.parse()?;
let p = match prepare(&i)? {
Ok(p) => p,
Err(msg) => return Ok(Output::err(msg)),
let mut k = Kernel::default();
let h = match rust_hcr(&mut k, &i)? {
Ok(h) => h,
Err(o) => return Ok(o),
};
let s = ffi::split(&p.scene).ok_or_else(|| harness("short scene output"))?;
let (mut scene, exact_faces) = import(s.export)?;
if inexact(&p.r, s.curve_types, &exact_faces) {
return Ok(Output::err("inexact_geometry"));
}
// the surfaces' owners (`SmSurface::GetOwner`, set by the B-rep build upstream and
// read by `SmBrep::GetSurfaces`): the kernel's arena identities
let face_ids: Vec<FaceId> = scene.faces().ids().collect();
for id in face_ids {
if let Some(surface) = scene
.face_mut(id)
.map_err(|e| harness(e.to_string()))?
.surface
.as_mut()
{
let _ = surface.set_owner(Some(arena_identity(CacheObject::Face(id))));
}
// the face's tolerance, as the face upstream answers GetTolerance with
scene
.sync_face_surface_tolerance(id)
.map_err(|e| harness(e.to_string()))?;
}
let [x0, y0, z0, x1, y1, z1] = s.scene_box;
let (lo, hi) = (Vec3::new(x0, y0, z0), Vec3::new(x1, y1, z1));
let scene_box = if lo.x <= hi.x && lo.y <= hi.y && lo.z <= hi.z {
Extent3d::from_points(lo, hi)
} else if Extent3d::new().min() == lo && Extent3d::new().max() == hi {
Extent3d::new()
} else {
return Err(harness("scene box neither ordered nor empty"));
let b = h.scene_brep();
let Some(r) = resolve(&i, b.edges().len(), b.vertices().len()) else {
return Ok(Output::err("no_edge"));
};
match run_rust(&i, &p.r, scene, scene_box) {
let (op, ops) = ops_block(&r);
if let Err(m) = validate_rust(op, &h, &ops) {
return Ok(Output::err(m));
}
match run_rust(&k, h, &r) {
Ok((status, record)) => Ok(output(status, record)),
Err(e) => Ok(Output::unported(&e).unwrap_or_else(|| match e {
SolidError::InvalidInput {
@@ -445,29 +624,9 @@ fn put_piece(k: &Kernel, o: &mut Vec<f64>, c: &Curve) {
}
}
fn run_rust(i: &Inputs, r: &Resolved, scene: Brep, scene_box: Extent3d) -> Result<(f64, Vec<f64>), SolidError> {
let k = Kernel::default();
let transform = match i.placement {
None => view_transform_for_vector(Vec3::new(i.view[0], i.view[1], i.view[2]))?,
Some([z, x]) => {
let mut a = Axis2Placement::default();
a.set_step_canonical(
Vec3::new(i.view[0], i.view[1], i.view[2]),
Vec3::new(z[0], z[1], z[2]),
Vec3::new(x[0], x[1], x[2]),
)?;
a
}
};
let opts = HcrOptions {
approx_tol_3d: i.approx_tol,
ang_tol_rad: i.ang_tol,
display_seams: i.seams,
display_smooth_edges: i.smooth,
};
let mut h = Hcr::from_scene(&k, scene, &transform, scene_box, opts);
fn run_rust(k: &Kernel, mut h: Hcr, r: &Resolved) -> Result<(f64, Vec<f64>), SolidError> {
for &e in &r.sil {
h.add_silhouette_edge(&k, eid(e))?;
h.add_silhouette_edge(k, eid(e))?;
}
for &e in &r.back {
h.add_back_facing_edge(eid(e));
@@ -484,19 +643,19 @@ fn run_rust(i: &Inputs, r: &Resolved, scene: Brep, scene_box: Extent3d) -> Resul
rec.extend([v.x, v.y, v.z, h.approx_tol_3d(), h.ang_tol_rad()]);
rec.extend([flag(h.display_seams()), flag(h.display_smooth_edges())]);
}
OpIn::IsSilhouette { edge } => rec.push(flag(h.is_silhouette_edge(&k, eid(edge))?)),
OpIn::IsBackFacing { edge } => rec.push(flag(h.is_back_facing_edge(&k, eid(edge))?)),
OpIn::IsSilhouette { edge } => rec.push(flag(h.is_silhouette_edge(k, eid(edge))?)),
OpIn::IsBackFacing { edge } => rec.push(flag(h.is_back_facing_edge(k, eid(edge))?)),
OpIn::Intersect { edge, fa, fb } | OpIn::CurveVisibility { edge, fa, fb, .. } => {
let e = eid(edge);
let ivl = edge::interval(h.scene_brep(), e)?;
let ivl = Extent1d::from_min_max(ivl.evaluate(fa), ivl.evaluate(fb));
let tol = edge::tolerance(h.scene_brep(), e)?;
let mut cc = new_curve_classification(&k, HcrObject::Curve(e), ivl, tol);
let mut cc = new_curve_classification(k, HcrObject::Curve(e), ivl, tol);
let res = match r.op {
OpIn::CurveVisibility { start_hint, end_hint, .. } => {
h.compute_curve_visibility(&k, &mut cc, hint_ulong(start_hint), hint_ulong(end_hint))
h.compute_curve_visibility(k, &mut cc, hint_ulong(start_hint), hint_ulong(end_hint))
}
_ => h.intersect_curve_with_silhouettes(&k, &mut cc),
_ => h.intersect_curve_with_silhouettes(k, &mut cc),
};
match res {
Ok(()) => {}
@@ -512,18 +671,18 @@ fn run_rust(i: &Inputs, r: &Resolved, scene: Brep, scene_box: Extent3d) -> Resul
marked.insert(eid(edge));
}
let v = (vertex >= 0).then(|| VertexId::from_index(vertex as u32));
match h.compute_edge_visibility(&k, eid(edge), v, visible, &mut marked) {
match h.compute_edge_visibility(k, eid(edge), v, visible, &mut marked) {
Ok(()) => {}
Err(SolidError::Upstream { code, .. }) => status = f64::from(code),
Err(e) => return Err(e),
}
rec.push(h.visible_curves().len() as f64);
for c in h.visible_curves() {
put_piece(&k, &mut rec, c);
put_piece(k, &mut rec, c);
}
rec.push(h.invisible_curves().len() as f64);
for c in h.invisible_curves() {
put_piece(&k, &mut rec, c);
put_piece(k, &mut rec, c);
}
}
OpIn::SilhouetteEdges => {
@@ -533,7 +692,7 @@ fn run_rust(i: &Inputs, r: &Resolved, scene: Brep, scene_box: Extent3d) -> Resul
rec.push(edges.len() as f64);
rec.extend(edges.iter().map(|e| f64::from(e.index())));
}
OpIn::PointVisible { p } => rec.push(flag(h.is_point_visible(&k, Vec3::new(p[0], p[1], p[2]))?)),
OpIn::PointVisible { p } => rec.push(flag(h.is_point_visible(k, Vec3::new(p[0], p[1], p[2]))?)),
}
Ok((status, rec))
}
@@ -41,6 +41,10 @@ struct Inputs {
#[serde(default)]
occurrence: u32,
build: bool,
/// `GetTree` after the build step: 0 none, 1 with the editing flag as is, 2 with
/// editing disabled first.
#[serde(default)]
get_tree: u8,
#[serde(default)]
editing: bool,
#[serde(default)]
@@ -61,12 +65,22 @@ fn flag(value: bool) -> f64 {
}
impl Inputs {
/// The shim's `build_mode` (see the FFI module).
fn build_mode(&self) -> f64 {
match (self.build, self.get_tree) {
(false, 0) => 0.0,
(true, 0) => 1.0,
(true, g) => 1.0 + f64::from(g),
(false, g) => 3.0 + f64::from(g),
}
}
fn flat(&self) -> Vec<f64> {
let mut out = vec![
f64::from(self.surface_type),
f64::from(self.loops),
f64::from(self.occurrence),
flag(self.build),
self.build_mode(),
flag(self.editing),
flag(self.modified),
flag(self.test_tol),
@@ -81,6 +95,9 @@ impl Inputs {
fn parse(value: &Value) -> Result<Inputs, HarnessError> {
let input: Inputs = serde_json::from_value(value.clone())
.map_err(|e| harness(format!("native_face input: {e}")))?;
if input.get_tree > 2 {
return Err(harness("native_face get_tree must be 0, 1 or 2"));
}
ffi::validate(&input.path, &input.flat()).map_err(|e| harness(e.to_string()))?;
Ok(input)
}
@@ -359,8 +376,20 @@ fn rust_record(input: &Inputs, brep: &mut Brep, face: FaceId) -> SolidResult<Vec
EntityRef::Face(face),
tolerance,
)?;
if input.build {
let built = cache.build_tree_with_face(&k, &mut BrepCacheFace { b: brep, f: face });
if input.build || input.get_tree != 0 {
let mut built = if input.build {
cache.build_tree_with_face(&k, &mut BrepCacheFace { b: brep, f: face })
} else {
Ok(())
};
if built.is_ok() && input.get_tree != 0 {
if input.get_tree == 2 {
brep.set_editing_enabled(false);
}
built = cache
.get_tree_with_face(&k, &mut BrepCacheFace { b: brep, f: face })
.map(|_| ());
}
let build_status = match built {
Ok(()) => SUCCESS,
Err(error) => status(error)?,
@@ -3,8 +3,11 @@
//! Upstream: `source/SMLib/src/SmTrimSrfCache.cpp` (`BuildTree`, `PointTest`,
//! `PointClassify`). Shim: `harness/cpp/shim/surface_trim_srf_cache_faces.cpp`.
//!
//! Input: `surface_type loop_count occurrence build editing modified test_tol
//! Input: `surface_type loop_count occurrence build_mode editing modified test_tol
//! boundary_probes sample_count (u_fraction v_fraction source_tolerance)*`.
//! `build_mode` is 0 (none), 1 (`BuildTree`), 2 (`BuildTree`, then `GetTree`), 3
//! (`BuildTree`, editing disabled, `GetTree`), 4 (`GetTree`) or 5 (editing disabled,
//! `GetTree`); `build_status` is that of the last call (`GetTree` NULL is `SM_ERR`).
//! The selector is the zero-based occurrence among faces of that **exact** concrete
//! surface type and upward-loop count in the shared native Graph walk.
//!
@@ -70,8 +73,12 @@ pub fn validate(path: &str, input: &[f64]) -> Result<(), FfiError> {
));
}
}
let mode = *head.get(3).ok_or_else(|| bad("missing build mode"))?;
if mode.fract() != 0.0 || !(0.0..=5.0).contains(&mode) {
return Err(bad("build mode must be an integer in 0..=5"));
}
if head
.get(3..8)
.get(4..8)
.ok_or_else(|| bad("missing flags"))?
.iter()
.any(|v| *v != 0.0 && *v != 1.0)
@@ -149,7 +156,9 @@ mod tests {
(0, 15030.0),
(1, -1.0),
(2, 0.5),
(3, 2.0),
(3, 6.0),
(3, 0.5),
(4, 2.0),
(8, 65.0),
(9, f64::INFINITY),
(10, f64::NAN),
@@ -269,6 +269,20 @@ pub(crate) fn brep_block_at(input: &[f64], path: &str) -> Result<usize, FfiError
Ok(rd.at)
}
/// The input checks [`eval`] makes on a B-rep block before any shim call (pure Rust,
/// no C++ is run): a runner whose Rust side builds the block itself refuses exactly the
/// blocks [`eval`] refuses.
///
/// # Errors
///
/// [`FfiError::InvalidArgument`] for a block [`eval`] rejects.
pub fn validate_block(path: &str, input: &[f64]) -> Result<(), FfiError> {
let mut rd = Reader::new(input);
brep_block(&mut rd, path)?;
let block = input.get(..rd.at).ok_or_else(|| bad("input too short"))?;
check_block_magnitudes(block)
}
/// Most faces of a kind-10 block.
pub const MAX_FACES: usize = 4;
+122 -42
View File
@@ -70,7 +70,12 @@
//! [`Hcr::compute_silhouettes`] and [`Hcr::compute_global_visibility`] take the
//! kernel mutably.
//! - `IsPointVisible` keeps the ray tracer's surface set and grid and rebuilds the
//! tracer (which borrows the surfaces) for each ray.
//! tracer (which borrows the surfaces) for each ray. A face-owned surface's cache
//! (`smsurf_GetSurfaceCache`, then the `GetTree` calls of `AddSurfaceToGrid` and
//! `ExpandHierarchy`, in that order) runs through its face before the tracer borrows
//! the surfaces: the trimmed cache's `BuildTree` reads the face and may change its
//! B-rep. The tracer then reads that tree as is
//! (`RayTracer::add_surface_with_fetched_cache`).
//! - `DrawVisibleCurves`, `IsConcaveSilhouette` and `IsFrontFacingFace` are not owned by
//! this unit (no caller, graphics only).
@@ -102,10 +107,13 @@ use crate::nurbs::b_spline_curve_n_lib::TrimMode;
use crate::nurbs::b_spline_surface::BSplineSurface;
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
use crate::query::brep::{self as qbrep, BoundingBoxOptions};
use crate::query::face::p2::BrepCacheFace;
use crate::query::face::p2::{point_classify_prepared, PointClassifyOptions};
use crate::surface::surface::p1::get_surface_cache_with_face;
use crate::surface::surface::p2::surface_cache;
use crate::surface::surface::{Surface, SurfaceKind};
use crate::surface::surface_cache::face_context::prepare_face_surface_cache;
use crate::surface::surface_cache::face_context::MappedCacheFace;
use crate::surface::surface_cache::SurfaceCache;
use crate::surface::volume::Volume;
use crate::topology::{edge, edgeuse, vertex, vertexuse};
use crate::topology::{Brep, BrepId, EdgeId, EdgeuseId, FaceId, RegionId, VertexId};
@@ -498,7 +506,92 @@ enum RaySurface {
/// First cache handle of a scene entity: below it are the tracer's surface indices.
const ENTITY_HANDLE_BASE: usize = usize::MAX / 2;
/// The handles `prepare_face_surface_cache` addresses the scene's entities by, in the
/// A surface cache the tracer lists: one whose `GetTree` calls the tracer runs itself
/// (`Lazy`), or a face-owned one whose `GetTree` calls already ran through its face
/// (`Fetched`, see [`face_surface_cache`]).
enum ListedCache {
Lazy(SurfaceCache<usize>),
Fetched(SurfaceCache<usize>),
}
/// `smsurf_GetSurfaceCache` of a surface without face and `AddSurfaceToGrid`'s
/// `GetTree() == NULL` check: the cache, `None` when the cache or its tree fails (the
/// surface is not listed; the status is ignored upstream) or the tree has no top node.
///
/// # Errors
///
/// [`SolidError::Unported`] from the cache build.
fn standalone_surface_cache(
k: &Kernel,
surface: &Surface,
handle: usize,
) -> Result<Option<ListedCache>> {
let mut cache = match surface_cache::<usize>(k, surface, Some(handle)) {
Ok(cache) => cache,
Err(e) if e.is_unported() => return Err(e),
Err(_) => return Ok(None),
};
// `AddSurfaceToGrid` lists the surface only once its tree has a top node
let has_top = match cache.get_tree(k) {
Ok(tree) => tree.top_node().is_some(),
Err(e) if e.is_unported() => return Err(e),
Err(_) => false,
};
Ok(has_top.then_some(ListedCache::Lazy(cache)))
}
/// `smsurf_GetSurfaceCache` of the surface of face `f` of `scene` (built through the
/// face, which may create its UV trim curves), then the three `GetTree` calls the
/// tracer makes on it (`AddSurfaceToGrid`'s `GetTree() == NULL` check and
/// `GetTree()->GetTopNode()`, then `ExpandHierarchy`'s `GetTree()`), each with the
/// face: a face-owned trimmed cache whose face has no trim curves, or whose B-rep is
/// being edited, rebuilds on every call (`SmTrimSrfCache::BuildTree`). `None` when a
/// step fails (the surface is not listed) or the tree has no top node.
///
/// # Errors
///
/// [`SolidError::Unported`] from the cache build; those of the face lookups.
fn face_surface_cache(
k: &Kernel,
scene: &mut Brep,
f: FaceId,
handle: usize,
handle_of: &dyn Fn(EntityRef) -> Result<usize>,
entity_of: &dyn Fn(usize) -> Result<EntityRef>,
) -> Result<Option<ListedCache>> {
// the cache refers to the face's surface (an exact evaluation copy: the face's
// B-rep is changed through the face while the cache is built)
let surface = scene
.face(f)?
.surface
.as_ref()
.ok_or(undefined("face without surface"))?
.clone_for_evaluation()?;
let mut mapped = match MappedCacheFace::new(BrepCacheFace { b: scene, f }, handle_of, entity_of)
{
Ok(mapped) => mapped,
Err(e) if e.is_unported() => return Err(e),
Err(_) => return Ok(None),
};
let mut cache = match get_surface_cache_with_face(k, &surface, Some(handle), &mut mapped) {
Ok(cache) => cache,
Err(e) if e.is_unported() => return Err(e),
Err(_) => return Ok(None),
};
// AddSurfaceToGrid: GetTree() == NULL, GetTree()->GetTopNode(); ExpandHierarchy:
// GetTree()
for check_top in [true, true, false] {
match cache.get_tree_with_face(k, &mut mapped) {
Ok(tree) if check_top && tree.top_node().is_none() => return Ok(None),
Ok(_) => {}
Err(e) if e.is_unported() => return Err(e),
Err(_) => return Ok(None),
}
}
Ok(Some(ListedCache::Fetched(cache.into_base())))
}
/// The handles [`face_surface_cache`] addresses the scene's entities by, in the
/// tracer's usize handle space above [`ENTITY_HANDLE_BASE`], and the inverse table.
fn entity_handles(b: &Brep) -> (BTreeMap<EntityRef, usize>, Vec<EntityRef>) {
let all: Vec<EntityRef> = b
@@ -1509,23 +1602,19 @@ impl Hcr {
}
RaySurface::Owned(_) => None,
};
let built = match (face_owned, s) {
let handle = listed_faces.len();
let fetched = match (face_owned, s) {
(Some(f), _) => {
let (to_handle, to_entity) =
entities.get_or_insert_with(|| entity_handles(&self.scene));
prepare_face_surface_cache(
k,
&mut self.scene,
f,
listed_faces.len(),
&|e| to_handle.get(&e).copied().ok_or(undefined("cache entity")),
&|h| {
h.checked_sub(ENTITY_HANDLE_BASE)
.and_then(|i| to_entity.get(i))
.copied()
.ok_or(undefined("cache handle"))
},
)
let handle_of = |e| to_handle.get(&e).copied().ok_or(undefined("cache entity"));
let entity_of = |h: usize| {
h.checked_sub(ENTITY_HANDLE_BASE)
.and_then(|i| to_entity.get(i))
.copied()
.ok_or(undefined("cache handle"))
};
face_surface_cache(k, &mut self.scene, f, handle, &handle_of, &entity_of)?
}
(None, RaySurface::Face(f)) => {
let surface = self
@@ -1534,31 +1623,16 @@ impl Hcr {
.surface
.as_ref()
.ok_or(undefined("face without surface"))?;
surface_cache::<usize>(k, surface, Some(listed_faces.len()))
standalone_surface_cache(k, surface, handle)?
}
(None, RaySurface::Owned(surface)) => {
surface_cache::<usize>(k, surface, Some(listed_faces.len()))
(None, RaySurface::Owned(surface)) => standalone_surface_cache(k, surface, handle)?,
};
match fetched {
Some(cache) => {
listed_faces.push(face_owned);
caches.push(Some(cache));
}
};
let mut cache = match built {
Ok(cache) => cache,
Err(e) if e.is_unported() => return Err(e),
Err(_) => {
caches.push(None);
continue;
}
};
// `AddSurfaceToGrid` lists the surface only once its tree has a top node
let has_top = match cache.get_tree(k) {
Ok(tree) => tree.top_node().is_some(),
Err(e) if e.is_unported() => return Err(e),
Err(_) => false,
};
if has_top {
listed_faces.push(face_owned);
caches.push(Some(cache));
} else {
caches.push(None);
None => caches.push(None),
}
}
@@ -1584,8 +1658,14 @@ impl Hcr {
.ok_or(undefined("face without surface"))?,
RaySurface::Owned(ref s) => &**s,
};
if let Some(cache) = cache {
ignore_status(tracer.add_surface_with_cache(k, surface, cache))?;
match cache {
Some(ListedCache::Fetched(cache)) => {
ignore_status(tracer.add_surface_with_fetched_cache(k, surface, cache))?;
}
Some(ListedCache::Lazy(cache)) => {
ignore_status(tracer.add_surface_with_cache(k, surface, cache))?;
}
None => {}
}
}
+39 -2
View File
@@ -15,7 +15,7 @@
//! | [`Grid::add_element`], [`ElementSource`] | `SmGrid::AddElement`, `SmGridElementType` |
//! | [`Grid::add_surface_node`], [`Grid::add_surface_element`] | `SmGrid::AddElement(SM_GE_SURFACE)` (from a tree node; from prepared leaf data) |
//! | [`Grid::add_hierarchy_element`] | `SmGrid::AddHierarchyElement` |
//! | [`Grid::expand_hierarchy`] | `SmSurfaceHierarchyElement::ExpandHierarchy` |
//! | [`Grid::expand_hierarchy`], [`Grid::expand_hierarchy_fetched`] | `SmSurfaceHierarchyElement::ExpandHierarchy` |
//! | [`Grid::expand_hierarchy_with`] | `SmSurfaceHierarchyElement::ExpandHierarchy` over a caller-held surface cache tree |
//! | [`Grid::setup_3ddda`] | `SmGrid::Setup3DDDA` |
//! | `AxisStep` (private) | one axis of `SmGrid::Setup3DDDA` |
@@ -1180,6 +1180,33 @@ impl<O: Copy + PartialEq> Grid<O> {
k: &Kernel,
id: HierarchyId,
cache: &mut SurfaceCache<O>,
) -> Result<()> {
self.expand_hierarchy_in(k, id, cache, true)
}
/// [`Grid::expand_hierarchy`] for a cache whose `GetTree` the caller has already
/// run (a face-owned trimmed cache, whose tree build needs its face:
/// [`TrimSrfCache::get_tree_with_face`](crate::surface::trim_srf_cache::TrimSrfCache::get_tree_with_face)):
/// the cache's tree is read as is, never rebuilt.
///
/// # Errors
///
/// As [`Grid::expand_hierarchy`]; `InvalidInput` when the cache has no tree.
pub fn expand_hierarchy_fetched(
&mut self,
k: &Kernel,
id: HierarchyId,
cache: &mut SurfaceCache<O>,
) -> Result<()> {
self.expand_hierarchy_in(k, id, cache, false)
}
fn expand_hierarchy_in(
&mut self,
k: &Kernel,
id: HierarchyId,
cache: &mut SurfaceCache<O>,
fetch: bool,
) -> Result<()> {
const OP: &str = "geom::grid::Grid::expand_hierarchy";
let element = self.hierarchy.get(id.0).ok_or_else(|| bad_id(OP))?;
@@ -1191,7 +1218,17 @@ impl<O: Copy + PartialEq> Grid<O> {
});
}
// The clone lets `init` borrow the cache mutably (it builds polar boxes).
let tree = cache.get_tree(k)?.clone();
let tree = if fetch {
cache.get_tree(k)?.clone()
} else {
cache
.tree()
.ok_or(SolidError::InvalidInput {
operation: OP,
reason: "surface cache has no tree",
})?
.clone()
};
self.expand_hierarchy_with(id, &tree, |_, node| {
SurfaceGridElement::init(k, cache, node)
})
+26 -1
View File
@@ -13,7 +13,7 @@
//! | `Drop` (implicit) | `~SmRayTracer` |
//! | [`RayTracer::set_solver_tolerance`] | `SmRayTracer::SetSolverTolerance` |
//! | [`RayTracer::fire_ray`], [`RayHit`] | `SmRayTracer::FireRay` |
//! | [`RayTracer::add_surface_to_grid`], [`RayTracer::add_surface_with_bbox`] | `SmRayTracer::AddSurfaceToGrid` |
//! | [`RayTracer::add_surface_to_grid`], [`RayTracer::add_surface_with_bbox`], [`RayTracer::add_surface_with_fetched_cache`] | `SmRayTracer::AddSurfaceToGrid` |
//! | [`RayTracer::add_poly_brep_to_grid`] | `SmRayTracer::AddPolyBrepToGrid` |
//! | [`RayTracer::poly_brep`], [`RayTracer::poly_brep_count`] | `m_sPolyBreps` |
//! | [`RayRenderer`], [`RayRenderer::new`] | `SmRayRenderer`, `SmRayRenderer::SmRayRenderer` |
@@ -1251,6 +1251,31 @@ impl<'s> RayTracer<'s> {
self.grid.expand_hierarchy(k, id, &mut cache)
}
/// [`RayTracer::add_surface_with_cache`] for a cache whose `GetTree` calls the
/// caller has already run, in upstream order: `AddSurfaceToGrid`'s two and
/// `ExpandHierarchy`'s one. A face-owned trimmed cache rebuilds its tree through
/// its face ([`TrimSrfCache::get_tree_with_face`](crate::surface::trim_srf_cache::TrimSrfCache::get_tree_with_face)),
/// which may change the face's B-rep, so it cannot run while the tracer borrows
/// the surfaces. Here the tree is read as is.
///
/// # Errors
///
/// As [`RayTracer::add_surface_with_cache`]; `SM_ERR` when the cache has no tree
/// or the tree no top node.
pub fn add_surface_with_fetched_cache(
&mut self,
k: &Kernel,
surface: &'s Surface,
mut cache: SurfaceCache<usize>,
) -> Result<()> {
const OP: &str = "intersect::ray_tracer::add_surface_with_fetched_cache";
let tree = cache.tree().ok_or(SolidError::sm_err(OP))?;
let top = tree.top_node().ok_or(SolidError::sm_err(OP))?;
let bbox = tree.node(top)?.bbox;
let id = self.add_surface_with_bbox(surface, &bbox)?;
self.grid.expand_hierarchy_fetched(k, id, &mut cache)
}
/// Embeds the faces of a poly B-rep in the grid (`AddPolyBrepToGrid`): notifies
/// the poly B-rep of an edit (`env` is its notification environment), builds its
/// `SmBrepCache` and adds every leaf of the cache's surface tree to the grid as a
+36 -1
View File
@@ -15,6 +15,7 @@
//! | [`TrimSrfCache::point_test`] | `SmTrimSrfCache::PointTest` |
//! | [`TrimSrfCache::point_classify`] | `SmTrimSrfCache::PointClassify` |
//! | [`TrimSrfCache::build_tree`], [`TrimSrfCache::build_tree_with_face`] | `SmTrimSrfCache::BuildTree` |
//! | [`TrimSrfCache::get_tree_with_face`] | `SmSurfaceCache::GetTree` of a trimmed cache (virtual `BuildTree`) |
//! | [`TrimSrfCache::point_test_in`], [`TrimSrfCache::point_classify_in`] | `PointTest`, `PointClassify` over the given face |
//! | [`CacheFace`] | `GetFace()` and what the cache reads from the face and its B-rep |
//! | [`TrimSrfCache::bind_face`] | `GetFace()` with an explicit caller-handle mapping |
@@ -88,7 +89,7 @@ use crate::geom::extent1d::Extent1d;
use crate::geom::extent2d::Extent2d;
use crate::geom::extent3d::Extent3d;
use crate::geom::object::ObjectRef;
use crate::geom::tree::{NodeClass, NodeData, NodeId};
use crate::geom::tree::{NodeClass, NodeData, NodeId, Tree};
use crate::geom::vector2d::{Point2, Vec2};
use crate::geom::vector3d::{Point3, Vec3};
use crate::kernel::Kernel;
@@ -133,6 +134,7 @@ pub const POINT_CLASS_VERTEX: i32 = 104;
const OP_CLASSIFY: &str = "surface::trim_srf_cache::TrimSrfCache::point_classify";
const OP_EU: &str = "surface::trim_srf_cache::TrimSrfCache::implant_edgeuse";
const OP_VU: &str = "surface::trim_srf_cache::TrimSrfCache::implant_vertexuse";
const OP_GET_TREE: &str = "surface::trim_srf_cache::TrimSrfCache::get_tree_with_face";
const OP_BUILD: &str = "surface::trim_srf_cache::TrimSrfCache::build_tree_with_face";
/// `SmTrimSrfCache::IsKindOf`: the trimmed surface cache or a base type.
@@ -696,6 +698,39 @@ impl<O: Copy + PartialEq> TrimSrfCache<O> {
self.build_tree_in(k, Some(face))
}
/// The tree, built first when there is none, or when the surface belongs to a face
/// whose boundary is not in the tree yet and no build is under way
/// (`SmSurfaceCache::GetTree`, whose virtual `BuildTree` is this cache's
/// [`TrimSrfCache::build_tree_with_face`]); `face` is the face owning the cache's
/// surface (`GetFace()`). A cache that is not a trimmed one (the plain
/// `SmSurfaceCache` fallback of `smsurf_GetSurfaceCache`) runs its own
/// [`SurfaceCache::get_tree`].
///
/// # Errors
///
/// Those of [`TrimSrfCache::build_tree_with_face`] (upstream returns NULL), and
/// of [`SurfaceCache::get_tree`] for a plain cache.
pub fn get_tree_with_face(
&mut self,
k: &Kernel,
face: &mut dyn CacheFace<O>,
) -> Result<&mut Tree<O>> {
if !self.base.trimmed_dispatch {
return self.base.get_tree(k);
}
let face_owned = self.base.surface().face()?.is_some();
if self.base.tree().is_none()
|| (!self.base.have_t_surface_cache
&& face_owned
&& self.base.face_containment_done != FaceContainment::UnderConstruction)
{
self.build_tree_with_face(k, face)?;
}
self.base
.tree_mut()
.ok_or_else(|| invalid(OP_GET_TREE, "cache has no tree"))
}
fn build_tree_in(&mut self, k: &Kernel, face: Option<&mut dyn CacheFace<O>>) -> Result<()> {
// prevent bad recursion back into BuildTree
let saved = self.base.face_containment_done;
@@ -69,6 +69,76 @@
"test_tol": true, "boundary_probes": true,
"samples": [[0.5,0.5,0.00001], [-0.25,0.5,0.00001]]
}}
},
{
"name": "native_face_bspline_get_tree_editing_rebuilds_standalone",
"upstream_ref": "TestFiles/pt_TestFiles/Mouse.smb; source/SMLib/src/SmSurfaceCache.cpp:5459 (GetTree); source/SMLib/src/SmTrimSrfCache.cpp:132; GetTree on an editing-enabled face (the blend.hcr IsPointVisible regime): BuildTree runs again on the built tree and keeps the standalone flags",
"inputs": {"native_face": {
"path": {"$upstream_file": "Mouse.smb"},
"surface_type": 15002, "loops": 1, "build": true, "get_tree": 1, "editing": true,
"test_tol": true, "boundary_probes": true,
"samples": [[0.5,0.5,0.00001], [0.25,0.5,0.00001], [-0.25,0.5,0.00001], [1.25,0.5,0.00001]]
}}
},
{
"name": "native_face_bspline_get_tree_after_editing_implants_trims",
"upstream_ref": "TestFiles/pt_TestFiles/Mouse.smb; source/SMLib/src/SmSurfaceCache.cpp:5459 (GetTree); source/SMLib/src/SmTrimSrfCache.cpp:132; BuildTree with editing on, then editing off and GetTree: the rebuild implants the trims into the existing tree",
"inputs": {"native_face": {
"path": {"$upstream_file": "Mouse.smb"},
"surface_type": 15002, "loops": 1, "build": true, "get_tree": 2, "editing": true,
"test_tol": true, "boundary_probes": true,
"samples": [[0.5,0.5,0.00001], [0.25,0.5,0.00001], [-0.25,0.5,0.00001], [1.25,0.5,0.00001]]
}}
},
{
"name": "native_face_bspline_get_tree_trimmed_no_rebuild",
"upstream_ref": "TestFiles/pt_TestFiles/Mouse.smb; source/SMLib/src/SmSurfaceCache.cpp:5459 (GetTree); source/SMLib/src/SmTrimSrfCache.cpp:132; GetTree after a trimmed build (m_bHaveTSurfaceCache) returns the tree without rebuilding",
"inputs": {"native_face": {
"path": {"$upstream_file": "Mouse.smb"},
"surface_type": 15002, "loops": 1, "build": true, "get_tree": 1,
"test_tol": true, "boundary_probes": true,
"samples": [[0.5,0.5,0.00001], [0.25,0.5,0.00001], [-0.25,0.5,0.00001], [1.25,0.5,0.00001]]
}}
},
{
"name": "native_face_bspline_get_tree_builds_unbuilt",
"upstream_ref": "TestFiles/pt_TestFiles/Mouse.smb; source/SMLib/src/SmSurfaceCache.cpp:5459 (GetTree); source/SMLib/src/SmTrimSrfCache.cpp:132; GetTree with no tree builds it through the face",
"inputs": {"native_face": {
"path": {"$upstream_file": "Mouse.smb"},
"surface_type": 15002, "loops": 1, "build": false, "get_tree": 1,
"test_tol": true, "boundary_probes": true,
"samples": [[0.5,0.5,0.00001], [0.25,0.5,0.00001], [-0.25,0.5,0.00001], [1.25,0.5,0.00001]]
}}
},
{
"name": "native_face_holed_plane_get_tree_editing_rebuilds_standalone",
"upstream_ref": "TestFiles/pt_TestFiles/Fillet/ext_w_hole.smb; source/SMLib/src/SmSurfaceCache.cpp:5459 (GetTree); source/SMLib/src/SmTrimSrfCache.cpp:132; GetTree on an editing-enabled planar face rebuilds with the standalone flags",
"inputs": {"native_face": {
"path": {"$upstream_file": "Fillet/ext_w_hole.smb"},
"surface_type": 15011, "loops": 2, "build": true, "get_tree": 1, "editing": true,
"test_tol": true, "boundary_probes": true,
"samples": [[0.5,0.5,0.00001], [0.25,0.5,0.00001], [0.5,0.25,0.00001], [0.1,0.1,0.00001], [-0.25,0.5,0.00001]]
}}
},
{
"name": "native_face_holed_plane_get_tree_after_editing_implants_trims",
"upstream_ref": "TestFiles/pt_TestFiles/Fillet/ext_w_hole.smb; source/SMLib/src/SmSurfaceCache.cpp:5459 (GetTree); source/SMLib/src/SmTrimSrfCache.cpp:132; planar face: editing off before GetTree implants both loops into the existing tree",
"inputs": {"native_face": {
"path": {"$upstream_file": "Fillet/ext_w_hole.smb"},
"surface_type": 15011, "loops": 2, "build": true, "get_tree": 2, "editing": true,
"test_tol": true, "boundary_probes": true,
"samples": [[0.5,0.5,0.00001], [0.25,0.5,0.00001], [0.5,0.25,0.00001], [0.1,0.1,0.00001], [-0.25,0.5,0.00001]]
}}
},
{
"name": "native_face_holed_plane_get_tree_unbuilt_editing",
"upstream_ref": "TestFiles/pt_TestFiles/Fillet/ext_w_hole.smb; source/SMLib/src/SmSurfaceCache.cpp:5459 (GetTree); source/SMLib/src/SmTrimSrfCache.cpp:132; GetTree with no tree on an editing-enabled face builds the standalone tree",
"inputs": {"native_face": {
"path": {"$upstream_file": "Fillet/ext_w_hole.smb"},
"surface_type": 15011, "loops": 2, "build": false, "get_tree": 1, "editing": true,
"test_tol": true, "boundary_probes": true,
"samples": [[0.5,0.5,0.00001], [0.25,0.5,0.00001], [0.5,0.25,0.00001], [0.1,0.1,0.00001], [-0.25,0.5,0.00001]]
}}
}
]
}
@@ -76,7 +76,7 @@ void face_classify(std::vector<double>& out, SmSurfaceCache* cache, const FacePr
}
} // namespace
// Input: surface_type loop_count occurrence build editing modified test_tol
// Input: surface_type loop_count occurrence build_mode editing modified test_tol
// boundary_probes sample_count (u_fraction v_fraction source_tolerance)*.
// Output is documented in the safe FFI module. Every native result is computed here.
SC_EXPORT int32_t sc_surface_trim_srf_cache_faces_eval(
@@ -92,7 +92,8 @@ SC_EXPORT int32_t sc_surface_trim_srf_cache_faces_eval(
input[1] < 0 || input[1] > UINT32_MAX || std::floor(input[1]) != input[1] ||
input[2] < 0 || input[2] > UINT32_MAX || std::floor(input[2]) != input[2] ||
input[8] < 0 || input[8] > 64 || std::floor(input[8]) != input[8]) return SC_ERR_ARG;
for (int i = 3; i <= 7; ++i)
if (input[3] < 0 || input[3] > 5 || std::floor(input[3]) != input[3]) return SC_ERR_ARG;
for (int i = 4; i <= 7; ++i)
if (input[i] != 0.0 && input[i] != 1.0) return SC_ERR_ARG;
const int64_t count = (int64_t)input[8];
if (input_len != 9 + count * 3) return SC_ERR_ARG;
@@ -129,8 +130,16 @@ SC_EXPORT int32_t sc_surface_trim_srf_cache_faces_eval(
face_domain(out, domain);
built.brep->m_bEditingEnabled = input[4] != 0.0 ? TRUE : FALSE;
std::unique_ptr<NativeFaceCache> cache(new (context) NativeFaceCache(*face->GetSurface()));
const bool build_tree = input[3] != 0.0;
const SmStatus build_status = build_tree ? cache->BuildTree(nullptr) : SM_SUCCESS;
// input[3]: 0 none, 1 BuildTree, 2 BuildTree then GetTree, 3 BuildTree, editing
// off, GetTree, 4 GetTree, 5 editing off, GetTree (GetTree's virtual BuildTree).
const int mode = (int)input[3];
const bool build_tree = mode >= 1;
SmStatus build_status = (mode >= 1 && mode <= 3) ? cache->BuildTree(nullptr) : SM_SUCCESS;
if (build_status == SM_SUCCESS && mode >= 2)
{
if (mode == 3 || mode == 5) built.brep->m_bEditingEnabled = FALSE;
build_status = cache->GetTree() != nullptr ? SM_SUCCESS : SM_ERR;
}
cache->m_bFaceWasModified = input[5] != 0.0 ? TRUE : FALSE;
out.insert(out.end(), {build_tree ? (double)build_status : -1.0,
built.brep->m_bEditingEnabled == TRUE ? 1.0 : 0.0,
@@ -0,0 +1,29 @@
Updates: plan/findings/blend.hcr--own-scene-face-trim-cache-deferrals.md
# blend.hcr: face-owned GetTree wired, the three deferrals compare again (2026-10-28)
Unit: `blend.hcr` (caller), `surface.trim_srf_cache` (owner of the build).
Runtime path (confirmed with a temporary trace on `gen0103`): `ComputeEdgeVisibility`
runs with the scene's editing flag still on (upstream sets `m_bEditingEnabled = TRUE`
in `ConstructorInit` and clears it only in `ComputeGlobalVisibility`). The factory build
of each face-owned trimmed cache therefore takes `SmTrimSrfCache::BuildTree`'s
standalone branch (`m_bHaveTSurfaceCache = FALSE`, containment restored), so every later
`SmSurfaceCache::GetTree` re-runs the virtual `BuildTree`. The Rust `SurfaceCache::get_tree`
has no face to give it, which was the stub the cases hit (first in `Hcr::is_point_visible`'s
top-node check).
Fix: `TrimSrfCache::get_tree_with_face` is `GetTree` with the virtual `BuildTree`
dispatched to `build_tree_with_face`. `Hcr::is_point_visible` now builds a face-owned
cache with `smsurf_GetSurfaceCache` through the face and then makes the tracer's three
`GetTree` calls in upstream order (`AddSurfaceToGrid`: `GetTree() == NULL`,
`GetTree()->GetTopNode()`; `ExpandHierarchy`: `GetTree()`), each with the face, before
the tracer borrows the surfaces. `RayTracer::add_surface_with_fetched_cache` and
`Grid::expand_hierarchy_fetched` read that tree without calling `GetTree` again. The
bare `SurfaceCache::get_tree` stub for a face-owned trimmed cache without a face stays as is.
Result: `blend.hcr` 434 cases, 434 pass, 0 deferred, with_values 406 (before 403),
max_rel_err 0. `surface.trim_srf_cache` gains 7 direct native-face `GetTree` cases
(editing on: standalone rebuild; editing turned off between `BuildTree` and `GetTree`:
the rebuild implants the trims into the existing tree, 417 nodes on `Mouse.smb`;
trimmed tree: no rebuild; no tree: `GetTree` builds it), all passing with max_rel_err 0.
@@ -0,0 +1,35 @@
# blend.hcr: the Rust-built scene reaches TrimSrfCache::build_tree_with_face in IsPointVisible (2026-10-28)
Unit: `blend.hcr` (runner `crates/difftest/src/units/blend_hcr.rs`).
Change: the runner's Rust side no longer imports the C++ scene (the `SmHCR` scene copy,
its transform and its `CalculateBoundingBox`). It builds the B-rep block with the Rust
kernel (`.smb` files with the Rust reader), adds the extra wire edge, constructs the HCR
with `Hcr::new` / `Hcr::with_transform` (the Rust `copy_brep`, transform, infinite region
and bounding box) and renumbers the scene into the shim's traversal order.
Effect: 434 cases, 431 pass, 3 deferred (before: 434 pass). The three cases are
`ComputeEdgeVisibility` (op `edge_visibility`) on `.smb` file scenes (`gen0103`,
`gen0253`, `gen0320`; for example `gen0103`: `Mouse.smb`, view
`(0.0936, -0.5806, -0.8407)`, `approx_tol 1e-6`, `ang_tol 0.2`). The C++ side completes
them, as it did before. The Rust side returns
`SolidError::Unported { upstream: "TrimSrfCache::build_tree_with_face for a face-owned
surface", unit: "surface.trim_srf_cache" }` from `SurfaceCache::get_tree`
(`surface/surface_cache/p3.rs`), called by `Hcr::is_point_visible` from
`compute_curve_visibility` from `compute_edge_visibility`. So the cases are deferred,
not failed. Nothing was compared for them (before, each compared 25 values).
Suspected cause: with the imported scene, the ray surfaces of these faces took a path
that never built a face-owned trimmed cache through `get_tree`. The scene the Rust kernel
builds itself (native surfaces and UV trim state from the Rust reader, copied by
`copy_brep`) makes `IsPointVisible` list the faces as face-owned `RaySurface::Face`
entries, which then need the unported face-owned trimmed tree build. This is a gap in
the library, not in the harness. The cases should run again once
`TrimSrfCache::build_tree_with_face` is ported for face-owned surfaces.
Also: `gen0110` (a face over an out-of-box B-spline at 1e-7 scale) fails to build on both
sides. The C++ side now reports its own build failure as `brep_build_status_<status>`
(from the `topology.vertexuse` fixture export, C++ side only), matching the Rust
build's label, instead of the FFI's `fixture rejected`.
PR: difftest: blend.hcr — Rust-side Hcr scene instead of the C++ export (stacked on #1375).
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test:
status = "green"
claimed_by = ""
claimed_at = ""
notes = "OWNED UV CURVE TOLERANCE (PR #1307, 2026-10-27): ARM64 native gate 772 cases, 772 value-bearing passes, 0 failed/deferred/rejected/vacuous, max_rel_err=0; all 762 baseline cases retained. Ten new nonlinear/control/reimplantation cases exercise the real owning-edge tolerance. EdgeuseInput transfers exact geometry and resolved optional tolerance into retention; BuildTree resolves edgeuse ownership, child subdivision reuses retained geometry without cloning, and handle mapping preserves its tolerance. Ownerless curves retain system tolerance. Native guards keep unrelated coordinate/scalar/z limits unchanged; the larger 1e11 coordinate envelope is isolated to ImplantEu. No core CurveCache or numerical tolerance change. Prior history: POLY-EDGE SUBDIVISION (PR #1016, 2026-10-25): 762 cases, 762 passed with values, 0 failed/deferred/rejected/vacuous, max_rel_err=0, rel_tol=1e-9, no absolute floors. All 630 prior cases retain their value coverage; 128 separate fixed-seed cases and 4 tolerance-boundary fixtures now exercise polygon implantation followed by repeated subdivision, including mixed edge-use lists, zero/negative native tolerances, different original/cache surfaces and handle mapping. SubdivideNode reimplants parent polygon edges head first into child 1 then child 2, preserving the parent lists. Endpoints, tolerance and an exact bounded evaluation copy of the original face surface are retained per handle; reimplant after edits to refresh the snapshot. GREEN (PR #594, 2026-10-23): canonical Linux CI run 39936710786 on 3eefd5fc8 (includes upstream/main 38b0745b3) compares 630 cases, 630 passed with values, 0 failed, 0 deferred, max_rel_err=0, rel_tol=1e-9, no absolute floors. The full differential gate passes all 260 units (177 passed, 83 partial, 0 failed); native runner tests pass (253 passed, 0 failed, 32 ignored), including direct C++/Rust equality for built/unbuilt nonplanar face interior/exterior/edge/vertex queries. That run's only failure was a test-only Clippy chunks_exact suggestion, corrected with this promotion; final-head all-seven CI remains the review gate. All 628 original cases and 568 original passes are retained: 60 former runner-only SmBrep::CreateFaceFromSurface deferrals (4 file fixtures, 56 generated) now compare, plus 2 new saddle boundary fixtures. OpIn::Face independently builds real topology through landed brep p8 and uses the existing BrepCacheFace; build=false stays lazy, statuses/object types/owner tolerances are preserved. No production kernel, shared native_face helper, C++/FFI, seeds, guards or tolerances changed; no primitive/sweep parity improvement is claimed. No local native tests, baseline repeats or C++ builds were run. Prior #533/#537 already supplied the shared trimmed-cache machinery and 568 baseline passes, including seven real holed/nonplanar face probes; those gains are not attributed to this follow-up. Earlier notes' construction/classifier gaps are superseded by the landed adapters and this measured evidence. 11 fns; largest: SmTrimSrfCache::ImplantEdgeuse, SmTrimSrfCache::BuildTree, SmTrimSrfCache::ImplantVertexuse, SmTrimSrfCache::ImplantPolyEdge; cycle back-edges: intersect.point_class(7), query.poly(5), surface.surface_cache_p3(5), query.face_p2(2), topology.face_p2(2), topology.face_p4(2), topology.loopuse(2), topology.vertexuse(2), query.tess(1), surface.surface_p1(1), surface.surface_p3(1), topology.edgeuse_p1(1), topology.face_p3(1), topology.vertex(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit."
notes = "FACE-OWNED GetTree (wire: face-owned tree build, 2026-10-28): TrimSrfCache::get_tree_with_face is SmSurfaceCache::GetTree with the virtual BuildTree dispatched to build_tree_with_face; blend.hcr IsPointVisible uses it (its three deferrals compare again). 7 new direct native-face cases (build_mode 2..5 in the faces shim: GetTree after an editing-on build rebuilds standalone; editing turned off before GetTree implants the trims into the existing tree; trimmed tree is not rebuilt; GetTree builds a missing tree), all pass with values, max_rel_err=0. SurfaceCache::get_tree without a face keeps its Unported stub for a face-owned trimmed cache. Earlier: OWNED UV CURVE TOLERANCE (PR #1307, 2026-10-27): ARM64 native gate 772 cases, 772 value-bearing passes, 0 failed/deferred/rejected/vacuous, max_rel_err=0; all 762 baseline cases retained. Ten new nonlinear/control/reimplantation cases exercise the real owning-edge tolerance. EdgeuseInput transfers exact geometry and resolved optional tolerance into retention; BuildTree resolves edgeuse ownership, child subdivision reuses retained geometry without cloning, and handle mapping preserves its tolerance. Ownerless curves retain system tolerance. Native guards keep unrelated coordinate/scalar/z limits unchanged; the larger 1e11 coordinate envelope is isolated to ImplantEu. No core CurveCache or numerical tolerance change. Prior history: POLY-EDGE SUBDIVISION (PR #1016, 2026-10-25): 762 cases, 762 passed with values, 0 failed/deferred/rejected/vacuous, max_rel_err=0, rel_tol=1e-9, no absolute floors. All 630 prior cases retain their value coverage; 128 separate fixed-seed cases and 4 tolerance-boundary fixtures now exercise polygon implantation followed by repeated subdivision, including mixed edge-use lists, zero/negative native tolerances, different original/cache surfaces and handle mapping. SubdivideNode reimplants parent polygon edges head first into child 1 then child 2, preserving the parent lists. Endpoints, tolerance and an exact bounded evaluation copy of the original face surface are retained per handle; reimplant after edits to refresh the snapshot. GREEN (PR #594, 2026-10-23): canonical Linux CI run 39936710786 on 3eefd5fc8 (includes upstream/main 38b0745b3) compares 630 cases, 630 passed with values, 0 failed, 0 deferred, max_rel_err=0, rel_tol=1e-9, no absolute floors. The full differential gate passes all 260 units (177 passed, 83 partial, 0 failed); native runner tests pass (253 passed, 0 failed, 32 ignored), including direct C++/Rust equality for built/unbuilt nonplanar face interior/exterior/edge/vertex queries. That run's only failure was a test-only Clippy chunks_exact suggestion, corrected with this promotion; final-head all-seven CI remains the review gate. All 628 original cases and 568 original passes are retained: 60 former runner-only SmBrep::CreateFaceFromSurface deferrals (4 file fixtures, 56 generated) now compare, plus 2 new saddle boundary fixtures. OpIn::Face independently builds real topology through landed brep p8 and uses the existing BrepCacheFace; build=false stays lazy, statuses/object types/owner tolerances are preserved. No production kernel, shared native_face helper, C++/FFI, seeds, guards or tolerances changed; no primitive/sweep parity improvement is claimed. No local native tests, baseline repeats or C++ builds were run. Prior #533/#537 already supplied the shared trimmed-cache machinery and 568 baseline passes, including seven real holed/nonplanar face probes; those gains are not attributed to this follow-up. Earlier notes' construction/classifier gaps are superseded by the landed adapters and this measured evidence. 11 fns; largest: SmTrimSrfCache::ImplantEdgeuse, SmTrimSrfCache::BuildTree, SmTrimSrfCache::ImplantVertexuse, SmTrimSrfCache::ImplantPolyEdge; cycle back-edges: intersect.point_class(7), query.poly(5), surface.surface_cache_p3(5), query.face_p2(2), topology.face_p2(2), topology.face_p4(2), topology.loopuse(2), topology.vertexuse(2), query.tess(1), surface.surface_p1(1), surface.surface_p3(1), topology.edgeuse_p1(1), topology.face_p3(1), topology.vertex(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit."