wire: intersect.ray_tracer AddPolyBrepToGrid and sm_FindPolyHit over query::poly

RayTracer::add_poly_brep_to_grid notifies the poly B-rep (SM_NO_PRE_EDIT), builds
its SmBrepCache and adds a polygon grid element per surface-tree leaf.
find_poly_hit ports sm_FindPolyHit (sphere reject, LinePlaneIntersect,
PointInPolygon in 3D, hit-any exit, parent walk as a bounded loop in the same
order). FireRay's polygon branch now fills the solution as upstream does; it
only moved the hit parameter before. RayObject::PolyFace names the hit face.

Shim and FFI: add_poly_brep op building real SmPolyBreps; the solution record
names poly faces. Fixtures: upstream smTessTest test 10 (Extrusion.pbp) and 11
built cases (harness/fixturegen/ray_tracer_poly.py); the generator appends
poly B-reps to about a third of the generated cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootarchitect
2026-10-27 21:25:52 +02:00
parent dbdfccb872
commit 2eaea76f2b
7 changed files with 11477 additions and 67 deletions
@@ -11,9 +11,13 @@
//! (`SmLSI3LocalSolver::LocalSolve` on one leaf) and `add_surface`
//! (`SmRayTracer::AddSurfaceToGrid`: builds the surface cache and expands the surface into
//! its subdivision leaves; the record holds each leaf's domain, box, mid-point data, pseudo box
//! and polar box). Both sides produce the flat record documented in the ffi module, reported
//! as the values `rec`. The grid elements of a ray walk are reported as positions among the
//! `add_leaf` leaves (`-1` for a leaf `add_surface` made).
//! and polar box) and `add_poly_brep` (an `SmPolyBrep` of one region and shell with closed
//! loops, then `SmRayTracer::AddPolyBrepToGrid`: builds its `SmBrepCache` and adds a polygon
//! element per surface-tree leaf, whose box and sphere the record holds; rays reaching them run
//! `sm_FindPolyHit`, and a poly face hit is reported by poly B-rep and face position). Both
//! sides produce the flat record documented in the ffi module, reported as the values `rec`.
//! The grid elements of a ray walk are reported as positions among the `add_leaf` leaves (`-1`
//! for an element `add_surface` or `add_poly_brep` made).
//!
//! Deferred cases: every ray or solve that gets past the leaf's pseudo box test reaches
//! `SmSurface::LocalLineIntersect` (`surface.surface_p8`). The generator makes mostly cases whose rays are rejected before
@@ -42,7 +46,13 @@ use solidcore::nurbs::b_spline_surface::BSplineSurface;
use solidcore::nurbs::knots::Side;
use solidcore::nurbs::math::{CPoint, NOW};
use solidcore::nurbs::srf_basic::Surface as NurbSurface;
use solidcore::geom::a_object::CacheStore;
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
use solidcore::geom::object::ObjectRef;
use solidcore::query::poly::{PolyBrep, PolyFaceId, PolyNotifyEnv, PolyObjectKind};
use solidcore::surface::plane::Plane;
use solidcore::topology::topology::Topology;
use std::collections::BTreeMap;
use solidcore::surface::surface::Surface;
use solidcore::SolidError;
use solidcore_ffi::units::intersect_ray_tracer as ffi;
@@ -91,6 +101,16 @@ struct GridIn {
ray_acc: f64,
}
/// A poly face: tolerance (also its edges'), the constructor normal and closed loops of
/// vertex indices, outer first.
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PolyFaceSpec {
tol: f64,
#[serde(default)]
normal: Option<P3>,
loops: Vec<Vec<usize>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
enum OpIn {
@@ -157,6 +177,13 @@ enum OpIn {
p: P3,
v: P3,
},
AddPolyBrep {
tol: f64,
/// `x y z tol` per vertex.
vertices: Vec<[f64; 4]>,
faces: Vec<PolyFaceSpec>,
expansion: f64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -293,6 +320,24 @@ fn ffi_program(i: &Inputs) -> ffi::Program {
},
OpIn::RenderPixel { x, y } => ffi::Op::RenderPixel(*x, *y),
OpIn::RenderRay { p, v } => ffi::Op::RenderRay(*p, *v),
OpIn::AddPolyBrep {
tol,
vertices,
faces,
expansion,
} => ffi::Op::AddPolyBrep(ffi::PolyBrepIn {
tolerance: *tol,
vertices: vertices.clone(),
faces: faces
.iter()
.map(|f| ffi::PolyFaceIn {
tolerance: f.tol,
normal: f.normal,
loops: f.loops.clone(),
})
.collect(),
expansion: *expansion,
}),
})
.collect(),
}
@@ -355,6 +400,60 @@ fn rust_surface(s: &SurfIn) -> Result<Surface, HarnessError> {
}
}
/// The poly B-rep the shim builds: `SmPolyBrep(tol)`, one region and shell, the vertices
/// (`SmPolyVertex` put on the vertex list), then per face `SmPolyFace(tol, .., normal)` and
/// per loop `StartPolyEdgeLoop`, one `AddPolyEdge` per vertex and `FinishPolyEdgeLoop`.
/// Returns it with its faces in creation order.
fn build_poly(
k: &Kernel,
tol: f64,
vertices: &[[f64; 4]],
faces: &[PolyFaceSpec],
) -> solidcore::Result<(PolyBrep, Vec<PolyFaceId>)> {
let bad = |reason| SolidError::InvalidInput {
operation: "difftest::intersect_ray_tracer::build_poly",
reason,
};
// the FFI's bounds, so a fuzzed case cannot make the Rust side allocate without end
if vertices.is_empty()
|| vertices.len() > ffi::MAX_POLY_VERTICES
|| faces.is_empty()
|| faces.len() > ffi::MAX_POLY_FACES
{
return Err(bad("poly B-rep size"));
}
let mut b = PolyBrep::new(k, tol);
let region = b.add_region()?;
let shell = b.add_shell(region)?;
let mut vs = Vec::new();
for v in vertices {
vs.push(b.add_vertex(k, Vec3::new(v[0], v[1], v[2]), v[3])?);
}
let mut out = Vec::new();
for f in faces {
if f.loops.is_empty() || f.loops.len() > ffi::MAX_POLY_LOOPS {
return Err(bad("poly loop count"));
}
let face = b.add_face(k, shell, f.tol, f.normal.map(v3))?;
out.push(face);
for l in &f.loops {
if l.is_empty() || l.len() > ffi::MAX_LOOP_VERTICES {
return Err(bad("poly loop size"));
}
let lp = b.start_poly_edge_loop(face)?;
for (j, &i0) in l.iter().enumerate() {
let i1 = l.get((j + 1) % l.len()).copied().ok_or(bad("loop"))?;
let v0 = *vs.get(i0).ok_or(bad("poly vertex index"))?;
let v1 = *vs.get(i1).ok_or(bad("poly vertex index"))?;
let (p0, p1) = (b.vertex_point(v0)?, b.vertex_point(v1)?);
b.add_poly_edge(k, lp, f.tol, p0, p1, v0)?;
}
b.finish_poly_edge_loop(lp)?;
}
}
Ok((b, out))
}
/// The status of a failed Rust operation, as upstream numbers it.
fn status_of(e: &SolidError) -> f64 {
match e {
@@ -364,9 +463,18 @@ fn status_of(e: &SolidError) -> f64 {
}
}
/// `surface_of` maps the tracer's surface list to the runner's surfaces: `add_surface`
/// lists a surface a second time.
fn put_solution(o: &mut Vec<f64>, s: &Solution<RayObject>, surface_of: &[usize]) {
/// The runner's view of the tracer's lists: `surface_of` maps the tracer's surface list to
/// the runner's surfaces (`add_surface` lists a surface a second time), `poly_of` the
/// tracer's poly B-rep list to the program's `add_poly_brep` ops, whose faces in creation
/// order are `poly_faces`.
struct Lists<'a> {
surface_of: &'a [usize],
poly_of: &'a [usize],
poly_faces: &'a [Vec<PolyFaceId>],
}
fn put_solution(o: &mut Vec<f64>, s: &Solution<RayObject>, lists: &Lists<'_>) {
let surface_of = lists.surface_of;
o.push(f64::from(s.solution_type.code()));
o.push(s.num_variables as f64);
o.push(s.num_objects as f64);
@@ -386,6 +494,16 @@ fn put_solution(o: &mut Vec<f64>, s: &Solution<RayObject>, surface_of: &[usize])
} else {
0.0
});
for j in 0..2 {
let ids = match s.objects.get(j).copied().flatten() {
Some(RayObject::PolyFace(b, f)) => lists.poly_of.get(b).and_then(|&b| {
let faces = lists.poly_faces.get(b)?;
Some([b as f64, faces.iter().position(|&x| x == f)? as f64])
}),
_ => None,
};
o.extend(ids.unwrap_or([-1.0, -1.0]));
}
}
fn put_color(
@@ -417,6 +535,8 @@ struct Stats {
elements_seen: usize,
rejected_leaves: usize,
errors: usize,
/// Rays whose nearest (or any) hit is a poly face.
poly_hits: usize,
}
fn run_rust(k: &Kernel, i: &Inputs, st: &mut Stats) -> solidcore::Result<Vec<f64>> {
@@ -430,6 +550,32 @@ fn run_rust(k: &Kernel, i: &Inputs, st: &mut Stats) -> solidcore::Result<Vec<f64
})
})
.collect::<Result<_, _>>()?;
// the poly B-reps of the `add_poly_brep` ops, built up front so the tracer can hold them
let mut polys: Vec<PolyBrep> = Vec::new();
let mut poly_faces: Vec<Vec<PolyFaceId>> = Vec::new();
for op in &i.ops {
if let OpIn::AddPolyBrep {
tol,
vertices,
faces,
..
} = op
{
if polys.len() >= ffi::MAX_POLY_BREPS {
return Err(SolidError::InvalidInput {
operation: "difftest::intersect_ray_tracer",
reason: "too many poly B-reps",
});
}
let (b, f) = build_poly(k, *tol, vertices, faces)?;
polys.push(b);
poly_faces.push(f);
}
}
let mut poly_slots = polys.iter_mut();
// the program's poly B-rep of each entry of the tracer's poly B-rep list
let mut poly_of: Vec<usize> = Vec::new();
let mut poly_ops = 0_usize;
let grid = Grid::new(&ext3(i.grid.bbox), i.grid.size)?;
let mut tracer_slot = Some(RayTracer::new(grid, i.grid.ray_tol, i.grid.ray_acc));
let mut renderer: Option<RayRenderer> = None;
@@ -526,7 +672,15 @@ fn run_rust(k: &Kernel, i: &Inputs, st: &mut Stats) -> solidcore::Result<Vec<f64
Ok(hit) => {
rec.push(0.0);
rec.push(f64::from(u8::from(hit.hits_something)));
put_solution(&mut rec, &hit.solution, &surface_of);
put_solution(
&mut rec,
&hit.solution,
&Lists {
surface_of: &surface_of,
poly_of: &poly_of,
poly_faces: &poly_faces,
},
);
rec.push(hit.grid_elements.len() as f64);
rec.extend(hit.grid_elements.iter().map(|e| {
leaf_ids.iter().position(|l| l == e).map_or(-1.0, |p| p as f64)
@@ -539,13 +693,25 @@ fn run_rust(k: &Kernel, i: &Inputs, st: &mut Stats) -> solidcore::Result<Vec<f64
st.grid_misses += 1;
}
st.rejected_leaves += usize::from(!hit.hits_something);
st.poly_hits += usize::from(matches!(
hit.solution.objects.first(),
Some(Some(RayObject::PolyFace(..)))
));
}
Err(e @ SolidError::Unported { .. }) => return Err(e),
Err(e) => {
st.errors += 1;
rec.push(status_of(&e));
rec.push(0.0);
put_solution(&mut rec, &Solution::new(), &surface_of);
put_solution(
&mut rec,
&Solution::new(),
&Lists {
surface_of: &surface_of,
poly_of: &poly_of,
poly_faces: &poly_faces,
},
);
rec.push(0.0);
}
}
@@ -589,7 +755,15 @@ fn run_rust(k: &Kernel, i: &Inputs, st: &mut Stats) -> solidcore::Result<Vec<f64
}
rec.push(f64::from(u8::from(r.found_intersection)));
rec.push(f64::from(u8::from(r.needs_more_subdivision)));
put_solution(&mut rec, &r.solution, &surface_of);
put_solution(
&mut rec,
&r.solution,
&Lists {
surface_of: &surface_of,
poly_of: &poly_of,
poly_faces: &poly_faces,
},
);
}
OpIn::AddSurface { surface } => {
let surf = surfaces.get(*surface).ok_or_else(|| bad_index("surface"))?;
@@ -702,6 +876,53 @@ fn run_rust(k: &Kernel, i: &Inputs, st: &mut Stats) -> solidcore::Result<Vec<f64
let res = r.render_ray(k, v3(*p), v3(*v));
put_color(&mut rec, res, r.pixels_shaded())?;
}
OpIn::AddPolyBrep { expansion, .. } => {
let brep = poly_slots.next().ok_or_else(|| bad_index("poly B-rep"))?;
let program_index = poly_ops;
poly_ops += 1;
let tracer = tracer!();
let before = tracer.grid_mut().elements().len();
let mut records: BTreeMap<ObjectKey, Topology> = BTreeMap::new();
let mut attributes = AttributeStore::new();
let mut caches: CacheStore<()> = CacheStore::new();
let mut env = PolyNotifyEnv {
records: &mut records,
attributes: &mut attributes,
caches: &mut caches,
context: None,
this: ObjectRef {
type_code: PolyObjectKind::Brep.type_code(),
key: ObjectKey(1),
},
};
let status =
match tracer.add_poly_brep_to_grid(k, brep, &mut env, *expansion) {
Ok(()) => {
poly_of.push(program_index);
0.0
}
Err(e @ SolidError::Unported { .. }) => return Err(e),
Err(e) => status_of(&e),
};
// the polygon elements it made: count, then per element its box, sphere
// centre and radius
let made: Vec<_> = tracer
.grid_mut()
.elements()
.iter()
.skip(before)
.filter(|e| matches!(e.item(), GridItem::Polygon(_)))
.collect();
rec.push(status);
rec.push(made.len() as f64);
for e in made {
let b = e.bounding_box();
let (b0, b1) = (b.min(), b.max());
rec.extend([b0.x, b0.y, b0.z, b1.x, b1.y, b1.z]);
let c = e.sphere_center();
rec.extend([c.x, c.y, c.z, e.sphere_radius()]);
}
}
}
}
Ok(rec)
@@ -1172,6 +1393,11 @@ fn gen_case(rng: &mut Rng, safe: bool) -> Inputs {
if !safe && rng.chance(0.1) && !surfaces.is_empty() {
ops.push(OpIn::AddSurface { surface: 0 });
}
// poly B-reps and rays at their faces (drawn after everything above, so the ops before
// them do not depend on whether they are made)
if rng.chance(0.35) {
gen_poly_ops(rng, &mut ops, gbox, scale);
}
Inputs {
surfaces,
grid,
@@ -1179,6 +1405,114 @@ fn gen_case(rng: &mut Rng, safe: bool) -> Inputs {
}
}
/// One planar polygon face: its loops as points, outer first (a triangle, a quad, a regular
/// polygon, a non-convex star or a quad with a square hole), on a plane through `c` spanned
/// by `bx`, `by`, half size `r`; wound the other way with `reversed`.
fn gen_polygon(rng: &mut Rng, c: P3, bx: P3, by: P3, r: f64, reversed: bool) -> Vec<Vec<P3>> {
let at = |a: f64, b: f64| add(c, add(scale3(bx, a * r), scale3(by, b * r)));
let ring = |n: usize, radius: &dyn Fn(usize) -> f64| -> Vec<P3> {
(0..n)
.map(|i| {
let t = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
at(radius(i) * t.cos(), radius(i) * t.sin())
})
.collect()
};
let mut loops = match rng.below(5) {
0 => vec![vec![at(-1.0, -1.0), at(1.0, -0.5), at(0.0, 1.0)]],
1 => vec![vec![at(-1.0, -1.0), at(1.0, -1.0), at(1.0, 1.0), at(-1.0, 1.0)]],
2 => vec![ring(rng.range_i64(5, 8) as usize, &|_| 1.0)],
3 => vec![ring(2 * rng.range_i64(3, 6) as usize, &|i| if i % 2 == 0 { 1.0 } else { 0.4 })],
_ => {
let h = rng.uniform(0.2, 0.6);
vec![
vec![at(-1.0, -1.0), at(1.0, -1.0), at(1.0, 1.0), at(-1.0, 1.0)],
vec![at(-h, -h), at(-h, h), at(h, h), at(h, -h)],
]
}
};
if reversed {
for l in &mut loops {
l.reverse();
}
}
loops
}
/// Appends up to two poly B-reps of random planar faces inside `gbox` and rays at them.
fn gen_poly_ops(rng: &mut Rng, ops: &mut Vec<OpIn>, gbox: E6, scale: f64) {
let lo = [gbox[0], gbox[1], gbox[2]];
let hi = [gbox[3], gbox[4], gbox[5]];
let diag = len3(sub(hi, lo));
let mut targets: Vec<(P3, P3)> = Vec::new();
for _ in 0..rng.range_i64(1, 2) {
if ops.len() + 2 > ffi::MAX_OPS {
return;
}
let tol = *rng.choose(&[1e-5, 1e-6, 1e-3]).unwrap_or(&1e-5) * scale;
let mut vertices: Vec<[f64; 4]> = Vec::new();
let mut faces = Vec::new();
for _ in 0..rng.range_i64(1, 12) {
let c = [
rng.uniform(lo[0], hi[0]),
rng.uniform(lo[1], hi[1]),
rng.uniform(lo[2], hi[2]),
];
let [bx, by, bz] = if rng.chance(0.3) {
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
} else {
rotation_basis(rng)
};
let r = diag * rng.uniform(0.02, 0.3);
let reversed = rng.chance(0.3);
let loops = gen_polygon(rng, c, bx, by, r, reversed);
let mut idx = Vec::new();
for l in &loops {
idx.push(
l.iter()
.map(|p| {
vertices.push([p[0], p[1], p[2], tol]);
vertices.len() - 1
})
.collect(),
);
}
let normal = if rng.chance(0.15) {
Some(if reversed { scale3(bz, -1.0) } else { bz })
} else {
None
};
faces.push(PolyFaceSpec { tol, normal, loops: idx });
targets.push((add(c, scale3(add(bx, by), r * rng.uniform(-0.3, 0.3))), bz));
}
ops.push(OpIn::AddPolyBrep {
tol,
vertices,
faces,
expansion: *rng.choose(&[0.0, 1e-3 * scale, 0.05 * diag]).unwrap_or(&0.0),
});
}
while ops.len() < ffi::MAX_OPS && rng.chance(0.85) {
let Some(&(target, normal)) = rng.choose(&targets) else {
return;
};
// through the target: along the face normal (either way) or any direction
let dir = match rng.below(5) {
0 => normal,
1 => scale3(normal, -1.0),
_ => unit_dir(rng),
};
let start = sub(target, scale3(dir, diag * rng.uniform(0.2, 1.5)));
ops.push(OpIn::Fire {
p: start,
v: dir,
max_t: if rng.chance(0.85) { BIG_DOUBLE } else { rng.uniform(0.0, 2.0) * diag },
hit_any: rng.chance(0.4),
skip: None,
});
}
}
/// Mostly cases the Rust side can finish (rays rejected before `LocalLineIntersect`),
/// and some that defer.
fn gen_checked(k: &Kernel, rng: &mut Rng) -> Inputs {
@@ -1256,5 +1590,95 @@ mod tests {
assert!(ok + deferred == MIN_GENERATED, "{ok} {deferred} {st:?}");
assert!(st.walks > 200, "{st:?}");
assert!(st.elements_seen > 100, "{st:?}");
assert!(st.poly_hits > 50, "{st:?}");
}
fn poly_fixture_cases() -> Vec<(String, Inputs)> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../fixtures/intersect.ray_tracer/poly_breps.json");
let text = std::fs::read_to_string(path).unwrap();
let v: Value = serde_json::from_str(&text).unwrap();
v["cases"]
.as_array()
.unwrap()
.iter()
.map(|c| {
let name = c["name"].as_str().unwrap().to_string();
(name, serde_json::from_value(c["inputs"].clone()).unwrap())
})
.collect()
}
/// Every poly fixture gets a ray onto a poly face (passing cases that only miss would
/// leave `sm_FindPolyHit`'s hit path unchecked).
#[test]
fn poly_fixtures_hit_poly_faces() {
let k = Kernel::default();
for (name, i) in poly_fixture_cases() {
let mut st = Stats::default();
run_rust(&k, &i, &mut st).unwrap();
if name != "poly_render_shadows" {
assert!(st.poly_hits > 0, "{name}: {st:?}");
}
}
}
/// In `poly_big_face_over_small_faces` the big face lies on an inner node of the cache's
/// surface tree, so the rays that hit it reach it only by the walk up the parents.
#[test]
fn the_big_poly_face_sits_on_an_inner_tree_node() {
use solidcore::topology::brep_cache::{
arena_identity, make_brep_cache, CacheObject, CacheSource,
};
let k = Kernel::default();
let (_, i) = poly_fixture_cases()
.into_iter()
.find(|(n, _)| n == "poly_big_face_over_small_faces")
.unwrap();
let Some(OpIn::AddPolyBrep {
tol,
vertices,
faces,
..
}) = i.ops.first()
else {
panic!("first op is the poly B-rep");
};
let (b, ids) = build_poly(&k, *tol, vertices, faces).unwrap();
let cache = make_brep_cache(&k, CacheSource::PolyBrep(&b), &arena_identity).unwrap();
let tree = cache.surface_tree().unwrap();
let holder = tree
.all_tree_nodes()
.into_iter()
.find(|&n| {
tree.node_object_list(n).is_ok_and(|l| {
l.iter().any(|&o| {
tree.list(o).unwrap().object == Some(CacheObject::PolyFace(ids[0]))
})
})
})
.unwrap();
assert!(tree.node(holder).unwrap().child1.is_some());
// and a ray from above hits it first, found through the walk up from its leaf
let (mut b, ids) = build_poly(&k, *tol, vertices, faces).unwrap();
let mut tracer = RayTracer::new(Grid::new(&ext3(i.grid.bbox), i.grid.size).unwrap(), 1e-5, 1e-8);
let mut records: BTreeMap<ObjectKey, Topology> = BTreeMap::new();
let mut attributes = AttributeStore::new();
let mut caches: CacheStore<()> = CacheStore::new();
let mut env = PolyNotifyEnv {
records: &mut records,
attributes: &mut attributes,
caches: &mut caches,
context: None,
this: ObjectRef {
type_code: PolyObjectKind::Brep.type_code(),
key: ObjectKey(1),
},
};
tracer.add_poly_brep_to_grid(&k, &mut b, &mut env, 1e-3).unwrap();
let hit = tracer
.fire_ray(&k, Vec3::new(4.5, 6.5, 9.8), Vec3::new(0.0, 0.0, -1.0), BIG_DOUBLE)
.unwrap();
assert_eq!(hit.solution.objects[0], Some(RayObject::PolyFace(0, ids[0])));
}
}
@@ -6,7 +6,7 @@
//!
//! [`run`] builds real `SmPlane` / `SmBSplineSurface` surfaces, an `SmGrid` and an
//! `SmRayTracer` owning it, applies the [`Op`]s in order and returns one flat record: per
//! `Fire` and `Local` op
//! `Fire`, `Local`, `AddSurface` and `AddPolyBrep` op
//!
//! * `Fire`: `status hits <solution> n_elements ids(n) mins(n) maxs(n)`;
//! * `Local`: `status found needs_subdivision <solution>`;
@@ -14,11 +14,15 @@
//! `u_min v_min u_max v_max`, the box `min(3) max(3)`, the mid-point data `point(3) du(3)
//! dv(3) normal(3)`, the pseudo box `basis(9) interval_min_max(6)` and the polar box `state
//! domain(4) basis(9)` (domain and basis only for a bounded state, else zeros);
//! * `AddPolyBrep`: `status n_elements element(n)`, 10 values per polygon element it made:
//! the box `min(3) max(3)`, the sphere centre (3) and radius;
//!
//! where `<solution>` is `type num_variables num_objects solution_value p[0..10] surface
//! object1 node0` (`surface` the surface index or `-1`, `object1` `-1` for null, `node0` `1`
//! when `m_apNodes[0]` is set). The ids of a `Fire` are positions among the `AddLeaf` leaves, `-1`
//! for an element `AddSurface` made. `SolverTol` and `AddLeaf` add nothing.
//! object1 node0 poly0(2) poly1(2)` (`surface` the surface index or `-1`, `object1` `-1` for
//! null, `node0` `1` when `m_apNodes[0]` is set, `poly0` / `poly1` the poly B-rep and face
//! indices of objects 0 and 1, `-1 -1` when not a poly face). The ids of a `Fire` are positions
//! among the `AddLeaf` leaves, `-1` for an element `AddSurface` or `AddPolyBrep` made.
//! `SolverTol` and `AddLeaf` add nothing.
//!
//! | [`Op`] | upstream |
//! |--------------|-----------------------------------------------------------------------------|
@@ -27,6 +31,7 @@
//! | `Fire` | `SmRayTracer::FireRay` |
//! | `Local` | `SmLSI3LocalSolver::LocalSolve` on a leaf |
//! | `AddSurface` | `SmRayTracer::AddSurfaceToGrid` |
//! | `AddPolyBrep` | `SmPolyBrep` (one region and shell, closed loops) and `SmRayTracer::AddPolyBrepToGrid` |
//!
//! [`run`] accepts only finite inputs of magnitude at most [`MAX_COORD`] (ray parameters and
//! tolerances: any finite value); planes with independent axes and positive scales; Bezier
@@ -35,7 +40,10 @@
//! natural domain with non-inverted bounding boxes (a pseudo box may be inverted) and non-zero polar vectors; ray vectors of `Local`
//! ops that are zero or at least [`MIN_LOCAL_VECTOR`] (the Newton iteration divides by it); at most
//! [`MAX_OPS`] ops, [`MAX_LEAVES`] leaves, [`MAX_SURFACES`] surfaces and [`MAX_POLAR`] polar
//! vectors per leaf.
//! vectors per leaf; at most [`MAX_POLY_BREPS`] poly B-reps of 1..=[`MAX_POLY_VERTICES`]
//! vertices and 1..=[`MAX_POLY_FACES`] faces, each face with 1..=[`MAX_POLY_LOOPS`] loops of
//! 1..=[`MAX_LOOP_VERTICES`] vertices (a face needs an outer loop with a first edge, which
//! `sm_FindPolyHit` dereferences), and vertex indices in range.
use crate::validate::bad;
use crate::{call_with_buffer, FfiError};
@@ -66,6 +74,16 @@ pub const MAX_SURFACES: usize = 4;
pub const MAX_POLAR: usize = 8;
/// Shortest non-zero ray vector of a `Local` op (a zero vector is allowed: `LocalSolve` handles it).
pub const MIN_LOCAL_VECTOR: f64 = 1e-6;
/// Most poly B-reps per program.
pub const MAX_POLY_BREPS: usize = 4;
/// Most vertices per poly B-rep.
pub const MAX_POLY_VERTICES: usize = 1024;
/// Most faces per poly B-rep.
pub const MAX_POLY_FACES: usize = 512;
/// Most loops per poly face.
pub const MAX_POLY_LOOPS: usize = 8;
/// Most vertices per poly loop.
pub const MAX_LOOP_VERTICES: usize = 32;
/// `NL_NOW`: the weight of a non-rational control point.
pub const NOW: f64 = -f64::MAX;
@@ -133,6 +151,31 @@ pub struct LeafIn {
pub elem_box: [f64; 6],
}
/// A poly face: its tolerance (also its edges'), the normal given to its constructor and
/// its closed loops, outer first, as vertex indices.
#[derive(Debug, Clone, PartialEq)]
pub struct PolyFaceIn {
/// `SmPolyFace(dTol, ..)` and the `AddPolyEdge` tolerance of its edges.
pub tolerance: f64,
/// `SmPolyFace(.., pOptNormal)` (`None` = null: the normal is computed).
pub normal: Option<[f64; 3]>,
/// The loops (each closed: one edge per vertex).
pub loops: Vec<Vec<usize>>,
}
/// A poly B-rep of one region and one shell, added to the grid.
#[derive(Debug, Clone, PartialEq)]
pub struct PolyBrepIn {
/// `SmPolyBrep(dTol)`.
pub tolerance: f64,
/// `x y z tol` per vertex (`SmPolyVertex(point, tol)`).
pub vertices: Vec<[f64; 4]>,
/// The faces, in creation order.
pub faces: Vec<PolyFaceIn>,
/// `dExpansionTolerance` of `AddPolyBrepToGrid`.
pub expansion: f64,
}
/// A `FireRay` call.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FireIn {
@@ -211,6 +254,8 @@ pub enum Op {
RenderPixel(usize, usize),
/// `RenderRay(point, vector)` (after `DoRender`): record `status r g b pixels_shaded`.
RenderRay([f64; 3], [f64; 3]),
/// Builds a poly B-rep and runs `AddPolyBrepToGrid` on it.
AddPolyBrep(PolyBrepIn),
}
/// The renderer constructor arguments.
@@ -409,6 +454,7 @@ fn encode(prog: &Program) -> Result<Vec<f64>, FfiError> {
}
e.int(prog.ops.len());
let mut leaves = 0_usize;
let mut poly_breps = 0_usize;
let mut rendered = Render::None;
for op in &prog.ops {
match op {
@@ -589,6 +635,49 @@ fn encode(prog: &Program) -> Result<Vec<f64>, FfiError> {
e.nums(p)?;
e.nums(v)?;
}
Op::AddPolyBrep(b) => {
if poly_breps >= MAX_POLY_BREPS {
return Err(bad("too many poly B-reps"));
}
poly_breps += 1;
let nv = b.vertices.len();
if !(1..=MAX_POLY_VERTICES).contains(&nv) {
return Err(bad("poly vertex count"));
}
if !(1..=MAX_POLY_FACES).contains(&b.faces.len()) {
return Err(bad("poly face count"));
}
e.int(11);
e.any(b.tolerance)?;
e.int(nv);
for v in &b.vertices {
e.nums(&v[..3])?;
e.any(v[3])?;
}
e.int(b.faces.len());
for f in &b.faces {
if !(1..=MAX_POLY_LOOPS).contains(&f.loops.len()) {
return Err(bad("poly loop count"));
}
e.any(f.tolerance)?;
e.flag(f.normal.is_some());
e.nums(&f.normal.unwrap_or([0.0; 3]))?;
e.int(f.loops.len());
for l in &f.loops {
if !(1..=MAX_LOOP_VERTICES).contains(&l.len()) {
return Err(bad("poly loop vertex count"));
}
e.int(l.len());
for &v in l {
if v >= nv {
return Err(bad("poly vertex index"));
}
e.int(v);
}
}
}
e.any(b.expansion)?;
}
}
}
Ok(e.0)
+461 -45
View File
@@ -15,6 +15,7 @@
//! | [`RayTracer::fire_ray`], [`RayHit`] | `SmRayTracer::FireRay` |
//! | [`RayTracer::add_surface_to_grid`], [`RayTracer::add_surface_with_bbox`] | `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` |
//! | `Drop` (implicit) | `~SmRayRenderer` |
//! | [`RayRenderer::do_render`], [`RayRenderer::render_pixel`], [`RayRenderer::render_ray`] | `SmRayRenderer::DoRender`, `RenderPixel`, `RenderRay` |
@@ -59,25 +60,41 @@
//! on; see the status deviation of `geom::grid` (a failed `Evaluate` is retried
//! without `non_zero_tangents`, a failed `EvaluateNormal` keeps the cross product,
//! [`SolidError::Unported`] is always propagated).
//! - **Polygon elements.** `sm_FindPolyHit` walks the `SmBrepCache` tree of a
//! polygon grid element and tests `SmPolyFace`s: neither exists yet, so a polygon
//! element in the ray's path is [`SolidError::Unported`], as is
//! [`RayTracer::add_poly_brep_to_grid`], so the `SmPolyBrep` list and the
//! per-context mark locks of `FireRay` (nothing can put a poly B-rep in the list)
//! are not ported; `GetMemoryUsed`, `Dump` and the drawing blocks are not reached
//! by the upstream tests and not ported either.
//! - **Poly B-reps.** The tracer holds each poly B-rep it was given mutably
//! (`SmPolyFace::GetNormal` and `PointInPolygon` cache through `const_cast`
//! upstream) together with the `SmBrepCache` that `AddPolyBrepToGrid` builds; the
//! cache is owned by the tracer rather than the cache manager (the `SM_NO_PRE_EDIT`
//! notification just dropped any cache the poly B-rep had, so `GetOrCreateObjectCache`
//! always makes a new one). A polygon grid element holds its tree node, and the
//! tracer maps the element to its poly B-rep; a [`RayObject::PolyFace`] names the
//! poly B-rep by its index in the tracer's list. Upstream returns from
//! `AddPolyBrepToGrid` with elements already in the grid when `AddElement` fails;
//! the poly B-rep is then not listed, here as upstream, and a later ray reaching such
//! an element is [`SolidError::InvalidInput`].
//! - **Marks.** `FireRay` takes a new `SM_MT_MARK` on the context of every poly B-rep
//! (`SmNewMarkAndLock`, unlocked on return) and `sm_FindPolyHit` marks each poly face
//! it tests with it; here each ray keeps the set of poly faces it has tested. (The
//! loop meant to skip poly B-reps sharing a context `continue`s only its inner loop;
//! a second new mark on the same context changes no face test.)
//! - **`sm_FindPolyHit` recursion** on the parent node, after the node's own faces,
//! is a loop up the parent links in the same order, bounded by the tree's node count
//! (a longer chain is a cycle: [`SolidError::InvalidInput`]).
//! - `GetMemoryUsed`, `Dump` and the drawing blocks are not reached by the upstream
//! tests and not ported.
//! - **Renderer.** [`RayRenderer`] owns its tracer and light sources by value.
//! `DoRender` returns the pixel colours (upstream keeps one row in a local and prints
//! a progress line per row with `smos_WriteBuffer`, which is dropped); the colours
//! upstream leaves unset before `DoRender` are black; the hit node `RenderRay`
//! passes as the skip element is `m_apNodes[0]`, which `FireRay` always clears, so
//! no element is skipped; unsigned products wrap as upstream's `ULONG`.
//! - **Unported callees.** `SmFace::PointClassify` (`query.face_p2`) and the poly
//! types return [`SolidError::Unported`] (see the `UNPORTED` markers). The surface
//! cache and its tree (`smsurf_GetSurfaceCache`, `GetTree`) are ported.
//! - **Unported callees.** `SmFace::PointClassify` goes through the caller's
//! [`RayFaceClassifier`]; a face-owned surface traced without one is
//! [`SolidError::Unported`] (`query.face_p2`, a dispatch gap). The surface cache and
//! its tree (`smsurf_GetSurfaceCache`, `GetTree`), the poly types and the poly B-rep
//! cache are ported.
//! - `SM_ERR` results the upstream ignores or `SER`s keep their behaviour; the
//! `SM_DEBUG_CODE` blocks are not ported. No recursion depends on input here
//! (`sm_FindPolyHit`, which recurses up a tree, is unported).
//! (`sm_FindPolyHit`'s is the bounded loop above).
use crate::curve::curve::p2::evaluate_geometric;
use crate::curve::curve::Curve;
@@ -88,25 +105,31 @@ use crate::geom::extent1d::Extent1d;
use crate::geom::extent2d::Extent2d;
use crate::geom::extent3d::Extent3d;
use crate::geom::geom_utility::p1::{
circle_coplanar_circle_intersect, line_closest_point, line_coplanar_circle_intersect,
line_plane_intersect_uv, CircleCircle,
self as gu, circle_coplanar_circle_intersect, line_closest_point,
line_coplanar_circle_intersect, line_plane_intersect_uv, CircleCircle,
};
use crate::geom::grid::{
ElementId, Grid, GridItem, HierarchyElement, HierarchyId, SurfaceGridElement,
ElementId, ElementSource, Grid, GridItem, HierarchyElement, HierarchyId, SurfaceGridElement,
};
use crate::geom::object::ObjectRef;
use crate::geom::object::{NotifyOperation, ObjectRef};
use crate::geom::solution_array::{ObjectKind, Solution, SolutionObject, SolutionType};
use crate::geom::tree::NodeId;
use crate::geom::tree::{NodeId, Tree};
use crate::geom::vector2d::{Point2, Vec2};
use crate::geom::vector3d::{Point3, Vec3};
use crate::kernel::Kernel;
use crate::math::{BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQ, PI};
use crate::nurbs::knots::Side;
use crate::query::poly::{PolyBrep, PolyFaceId, PolyNotifyEnv};
use crate::surface::surface::p1::evaluate_1st_derivatives;
use crate::surface::surface::p2::surface_cache;
use crate::surface::surface::Surface;
use crate::surface::surface_cache::p1::SurfaceCache;
use crate::topology::brep_cache::{
arena_identity, make_brep_cache, BrepCache, CacheObject, CacheSource,
};
use crate::topology::topology::TopologyRecords;
use crate::topology::{EdgeId, FaceId, VertexId};
use std::collections::{BTreeMap, BTreeSet};
/// A light source (`SmLightSource`): white light from a point or in a direction.
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -139,6 +162,9 @@ pub enum RayObject {
Edge(EdgeId),
/// The vertex of the hit point (`SmVertex`).
Vertex(VertexId),
/// A poly face that was hit (`SmPolyFace`): the poly B-rep's index in the
/// tracer's poly B-rep list and the face in it.
PolyFace(usize, PolyFaceId),
}
impl SolutionObject for RayObject {
@@ -148,10 +174,20 @@ impl SolutionObject for RayObject {
Self::Face(_) => ObjectKind::Face,
Self::Edge(_) => ObjectKind::Edge,
Self::Vertex(_) => ObjectKind::Vertex,
Self::PolyFace(..) => ObjectKind::PolyFace,
}
}
}
/// A poly B-rep in the tracer (`m_sPolyBreps`) with the `SmBrepCache` that
/// `AddPolyBrepToGrid` built for it: its polygon grid elements are leaves of the
/// cache's surface tree.
#[derive(Debug)]
struct TracedPolyBrep<'s> {
brep: &'s mut PolyBrep,
cache: BrepCache,
}
/// The outputs of `SmSurface::EvaluateNormalSection`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NormalSection {
@@ -831,6 +867,11 @@ pub struct RayTracer<'s> {
solver: Lsi3LocalSolver,
/// `m_sSurfaces`: the surfaces added to the grid.
surfaces: Vec<&'s Surface>,
/// `m_sPolyBreps`: the poly B-reps added to the grid, with their caches.
poly_breps: Vec<TracedPolyBrep<'s>>,
/// The poly B-rep (index in `poly_breps`) whose cache tree holds the node of
/// each polygon grid element (upstream reaches the tree through the node).
poly_elements: BTreeMap<ElementId, usize>,
/// `m_pSkipElement`: an element `FireRay` does not test.
pub skip_element: Option<ElementId>,
/// How a face-owned surface's points are classified against its face.
@@ -863,6 +904,8 @@ impl<'s> RayTracer<'s> {
grid,
solver,
surfaces: Vec::new(),
poly_breps: Vec::new(),
poly_elements: BTreeMap::new(),
faces: None,
skip_element: None,
hit_any_thing: false,
@@ -922,15 +965,29 @@ impl<'s> RayTracer<'s> {
&self.surfaces
}
/// The number of poly B-reps added so far (`m_sPolyBreps.GetSize()`).
#[must_use]
pub fn poly_brep_count(&self) -> usize {
self.poly_breps.len()
}
/// The poly B-rep at `index` in the tracer's poly B-rep list (`m_sPolyBreps`),
/// the index a [`RayObject::PolyFace`] holds.
#[must_use]
pub fn poly_brep(&self, index: usize) -> Option<&PolyBrep> {
self.poly_breps.get(index).map(|p| &*p.brep)
}
/// Intersects a ray with the geometry in the grid (`FireRay`): the nearest hit
/// (any hit when [`RayTracer::hit_any_thing`]) with ray parameter in
/// `[-EFF_ZERO, dMaxRayParameter)`; use `BIG_DOUBLE` for an infinite ray.
///
/// # Errors
///
/// Those of the grid walk; [`SolidError::Unported`] from the solver or a polygon
/// element; [`SolidError::InvalidInput`] for a grid element that is neither a
/// surface nor a polygon element or whose surface is not registered.
/// Those of the grid walk and of the poly face tests; [`SolidError::Unported`] from
/// the solver; [`SolidError::InvalidInput`] for a grid element that is neither a
/// surface nor a polygon element, whose surface is not registered or whose poly
/// B-rep is not listed.
pub fn fire_ray(
&mut self,
k: &Kernel,
@@ -948,7 +1005,9 @@ impl<'s> RayTracer<'s> {
max_distances: Vec::new(),
};
// (The per-context mark locks of the poly B-reps: none can be added.)
// Use pPolyBrep->Context->Mark as a tag for new ray-firing: one new mark per
// ray for every poly face (the faces this ray has tested)
let mut marked: BTreeSet<(usize, PolyFaceId)> = BTreeSet::new();
// Init Grid scratch ray walking data with first Voxel/Ray intersection
let hits_nothing = self.grid.setup_3ddda(ray_point, ray_vector)?;
@@ -995,10 +1054,62 @@ impl<'s> RayTracer<'s> {
// handle PolyBrep Faces
let leaf = match item {
GridItem::Polygon(node) => {
// get SmGridElement's SmTreeNode - PolyBreps store SmTreeNodes in
// SmGridElements
let index =
*self
.poly_elements
.get(&id)
.ok_or(SolidError::InvalidInput {
operation: OP,
reason: "polygon grid element of no listed poly B-rep",
})?;
let traced =
self.poly_breps
.get_mut(index)
.ok_or(SolidError::InvalidInput {
operation: OP,
reason: "polygon grid element of no listed poly B-rep",
})?;
let tree = traced
.cache
.surface_tree()
.ok_or(SolidError::null_pointer(OP))?;
// Handle polygon ray firing
let hit = find_poly_hit(node, ray_point, ray_vector, hit_t)?;
if let Some(hit) = hit {
hit_t = hit;
let hit = find_poly_hit(
k,
tree,
traced.brep,
node,
PolyRay {
point: ray_point,
vector: ray_vector,
hit_any_thing: self.hit_any_thing,
},
hit_t,
&mut |face| marked.insert((index, face)),
)?;
if let Some((t, hit_face)) = hit {
hit_t = t;
let hit_pnt = ray_point + ray_vector * hit_t;
let (hit_normal, _) =
traced.brep.face_normal(k, hit_face, false, true)?;
let out = &mut result.solution;
out.solution_type = SolutionType::SingleValue;
out.num_variables = 1;
out.start.parameters[0] = hit_t; // ray parameter
out.start.parameters[1] = BIG_DOUBLE;
out.start.parameters[2] = BIG_DOUBLE;
out.start.parameters[3] = hit_pnt.x; // ray/face intersection point
out.start.parameters[4] = hit_pnt.y;
out.start.parameters[5] = hit_pnt.z;
out.start.parameters[6] = hit_normal.x; // face normal at xsect
out.start.parameters[7] = hit_normal.y;
out.start.parameters[8] = hit_normal.z;
out.num_objects = 1;
out.objects[0] = Some(RayObject::PolyFace(index, hit_face));
out.objects[1] = Some(RayObject::PolyFace(index, hit_face));
result.hits_something = true;
if self.hit_any_thing {
break;
@@ -1140,19 +1251,66 @@ impl<'s> RayTracer<'s> {
self.grid.expand_hierarchy(k, id, &mut cache)
}
/// Embeds the faces of a poly B-rep in the grid (`AddPolyBrepToGrid`).
/// 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
/// polygon element over the leaf's box expanded by `expansion_tolerance`. The
/// poly B-rep joins the tracer's poly B-rep list, so it stays borrowed.
///
/// # Errors
///
/// [`SolidError::Unported`]: `SmPolyBrep` and its `SmBrepCache` are not ported
/// (`query.poly`), so `P` stands for the poly B-rep type to come.
pub fn add_poly_brep_to_grid<P: ?Sized>(
/// Those of [`PolyBrep::notify`], of the cache build and of
/// [`Grid::add_element`]; a null pointer when the cache has no surface tree.
pub fn add_poly_brep_to_grid<R: TopologyRecords + ?Sized, T>(
&mut self,
_poly_brep: &P,
_expansion_tolerance: f64,
k: &Kernel,
poly_brep: &'s mut PolyBrep,
env: &mut PolyNotifyEnv<'_, R, T>,
expansion_tolerance: f64,
) -> Result<()> {
// UNPORTED(query.poly): SmPolyBrep::Notify
Err(SolidError::unported("SmPolyBrep::Notify", "query.poly"))
const OP: &str = "intersect::ray_tracer::RayTracer::add_poly_brep_to_grid";
// warn PolyBrep its about to be modifided
let this = Some(env.this);
poly_brep.notify(env, NotifyOperation::PreEdit, this, None, None)?;
// Compute a Brep Cache - a spatial decomposition of the vertices, edges and
// faces contained in 3 SmTree objects used as a VertexTree, an EdgeTree, and a
// FaceTree (the notification dropped any cache the poly B-rep had)
let cache = make_brep_cache(k, CacheSource::PolyBrep(&*poly_brep), &arena_identity)?;
// get all Surface Tree Nodes
let tree = cache.surface_tree().ok_or(SolidError::null_pointer(OP))?;
let index = self.poly_breps.len();
// For every Surface Tree Node
for node_id in tree.all_tree_nodes() {
let node = tree.node(node_id)?;
// skip non-leaf nodes
if node.child1.is_some() {
continue;
}
// Get PNode's boundingBox increased by Tol
let mut bbox = node.bbox;
bbox.expand_absolute(expansion_tolerance);
// Add leaf node to Grid: an SmGridElement holding the node, listed in every
// voxel that its box meets
let id = self
.grid
.add_element(ElementSource::PolyBrep(node_id), &bbox)?;
self.poly_elements.insert(id, index);
} // end iter every Surface Tree Node
// Add the PolyBrep to the SmRayTracer PolyBrep List
self.poly_breps.push(TracedPolyBrep {
brep: poly_brep,
cache,
});
// all done
Ok(())
}
/// `AddSurfaceToGrid` up to the expansion, from the point where the top node of
@@ -1430,21 +1588,105 @@ impl<'s> RayRenderer<'s> {
}
}
/// `sm_FindPolyHit`: the nearest hit of the ray on the poly faces of a polygon element's
/// tree node and its parents, with parameter below `hit_t`: its parameter.
/// The ray of [`find_poly_hit`].
#[derive(Debug, Clone, Copy)]
struct PolyRay {
/// `crRayPoint`: the ray's start point.
point: Point3,
/// `crRayVector`: the ray's unit direction.
vector: Vec3,
/// `bHitAnyThing`: stop at the first hit.
hit_any_thing: bool,
}
/// `sm_FindPolyHit`: intersects the ray with the poly faces of `node` (a node of the
/// `SmBrepCache` surface tree `tree` of `brep`) and of its parents. A face hit with ray
/// parameter in `(-EFF_ZERO, hit_t)` lowers `hit_t`; the last such hit is returned
/// with its face (with `hit_any_thing`, the first). `mark` marks a face for this ray
/// and is false when the ray has tested the face before (`IsMarked` / `Mark`), which
/// then is skipped.
///
/// Upstream recurses on the parent node after the node's own faces; this walks up the
/// parents in a loop in the same order, bounded by the tree's node count.
fn find_poly_hit(
_node: NodeId,
_ray_point: Point3,
_ray_vector: Vec3,
_hit_t: f64,
) -> Result<Option<f64>> {
// The node's tree is the SmBrepCache surface tree of a poly B-rep and its objects
// are SmPolyFaces (SmPolyFace::ComputeSphereBound, GetNormal, PointInPolygon).
// UNPORTED(query.poly): SmPolyFace::ComputeSphereBound
Err(SolidError::unported(
"SmPolyFace::ComputeSphereBound",
"query.poly",
))
k: &Kernel,
tree: &Tree<CacheObject>,
brep: &mut PolyBrep,
node: NodeId,
ray: PolyRay,
mut hit_t: f64,
mark: &mut dyn FnMut(PolyFaceId) -> bool,
) -> Result<Option<(f64, PolyFaceId)>> {
const OP: &str = "intersect::ray_tracer::find_poly_hit";
let mut hit_face = None;
let mut current = Some(node);
let max_depth = tree.all_tree_nodes().len();
let mut depth = 0usize;
while let Some(node) = current {
// (a parent chain longer than the tree has nodes is a cycle)
depth += 1;
if depth > max_depth {
return Err(SolidError::InvalidInput {
operation: OP,
reason: "tree parent links form a cycle",
});
}
// for every object in pNode
for list in tree.node_object_list(node)? {
let Some(CacheObject::PolyFace(poly_face)) = tree.list(list)?.object else {
return Err(SolidError::null_pointer(OP));
};
// skip already processed PolyFaces (and mark this one)
if !mark(poly_face) {
continue; // This face has been processed before
}
// get PolyFace bounding sphere/Ray Distance
let (sphere_center, sphere_radius) =
brep.face_compute_sphere_bound(poly_face, false)?;
let vec_to_center = sphere_center - ray.point;
let center_t = vec_to_center.dot(ray.vector);
let point_on_ray = ray.point + ray.vector * center_t;
let vec_to = point_on_ray - sphere_center;
let dist_sq = vec_to.length_squared();
let sph_rad_sq = sphere_radius * sphere_radius;
// skip PolyFaces whose bounding Sphere does not intersect the ray
if dist_sq > sph_rad_sq {
continue;
}
let (normal, _) = brep.face_normal(k, poly_face, false, true)?;
let poly_loop = brep
.face_outer_poly_loop(poly_face)?
.ok_or(SolidError::null_pointer(OP))?;
let first_edge = brep
.loop_first_poly_edge(poly_loop)?
.ok_or(SolidError::null_pointer(OP))?;
let origin = brep.edge_start_point(first_edge)?;
let Ok(t_param) = gu::line_plane_intersect(ray.point, ray.vector, origin, normal)
else {
continue;
};
let point = ray.point + ray.vector * t_param;
if point.distance_squared(sphere_center) > sph_rad_sq {
continue; // Intersection is outside of sphere radius.
}
let inside = brep.face_point_in_polygon(k, poly_face, point, true)?;
if inside && t_param < hit_t && t_param > -EFF_ZERO {
hit_t = t_param;
hit_face = Some(poly_face);
if ray.hit_any_thing {
return Ok(hit_face.map(|f| (hit_t, f)));
}
}
} // end iter every pNode Object
// continue on the parent node
current = tree.node(node)?.parent;
}
Ok(hit_face.map(|f| (hit_t, f)))
}
#[cfg(test)]
@@ -1459,6 +1701,180 @@ mod tests {
Grid::new(&bbox, [2, 2, 2]).unwrap()
}
/// A poly B-rep of horizontal squares `[2, 8] x [2, 8]` at the heights `zs`, wound
/// counter-clockwise seen from above, and their faces.
fn stacked_squares(k: &Kernel, zs: &[f64]) -> (PolyBrep, Vec<PolyFaceId>) {
let mut b = PolyBrep::new(k, 1.0e-5);
let r = b.add_region().unwrap();
let s = b.add_shell(r).unwrap();
let mut faces = Vec::new();
for &z in zs {
let f = b.add_face(k, s, 1.0e-5, None).unwrap();
let pts = [
Vec3::new(2.0, 2.0, z),
Vec3::new(8.0, 2.0, z),
Vec3::new(8.0, 8.0, z),
Vec3::new(2.0, 8.0, z),
];
let vs: Vec<_> = pts
.iter()
.map(|&p| b.add_vertex(k, p, 1.0e-5).unwrap())
.collect();
let l = b.start_poly_edge_loop(f).unwrap();
for j in 0..4 {
b.add_poly_edge(k, l, 1.0e-5, pts[j], pts[(j + 1) % 4], vs[j])
.unwrap();
}
b.finish_poly_edge_loop(l).unwrap();
faces.push(f);
}
(b, faces)
}
/// Adds `brep` to `tracer` with an empty notification environment.
fn add_poly<'s>(k: &Kernel, tracer: &mut RayTracer<'s>, brep: &'s mut PolyBrep) {
use crate::geom::a_object::CacheStore;
use crate::geom::attribute::{AttributeStore, ObjectKey};
use crate::query::poly::PolyObjectKind;
let mut records: BTreeMap<ObjectKey, crate::topology::topology::Topology> = BTreeMap::new();
let mut attributes = AttributeStore::new();
let mut caches: CacheStore<()> = CacheStore::new();
let mut env = PolyNotifyEnv {
records: &mut records,
attributes: &mut attributes,
caches: &mut caches,
context: None,
this: ObjectRef {
type_code: PolyObjectKind::Brep.type_code(),
key: ObjectKey(1),
},
};
tracer
.add_poly_brep_to_grid(k, brep, &mut env, 1.0e-3)
.unwrap();
}
#[test]
fn a_poly_brep_adds_one_polygon_element_per_surface_tree_leaf() {
let k = Kernel::default();
let (mut b, _) = stacked_squares(&k, &[2.0, 6.0]);
let mut tracer = RayTracer::new(grid(), 1.0e-6, 1.0e-9);
add_poly(&k, &mut tracer, &mut b);
assert_eq!(tracer.poly_brep_count(), 1);
let elements = tracer.poly_elements.len();
assert!(elements >= 1);
let tree = tracer.poly_breps[0].cache.surface_tree().unwrap();
let leaves = tree
.all_tree_nodes()
.into_iter()
.filter(|&n| tree.node(n).unwrap().child1.is_none())
.count();
assert_eq!(elements, leaves);
}
#[test]
fn a_ray_hits_the_nearest_poly_face() {
let k = Kernel::default();
let (mut b, faces) = stacked_squares(&k, &[2.0, 6.0]);
let mut tracer = RayTracer::new(grid(), 1.0e-6, 1.0e-9);
add_poly(&k, &mut tracer, &mut b);
let hit = tracer
.fire_ray(
&k,
Vec3::new(5.0, 5.0, 9.5),
Vec3::new(0.0, 0.0, -1.0),
BIG_DOUBLE,
)
.unwrap();
assert!(hit.hits_something);
let s = &hit.solution;
assert_eq!(s.solution_type, SolutionType::SingleValue);
assert_eq!(s.num_variables, 1);
assert_eq!(s.num_objects, 1);
assert_eq!(s.objects[0], Some(RayObject::PolyFace(0, faces[1])));
assert_eq!(s.objects[1], Some(RayObject::PolyFace(0, faces[1])));
assert_eq!(
s.start.parameters[..9],
[3.5, BIG_DOUBLE, BIG_DOUBLE, 5.0, 5.0, 6.0, 0.0, 0.0, 1.0]
);
}
#[test]
fn a_poly_face_behind_the_ray_start_is_not_hit() {
let k = Kernel::default();
let (mut b, faces) = stacked_squares(&k, &[2.0, 6.0]);
let mut tracer = RayTracer::new(grid(), 1.0e-6, 1.0e-9);
add_poly(&k, &mut tracer, &mut b);
let hit = tracer
.fire_ray(
&k,
Vec3::new(5.0, 5.0, 4.0),
Vec3::new(0.0, 0.0, -1.0),
BIG_DOUBLE,
)
.unwrap();
assert!(hit.hits_something);
assert_eq!(
hit.solution.objects[0],
Some(RayObject::PolyFace(0, faces[0]))
);
assert_eq!(hit.solution.start.parameters[0], 2.0);
}
#[test]
fn rays_outside_or_along_the_poly_faces_hit_nothing() {
let k = Kernel::default();
let (mut b, _) = stacked_squares(&k, &[2.0, 6.0]);
let mut tracer = RayTracer::new(grid(), 1.0e-6, 1.0e-9);
add_poly(&k, &mut tracer, &mut b);
// through the faces' planes outside the squares
let miss = tracer
.fire_ray(
&k,
Vec3::new(9.0, 5.0, 9.5),
Vec3::new(0.0, 0.0, -1.0),
BIG_DOUBLE,
)
.unwrap();
assert!(!miss.hits_something);
// parallel to the faces, between them
let along = tracer
.fire_ray(
&k,
Vec3::new(0.5, 5.0, 4.0),
Vec3::new(1.0, 0.0, 0.0),
BIG_DOUBLE,
)
.unwrap();
assert!(!along.hits_something);
// stopped before the first face
let short = tracer
.fire_ray(&k, Vec3::new(5.0, 5.0, 9.5), Vec3::new(0.0, 0.0, -1.0), 3.0)
.unwrap();
assert!(!short.hits_something);
}
#[test]
fn hit_any_thing_stops_at_a_poly_hit() {
let k = Kernel::default();
let (mut b, faces) = stacked_squares(&k, &[2.0, 6.0]);
let mut tracer = RayTracer::new(grid(), 1.0e-6, 1.0e-9);
tracer.hit_any_thing = true;
add_poly(&k, &mut tracer, &mut b);
let hit = tracer
.fire_ray(
&k,
Vec3::new(5.0, 5.0, 9.5),
Vec3::new(0.0, 0.0, -1.0),
BIG_DOUBLE,
)
.unwrap();
assert!(hit.hits_something);
assert!(faces
.iter()
.any(|&f| hit.solution.objects[0] == Some(RayObject::PolyFace(0, f))));
}
#[test]
fn convert_to_parameter_delta_projects_on_the_tangent() {
let d = convert_to_parameter_delta(Vec3::new(2.0, 0.0, 0.0), Vec3::new(3.0, 4.0, 0.0));
File diff suppressed because it is too large Load Diff
+129 -7
View File
@@ -12,8 +12,12 @@
// from the leaf's domain, box, pseudo box and polar box given as data, computing the mid-point
// data from the real surface exactly as Init does (a case can give leaves the surface's own
// subdivision does not have). ADD_SURFACE runs the real SmRayTracer::AddSurfaceToGrid, which
// builds the surface cache and makes the leaves from its tree. SmGrid's protected members are
// reached by exposing them while SmGrid.h is included.
// builds the surface cache and makes the leaves from its tree. ADD_POLY_BREP builds a real
// SmPolyBrep (one region, one shell, closed loops; the constructors and AddPolyEdge as the
// query.poly shim uses them) and runs the real SmRayTracer::AddPolyBrepToGrid, which builds its
// SmBrepCache and adds a polygon element per surface-tree leaf; FireRay then reaches
// sm_FindPolyHit. SmGrid's and SmPolyBrep's protected members are reached by exposing them while
// their headers are included.
#include "sc_shim.h"
#include <SmAxis2Placement.h>
@@ -43,6 +47,12 @@
#include <SmGrid.h>
#undef protected
#define protected public
#define private public
#include <SmPoly.h>
#undef private
#undef protected
#include <SmRayTracer.h>
#include "sc_sm.h"
@@ -63,6 +73,14 @@ enum : int32_t
OP_DO_RENDER = 8,
OP_RENDER_PIXEL = 9,
OP_RENDER_RAY = 10,
OP_ADD_POLY_BREP = 11,
};
// The poly B-reps of a program and their faces in creation order (the ids of a solution).
struct Polys
{
std::vector<SmPolyBrep*> breps;
std::vector<std::vector<SmPolyFace*>> faces;
};
struct Bad : std::runtime_error
@@ -164,8 +182,10 @@ void put3(std::vector<double>& o, const SmVector3d& v)
}
// The solution fields of FireRay / LocalSolve: type, variable and object counts, solution value,
// the ten start parameters, the surface index and whether object 1 and node 0 are set.
void put_solution(std::vector<double>& o, const SmSolution& s, const std::vector<SmSurface*>& surfs)
// the ten start parameters, the surface index, whether object 1 and node 0 are set, and the poly
// B-rep and face indices of object 0 and of object 1 (-1 -1 when not a poly face of the program).
void put_solution(std::vector<double>& o, const SmSolution& s, const std::vector<SmSurface*>& surfs,
const Polys& polys)
{
o.push_back((double)s.m_eSolutionType);
o.push_back((double)s.m_lNumVariables);
@@ -182,6 +202,77 @@ void put_solution(std::vector<double>& o, const SmSolution& s, const std::vector
o.push_back(surf);
o.push_back(s.m_apObjects[1] == nullptr ? -1.0 : -2.0);
o.push_back(s.m_apNodes[0] == nullptr ? 0.0 : 1.0);
for (int j = 0; j < 2; j++)
{
double brep = -1.0;
double face = -1.0;
for (size_t b = 0; b < polys.faces.size(); b++)
for (size_t f = 0; f < polys.faces[b].size(); f++)
if (s.m_apObjects[j] != nullptr && s.m_apObjects[j] == (SmObject*)polys.faces[b][f])
{
brep = (double)b;
face = (double)f;
}
o.push_back(brep);
o.push_back(face);
}
}
// `tol n_vertices (x y z tol)* n_faces (tol has_normal nx ny nz n_loops (k v0 .. v(k-1))*)*`: one
// region, one shell, the face normal given to the constructor or not, every loop closed (k edges),
// each edge with its face's tolerance.
SmPolyBrep* read_poly_brep(Rd& rd, const SmContext& ctx, std::vector<SmPolyFace*>& faces)
{
double tol = rd.num();
SmPolyBrep* b = new (ctx) SmPolyBrep(tol);
try
{
SmPolyRegion* region = new (b) SmPolyRegion(b);
SmPolyShell* shell = new (b) SmPolyShell(region);
int64_t nv = rd.idx(1, 1024);
std::vector<SmPolyVertex*> vertices;
for (int64_t i = 0; i < nv; ++i)
{
SmVector3d p = rd.vec();
double t = rd.num();
SmPolyVertex* v = new (b) SmPolyVertex(p, t);
b->m_pPolyVertexListHead->PostInsert(v);
vertices.push_back(v);
}
int64_t nf = rd.idx(1, 512);
for (int64_t i = 0; i < nf; ++i)
{
double ftol = rd.num();
bool has_n = rd.flag();
SmVector3d n = rd.vec();
SmPolyFace* f = new (b) SmPolyFace(ftol, NULL, NULL, shell, has_n ? &n : NULL);
faces.push_back(f);
int64_t nl = rd.idx(1, 8);
for (int64_t l = 0; l < nl; ++l)
{
int64_t k = rd.idx(1, 32);
std::vector<SmPolyVertex*> vs;
for (int64_t j = 0; j < k; ++j)
vs.push_back(vertices[(size_t)rd.idx(0, nv - 1)]);
SmPolyLoop* lp = NULL;
f->StartPolyEdgeLoop(lp);
for (int64_t j = 0; j < k; ++j)
{
SmPolyVertex* v0 = vs[(size_t)j];
SmPolyVertex* v1 = vs[(size_t)((j + 1) % k)];
SmPolyEdge* e = NULL;
lp->AddPolyEdge(ftol, v0->GetPoint(), v1->GetPoint(), v0, NULL, b, e);
}
lp->FinishPolyEdgeLoop(b);
}
}
}
catch (...)
{
delete b;
throw;
}
return b;
}
int32_t run(const double* p, int64_t n, std::vector<double>& o)
@@ -192,6 +283,7 @@ int32_t run(const double* p, int64_t n, std::vector<double>& o)
SmRayTracer* rt = nullptr;
SmRayRenderer* renderer = nullptr;
std::vector<SmSurfaceGridElement*> leaves;
Polys polys;
int32_t rc = SC_OK;
try
{
@@ -210,7 +302,7 @@ int32_t run(const double* p, int64_t n, std::vector<double>& o)
int64_t nops = rd.idx(0, 64);
for (int64_t k = 0; k < nops; k++)
{
int op = (int)rd.idx(1, 10);
int op = (int)rd.idx(1, 11);
switch (op)
{
case OP_SOLVER_TOL:
@@ -296,7 +388,7 @@ int32_t run(const double* p, int64_t n, std::vector<double>& o)
SmStatus s = rt->FireRay(rp, rv, maxT, bHit, sol, ge, mn, mx);
o.push_back(st(s));
o.push_back(bHit ? 1.0 : 0.0);
put_solution(o, sol, surfs);
put_solution(o, sol, surfs, polys);
o.push_back((double)ge.GetSize());
for (ULONG i = 0; i < ge.GetSize(); i++)
{
@@ -343,7 +435,7 @@ int32_t run(const double* p, int64_t n, std::vector<double>& o)
o.push_back(st(s));
o.push_back(bFound ? 1.0 : 0.0);
o.push_back(bNeeds ? 1.0 : 0.0);
put_solution(o, sol, surfs);
put_solution(o, sol, surfs, polys);
break;
}
case OP_ADD_SURFACE:
@@ -466,6 +558,34 @@ int32_t run(const double* p, int64_t n, std::vector<double>& o)
o.push_back((double)renderer->GetPixelsShaded());
break;
}
case OP_ADD_POLY_BREP:
{
if (polys.breps.size() >= 4)
throw Bad();
std::vector<SmPolyFace*> faces;
SmPolyBrep* pb = read_poly_brep(rd, sCtx, faces);
polys.breps.push_back(pb);
polys.faces.push_back(faces);
double expansion = rd.num();
SmTArray<void*> before;
g->m_sPolygonElementMgr.GetActiveElements(before);
SmStatus s = rt->AddPolyBrepToGrid(pb, expansion);
SmTArray<void*> after;
g->m_sPolygonElementMgr.GetActiveElements(after);
// status, the number of polygon elements it made, and per element its box, sphere
// centre and radius
o.push_back(st(s));
o.push_back((double)(after.GetSize() - before.GetSize()));
for (ULONG i = before.GetSize(); i < after.GetSize(); i++)
{
SmGridElement* e = (SmGridElement*)after[i];
put3(o, e->m_sBBox.GetMin());
put3(o, e->m_sBBox.GetMax());
put3(o, e->m_sSphereCenter);
o.push_back(e->m_dSphereRadius);
}
break;
}
default:
throw Bad();
}
@@ -481,6 +601,8 @@ int32_t run(const double* p, int64_t n, std::vector<double>& o)
delete rt; // deletes the grid, not the surfaces
for (SmSurface* s : surfs)
delete s;
for (SmPolyBrep* b : polys.breps)
delete b; // and its SmBrepCache
return rc;
}
} // namespace
+317
View File
@@ -0,0 +1,317 @@
#!/usr/bin/env python3
"""Writes fixtures/intersect.ray_tracer/poly_breps.json: rays fired at poly B-reps.
Upstream `tests/prog_test/src/smTessTest.cpp` `my_test_tess` test 10 ("Test ray-firing for
poly brep") reads `pt_TestFiles/Tess/Extrusion.pbp` into an `SmPolyBrep`, makes a 20^3
`SmGrid` over the poly B-rep's box expanded by 1.0, an `SmRayTracer(ctx, grid,
SM_ZONE_TOL_3D, 1.0e-8)`, calls `AddPolyBrepToGrid(pPolyBrep, 0.001)` and fires one ray from
the box centre moved back 15 along the unit (1, 1, 1), with `dMaxRayParameter` 40 and
`m_bHitAnyThing` FALSE, asserting that it hits. Case `tess_test10_extrusion` is that test:
the file's one region and shell, its 296 faces in file order with their stored tolerances
and normals, each face's loops as the start vertices of their edges (`SmPolyEdgeData` holds
the edge's start vertex; a loop's edges are consecutive from its start edge, and a face's
loops from its first loop), and the vertices with their tolerances. The grid box is the box
of the vertex points expanded by 1.0 (upstream's `SmPolyBrep::CalculateBoundingBox` box is
not reproduced; the grid only places the voxels). Further rays of the case: the same ray
with hit-any-thing, the reverse ray, and rays along the axes through the centre.
The other cases are built here to reach the branches of `sm_FindPolyHit` and the polygon
branch of `FireRay`: nearest of stacked faces, a face behind the ray start, misses outside
the polygon and in its plane, a hole, a non-convex notch, coincident faces in two poly
B-reps (the first one tested keeps the hit), a big face over many small ones (objects on
inner tree nodes: the walk up the parents), a poly face against a surface, computed and
given normals, a degenerate face, expansion tolerances, and a render with shadows.
Run from the repo root: `python3 -I harness/fixturegen/ray_tracer_poly.py`.
"""
import json
import math
import os
UPSTREAM = "upstream/TestFiles/pt_TestFiles/Tess/Extrusion.pbp"
OUT = "fixtures/intersect.ray_tracer/poly_breps.json"
SM_ZONE_TOL_3D = 1e-05 # as the existing ray tracer fixtures use it
BIG = 1.0e20 # SM_BIG_DOUBLE
def read_pbp(path):
"""The faces, loops, edges and vertices of an ASCII .pbp file (SmPolyBrepData)."""
with open(path) as f:
lines = [ln.strip() for ln in f]
at = 0
def section(title):
nonlocal at
while not lines[at].startswith("//" + title):
at += 1
at += 1
count = int(lines[at].split()[0])
at += 1
while lines[at].startswith("//"):
at += 1
return count
def nums():
nonlocal at
vals = lines[at].split()
at += 1
return vals
tol = float(next(ln for ln in lines if ln.endswith("PolyBrep Tolerance")).split()[0])
count = section("PolyFace Data")
faces = []
for _ in range(count):
idx, ftol, nloops, first = nums()[:4]
n = [float(x) for x in nums()[:3]]
nums() # attributes
faces.append((float(ftol), int(nloops), int(first), n))
count = section("PolyLoop Data")
loops = []
for _ in range(count):
idx, nedges, start = nums()[:3]
nums()
loops.append((int(nedges), int(start)))
count = section("PolyEdge Data")
edges = []
for _ in range(count):
vals = nums()
edges.append(int(vals[2])) # m_lVertex: the start vertex
nums()
count = section("PolyVertex Data")
vertices = []
for _ in range(count):
vtol = float(nums()[1])
p = [float(x) for x in nums()[:3]]
nums()
vertices.append(p + [vtol])
out_faces = []
for ftol, nloops, first, n in faces:
fl = []
for li in range(first, first + nloops):
nedges, start = loops[li]
fl.append([edges[e] for e in range(start, start + nedges)])
out_faces.append({"tol": ftol, "normal": n, "loops": fl})
return tol, vertices, out_faces
def unit(v):
n = math.sqrt(sum(x * x for x in v))
return [x / n for x in v]
def fire(p, v, max_t=BIG, hit_any=False):
return {"op": "fire", "p": p, "v": v, "max_t": max_t, "hit_any": hit_any, "skip": None}
def poly(vertices, faces, tol=1e-5, expansion=1e-3):
return {"op": "add_poly_brep", "tol": tol, "vertices": vertices, "faces": faces,
"expansion": expansion}
def face(loops, tol=1e-5, normal=None):
f = {"tol": tol, "loops": loops}
if normal is not None:
f["normal"] = normal
return f
def rect(x0, y0, x1, y1, z, tol=1e-5):
"""Counter-clockwise from above."""
return [[x0, y0, z, tol], [x1, y0, z, tol], [x1, y1, z, tol], [x0, y1, z, tol]]
def plane_surface():
return {"kind": "plane", "o": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0], "scale": [1, 1],
"domain": [0, 0, 4, 4]}
def grid(bbox, n=4, ray_tol=SM_ZONE_TOL_3D):
return {"bbox": bbox, "size": [n, n, n], "ray_tol": ray_tol, "ray_acc": 1e-08}
def case(name, ref, ops, bbox, n=4, surfaces=None):
return {"name": name, "upstream_ref": ref,
"inputs": {"surfaces": surfaces or [plane_surface()], "grid": grid(bbox, n),
"ops": ops}}
def tess_test10():
tol, vertices, faces = read_pbp(UPSTREAM)
lo = [min(v[i] for v in vertices) - 1.0 for i in range(3)]
hi = [max(v[i] for v in vertices) + 1.0 for i in range(3)]
c = [0.5 * (lo[i] + hi[i]) for i in range(3)]
d = unit([1.0, 1.0, 1.0])
start = [c[i] - d[i] * 15.0 for i in range(3)]
ops = [poly(vertices, faces, tol=tol, expansion=0.001),
fire(start, d, 40.0),
fire(start, d, 40.0, hit_any=True),
fire([c[i] + d[i] * 15.0 for i in range(3)], [-x for x in d], 40.0)]
for axis in range(3):
for sign in (1.0, -1.0):
v = [0.0, 0.0, 0.0]
v[axis] = sign
p = list(c)
p[axis] -= sign * 30.0
ops.append(fire(p, v))
return case("tess_test10_extrusion",
"tests/prog_test/src/smTessTest.cpp:2716 (test 10: Extrusion.pbp, grid 20^3 over "
"the box expanded by 1.0, SM_ZONE_TOL_3D, 1e-8, AddPolyBrepToGrid 0.001, ray from "
"the centre - 15 (1,1,1)/sqrt 3 to 40; plus hit-any, reverse and axis rays)",
ops, lo + hi, n=20)
def stacked():
v = rect(2, 2, 8, 8, 2.0) + rect(2, 2, 8, 8, 6.0)
ops = [poly(v, [face([[0, 1, 2, 3]]), face([[4, 5, 6, 7]])]),
fire([5, 5, 9.5], [0, 0, -1]), # nearest: z = 6
fire([5, 5, 9.5], [0, 0, -1], hit_any=True),
fire([5, 5, 4.0], [0, 0, -1]), # z = 6 behind the start
fire([5, 5, -0.5], [0, 0, 1]), # from below: z = 2
fire([9.0, 5, 9.5], [0, 0, -1]), # outside the squares
fire([0.5, 5, 4.0], [1, 0, 0]), # parallel, between them
fire([5, 5, 9.5], [0, 0, -1], max_t=3.0), # stops before z = 6
fire([5, 5, 6.0], [0, 0, -1]), # starts on the face (t = 0)
fire([2.0, 2.0, 9.5], [0, 0, -1]), # through a corner vertex
fire([5, 2.0, 9.5], [0, 0, -1]), # through an edge
fire([0.5, 0.5, 9.5], unit([1, 1, -1]))] # oblique
return case("poly_stacked_squares", "sm_FindPolyHit: nearest, behind, miss, in-plane, "
"short ray, on-face start, vertex and edge hits", ops, [0, 0, 0, 10, 10, 10])
def holed():
v = rect(1, 1, 9, 9, 5.0) + [[4, 4, 5.0, 1e-5], [4, 6, 5.0, 1e-5], [6, 6, 5.0, 1e-5],
[6, 4, 5.0, 1e-5]] + rect(1, 1, 9, 9, 1.0)
ops = [poly(v, [face([[0, 1, 2, 3], [4, 5, 6, 7]]), face([[8, 9, 10, 11]])]),
fire([5, 5, 9.5], [0, 0, -1]), # through the hole: z = 1
fire([2, 2, 9.5], [0, 0, -1]), # the holed face
fire([4, 5, 9.5], [0, 0, -1]), # on the hole's edge
fire([5, 5, 9.5], [0, 0, -1], hit_any=True)]
return case("poly_face_with_hole", "PointInPolygon with an inner loop", ops,
[0, 0, 0, 10, 10, 10])
def notch():
# an L: the notch [5, 8] x [5, 8] is outside
pts = [[2, 2], [8, 2], [8, 5], [5, 5], [5, 8], [2, 8]]
v = [[x, y, 4.0, 1e-5] for x, y in pts]
ops = [poly(v, [face([[0, 1, 2, 3, 4, 5]])]),
fire([6.5, 6.5, 9.5], [0, 0, -1]), # in the notch: miss
fire([3.5, 6.5, 9.5], [0, 0, -1]),
fire([6.5, 3.5, 9.5], [0, 0, -1]),
fire([6.5, 6.5, 9.5], unit([-1, -1, -1]))]
return case("poly_nonconvex_notch", "PointInPolygon on a non-convex face", ops,
[0, 0, 0, 10, 10, 10])
def coincident():
a = rect(2, 2, 8, 8, 5.0)
ops = [poly(a, [face([[0, 1, 2, 3]])]),
poly(a, [face([[0, 1, 2, 3]])]),
fire([5, 5, 9.5], [0, 0, -1]),
fire([5, 5, 0.5], [0, 0, 1]),
fire([3, 7, 9.5], unit([0.2, -0.1, -1]))]
return case("poly_coincident_breps", "two poly B-reps with the same face: equal ray "
"parameters, the first tested keeps the hit (strict <)", ops, [0, 0, 0, 10, 10, 10])
def big_over_small():
v = rect(0.5, 0.5, 9.5, 9.5, 9.0)
faces = [face([[0, 1, 2, 3]])]
for i in range(8):
for j in range(8):
base = len(v)
x, y = 1 + i, 1 + j
z = 1.0 + 0.25 * ((i * 8 + j) % 7)
v += rect(x, y, x + 0.9, y + 0.9, z)
faces.append(face([[base, base + 1, base + 2, base + 3]]))
ops = [poly(v, faces)]
for x, y in [(1.45, 1.45), (4.5, 6.5), (8.95, 8.95), (5.0, 5.0), (3.2, 8.4), (7.99, 1.01)]:
ops.append(fire([x, y, 0.2], [0, 0, 1]))
ops.append(fire([x, y, 9.8], [0, 0, -1]))
ops.append(fire([0.2, 0.3, 0.2], unit([1, 1, 0.3])))
ops.append(fire([9.8, 9.7, 9.5], unit([-1, -0.9, -0.6]), hit_any=True))
return case("poly_big_face_over_small_faces", "a face spanning the tree on an inner node: "
"sm_FindPolyHit walks up the parents", ops, [0, 0, 0, 10, 10, 10], n=6)
def with_surface():
# the plane z = 0 over [0, 4]^2 and a poly square at z = 1 and z = -1
v = rect(0.5, 0.5, 3.5, 3.5, 1.0) + rect(0.5, 0.5, 3.5, 3.5, -1.0)
ops = [{"op": "add_surface", "surface": 0},
poly(v, [face([[0, 1, 2, 3]]), face([[4, 5, 6, 7]])]),
fire([2, 2, 3], [0, 0, -1]),
fire([2, 2, 0.5], [0, 0, -1]),
fire([0.2, 0.2, 3], [0, 0, -1]),
fire([2, 2, -3], [0, 0, 1])]
return case("poly_and_surface", "a poly face and a surface leaf in one grid", ops,
[-1, -1, -2, 5, 5, 2])
def normals():
v = rect(2, 2, 8, 8, 3.0) + rect(2, 2, 8, 8, 7.0)
# the first wound clockwise (normal -z), the second given a tilted normal
ops = [poly(v, [face([[3, 2, 1, 0]]), face([[4, 5, 6, 7]], normal=unit([0, 0.6, 0.8]))]),
fire([5, 5, 9.5], [0, 0, -1]),
fire([5, 5, 5.0], [0, 0, -1]),
fire([5, 5, 0.5], [0, 0, 1])]
return case("poly_face_normals", "computed (clockwise) and given face normals in the "
"solution and in the plane intersection", ops, [0, 0, 0, 10, 10, 10])
def degenerate():
v = [[2, 2, 5, 1e-5], [5, 5, 5, 1e-5], [8, 8, 5, 1e-5], [2, 2, 2, 1e-5], [8, 2, 2, 1e-5],
[5, 8, 2, 1e-5]]
ops = [poly(v, [face([[0, 1, 2]]), face([[3, 4, 5]])]),
fire([5, 5, 9.5], [0, 0, -1]),
fire([5, 4, 9.5], [0, 0, -1]),
fire([4, 4, 9.5], unit([1, 1, -1]))]
return case("poly_degenerate_face", "a collinear face (degenerate normal) above a triangle",
ops, [0, 0, 0, 10, 10, 10])
def expansions():
out = []
for e, name in [(0.0, "zero"), (2.5, "large")]:
v = rect(2, 2, 8, 8, 5.0)
ops = [poly(v, [face([[0, 1, 2, 3]])], expansion=e),
fire([5, 5, 9.5], [0, 0, -1]),
fire([8.5, 5, 9.5], [0, 0, -1])]
out.append(case(f"poly_expansion_{name}", "AddPolyBrepToGrid expansion tolerance", ops,
[0, 0, 0, 10, 10, 10]))
return out
def render():
v = rect(1, 1, 3, 3, 1.0) + rect(0, 0, 4, 4, -1.0)
ops = [poly(v, [face([[0, 1, 2, 3]]), face([[4, 5, 6, 7]])]),
{"op": "renderer", "origin": [0, 0, 0], "x": [1, 0, 0], "y": [0, 1, 0],
"view": [0.5, 0.5, 4.0, 3.5, 3.5, 4.0], "nx": 3, "ny": 3, "rate": 2},
{"op": "light", "v": unit([0.3, 0.2, 1]), "point": False},
{"op": "light", "v": [2, 2, 3.5], "point": True},
{"op": "do_render", "shadows": True, "background": [0.1, 0.2, 0.3],
"object": [0.8, 0.5, 0.2]},
{"op": "render_pixel", "x": 1, "y": 1},
{"op": "render_ray", "p": [2, 2, 3.5], "v": [0, 0, -1]},
{"op": "render_ray", "p": [3.5, 3.5, 3.5], "v": [0, 0, -1]}]
return case("poly_render_shadows", "SmRayRenderer over poly faces with shadow rays "
"(hit-any-thing)", ops, [-1, -1, -2, 5, 5, 5])
def main():
cases = [tess_test10(), stacked(), holed(), notch(), coincident(), big_over_small(),
with_surface(), normals(), degenerate(), *expansions(), render()]
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w") as f:
json.dump({"cases": cases}, f, indent=1)
f.write("\n")
print(f"wrote {OUT}: {len(cases)} cases")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin
status = "green"
claimed_by = ""
claimed_at = ""
notes = "partial: 421 cases, 310 pass, 0 fail, 111 deferred (surface.surface_p8 SmSurface::LocalLineIntersect x108, surface.surface_p6 SmSurface::CalculateContinuities x3 via the surface cache); surface_leaf matches upstream SE on degenerate patches (2 fixtures); SmSurface::EvaluateNormalSection wired to surface.surface_p3 (no case reaches it yet past the deferrals); UNPORTED stubs also for SmFace::PointClassify (query.face_p2), poly types (query.poly); CO-PORTED in intersect.ray_tracer (PR #48): SmSurfaceGridElement data members -> geom::grid::SurfaceGridElement (+ Grid::add_surface_element), SmRayRenderer/SmLightSource -> intersect::ray_tracer; 13 fns; largest: SmLSI3LocalSolver::LocalSolve, SmRayTracer::FireRay, sm_FindAdditionalTangentGuess, sm_FindPolyHit; cycle back-edges (call into later units; stub or co-port): query.poly(7), topology.topology(2), query.face_p2(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit Re-run (PR #118, LocalLineIntersect wired by surface.surface_p8): 421 cases, 415 pass, 6 deferred (surface.surface_p2). LocalSolve sets the solution node marker as upstream does. GREEN (wire: AddSurfaceToGrid): RayTracer::add_surface_to_grid builds the surface cache (nodes hold the surface by its tracer index) and expands it with Grid::expand_hierarchy_with, reading each leaf's patch data, pseudo box and polar box from the cache; the shim now runs the real SmRayTracer::AddSurfaceToGrid and the record compares status, leaf count and per leaf the domain, box, mid-point data, pseudo box and polar box (a polar box that stayed uninitialised was caught this way); the generator puts add_surface before the rays; 421 cases, 421 pass, 0 fail, 0 deferred, max_rel_err 0e0, no absolute floors; 6000 extra seeds clean. SurfaceGridElement::init / from_leaf_data (formerly surface_leaf) and Grid::expand_hierarchy (now taking the surface cache) moved to geom::grid with the geom.grid wiring (geom.grid green); add_surface_to_grid calls Grid::expand_hierarchy; unit numbers unchanged: 421 cases, 421 pass, 0 deferred, max_rel_err 0e0."
notes = "partial: 421 cases, 310 pass, 0 fail, 111 deferred (surface.surface_p8 SmSurface::LocalLineIntersect x108, surface.surface_p6 SmSurface::CalculateContinuities x3 via the surface cache); surface_leaf matches upstream SE on degenerate patches (2 fixtures); SmSurface::EvaluateNormalSection wired to surface.surface_p3 (no case reaches it yet past the deferrals); UNPORTED stubs also for SmFace::PointClassify (query.face_p2), poly types (query.poly); CO-PORTED in intersect.ray_tracer (PR #48): SmSurfaceGridElement data members -> geom::grid::SurfaceGridElement (+ Grid::add_surface_element), SmRayRenderer/SmLightSource -> intersect::ray_tracer; 13 fns; largest: SmLSI3LocalSolver::LocalSolve, SmRayTracer::FireRay, sm_FindAdditionalTangentGuess, sm_FindPolyHit; cycle back-edges (call into later units; stub or co-port): query.poly(7), topology.topology(2), query.face_p2(1); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit Re-run (PR #118, LocalLineIntersect wired by surface.surface_p8): 421 cases, 415 pass, 6 deferred (surface.surface_p2). LocalSolve sets the solution node marker as upstream does. GREEN (wire: AddSurfaceToGrid): RayTracer::add_surface_to_grid builds the surface cache (nodes hold the surface by its tracer index) and expands it with Grid::expand_hierarchy_with, reading each leaf's patch data, pseudo box and polar box from the cache; the shim now runs the real SmRayTracer::AddSurfaceToGrid and the record compares status, leaf count and per leaf the domain, box, mid-point data, pseudo box and polar box (a polar box that stayed uninitialised was caught this way); the generator puts add_surface before the rays; 421 cases, 421 pass, 0 fail, 0 deferred, max_rel_err 0e0, no absolute floors; 6000 extra seeds clean. SurfaceGridElement::init / from_leaf_data (formerly surface_leaf) and Grid::expand_hierarchy (now taking the surface cache) moved to geom::grid with the geom.grid wiring (geom.grid green); add_surface_to_grid calls Grid::expand_hierarchy; unit numbers unchanged: 421 cases, 421 pass, 0 deferred, max_rel_err 0e0. WIRED (PR #1273, query.poly back-edges): AddPolyBrepToGrid (SmPolyBrep::Notify SM_NO_PRE_EDIT, the poly B-rep SmBrepCache, one polygon element per surface-tree leaf) and sm_FindPolyHit (sphere reject, LinePlaneIntersect, PointInPolygon 3D, hit-any exit, parent walk as a bounded loop); FireRay's polygon branch now fills the solution as upstream (SM_ST_SINGLE_VALUE, t, BIG uv, point, face normal, the face in objects 0 and 1; it only moved the hit parameter before) and the per-context mark locks are a per-ray tested-face set. The shim builds real SmPolyBreps (add_poly_brep op; the solution record names poly faces); fixtures/intersect.ray_tracer/poly_breps.json (harness/fixturegen/ray_tracer_poly.py): upstream smTessTest test 10 (Extrusion.pbp, 296 faces) and 11 built cases (holes, notch, coincident faces in two poly B-reps, a face on an inner tree node, poly and surface, normals, degenerate face, expansions, render with shadows); the generator appends poly B-reps to about a third of the cases. No UNPORTED stubs left in this unit."