mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 13:01:52 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user