geometry: nearest ray hits
Browse files- crates/nanobrush-geometry/src/lib.rs +1 -0
- crates/nanobrush-geometry/src/ray.rs +318 -0
- docs/GEOMETRY.md +6 -0
- docs/PROGRESS_2026-10-26.md +1 -1
- docs/TODO.md +2 -0
crates/nanobrush-geometry/src/lib.rs
CHANGED
|
@@ -103,4 +103,5 @@ impl Budget {
|
|
| 103 |
}
|
| 104 |
}
|
| 105 |
pub mod patch;
|
|
|
|
| 106 |
pub mod world;
|
|
|
|
| 103 |
}
|
| 104 |
}
|
| 105 |
pub mod patch;
|
| 106 |
+
pub mod ray;
|
| 107 |
pub mod world;
|
crates/nanobrush-geometry/src/ray.rs
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use crate::{math::*, *};
|
| 2 |
+
#[derive(Clone, Copy, Debug)]
|
| 3 |
+
pub struct Ray {
|
| 4 |
+
origin: DVec3,
|
| 5 |
+
direction: DVec3,
|
| 6 |
+
}
|
| 7 |
+
impl Ray {
|
| 8 |
+
pub fn new(origin: DVec3, direction: DVec3) -> Result<Self> {
|
| 9 |
+
Ok(Self {
|
| 10 |
+
origin: finite(origin)?,
|
| 11 |
+
direction: unit(direction)?,
|
| 12 |
+
})
|
| 13 |
+
}
|
| 14 |
+
pub fn at(self, t: f64) -> Result<DVec3> {
|
| 15 |
+
if !t.is_finite() {
|
| 16 |
+
return Err(Error::NonFinite);
|
| 17 |
+
}
|
| 18 |
+
finite(add(self.origin, scale(self.direction, t)))
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
| 22 |
+
pub enum Target {
|
| 23 |
+
Brush {
|
| 24 |
+
brush: usize,
|
| 25 |
+
plane: usize,
|
| 26 |
+
},
|
| 27 |
+
Patch {
|
| 28 |
+
patch: usize,
|
| 29 |
+
quad: usize,
|
| 30 |
+
triangle: usize,
|
| 31 |
+
},
|
| 32 |
+
}
|
| 33 |
+
#[derive(Clone, Copy, Debug)]
|
| 34 |
+
pub struct Hit {
|
| 35 |
+
pub distance: f64,
|
| 36 |
+
pub point: DVec3,
|
| 37 |
+
pub normal: DVec3,
|
| 38 |
+
pub target: Target,
|
| 39 |
+
}
|
| 40 |
+
fn triangle(
|
| 41 |
+
ray: Ray,
|
| 42 |
+
a: DVec3,
|
| 43 |
+
b: DVec3,
|
| 44 |
+
c: DVec3,
|
| 45 |
+
max: f64,
|
| 46 |
+
p: Policy,
|
| 47 |
+
) -> Result<Option<(f64, DVec3)>> {
|
| 48 |
+
finite(a)?;
|
| 49 |
+
finite(b)?;
|
| 50 |
+
finite(c)?;
|
| 51 |
+
let e1 = finite(sub(b, a))?;
|
| 52 |
+
let e2 = finite(sub(c, a))?;
|
| 53 |
+
let h = finite(cross(ray.direction, e2))?;
|
| 54 |
+
let det = dot(e1, h);
|
| 55 |
+
let area_scale = length(e1) * length(e2);
|
| 56 |
+
if !det.is_finite() || !area_scale.is_finite() {
|
| 57 |
+
return Err(Error::NonFinite);
|
| 58 |
+
}
|
| 59 |
+
if det.abs() <= p.angular_epsilon * area_scale {
|
| 60 |
+
return Ok(None);
|
| 61 |
+
}
|
| 62 |
+
let s = sub(ray.origin, a);
|
| 63 |
+
let u = dot(s, h) / det;
|
| 64 |
+
let q = finite(cross(s, e1))?;
|
| 65 |
+
let v = dot(ray.direction, q) / det;
|
| 66 |
+
let t = dot(e2, q) / det;
|
| 67 |
+
if !u.is_finite() || !v.is_finite() || !t.is_finite() {
|
| 68 |
+
return Err(Error::NonFinite);
|
| 69 |
+
}
|
| 70 |
+
if u < -p.angular_epsilon
|
| 71 |
+
|| v < -p.angular_epsilon
|
| 72 |
+
|| u + v > 1. + p.angular_epsilon
|
| 73 |
+
|| t < -p.distance_epsilon
|
| 74 |
+
|| t > max
|
| 75 |
+
{
|
| 76 |
+
return Ok(None);
|
| 77 |
+
}
|
| 78 |
+
Ok(Some((t.max(0.), unit(cross(e1, e2))?)))
|
| 79 |
+
}
|
| 80 |
+
struct Query {
|
| 81 |
+
ray: Ray,
|
| 82 |
+
max: f64,
|
| 83 |
+
policy: Policy,
|
| 84 |
+
budget: Budget,
|
| 85 |
+
hit: Option<Hit>,
|
| 86 |
+
vertices: usize,
|
| 87 |
+
}
|
| 88 |
+
impl Query {
|
| 89 |
+
fn vertices(&mut self, n: usize) -> Result<()> {
|
| 90 |
+
self.vertices = self.vertices.checked_add(n).ok_or(Error::Overflow)?;
|
| 91 |
+
if self.vertices > self.budget.limits.max_vertices {
|
| 92 |
+
Err(Error::Limit)
|
| 93 |
+
} else {
|
| 94 |
+
Ok(())
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
fn test(&mut self, v: [DVec3; 3], target: Target) -> Result<()> {
|
| 98 |
+
self.budget.work(1)?;
|
| 99 |
+
if let Some((distance, normal)) =
|
| 100 |
+
triangle(self.ray, v[0], v[1], v[2], self.max, self.policy)?
|
| 101 |
+
{
|
| 102 |
+
let replace = self.hit.is_none_or(|old| {
|
| 103 |
+
distance < old.distance - self.policy.distance_epsilon
|
| 104 |
+
|| ((distance - old.distance).abs() <= self.policy.distance_epsilon
|
| 105 |
+
&& target < old.target)
|
| 106 |
+
});
|
| 107 |
+
if replace {
|
| 108 |
+
self.hit = Some(Hit {
|
| 109 |
+
distance,
|
| 110 |
+
point: self.ray.at(distance)?,
|
| 111 |
+
normal,
|
| 112 |
+
target,
|
| 113 |
+
});
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
Ok(())
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
/// Linear double-sided nearest surface query over explicit computed geometry.
|
| 120 |
+
/// No selection/visibility state exists in this model. Ray directions are unit,
|
| 121 |
+
/// so returned parameters are world distances rather than caller vector scales.
|
| 122 |
+
pub fn nearest(
|
| 123 |
+
world: &mb3_dat::World,
|
| 124 |
+
ray: Ray,
|
| 125 |
+
max_distance: f64,
|
| 126 |
+
policy: Policy,
|
| 127 |
+
limits: ComputeLimits,
|
| 128 |
+
) -> Result<Option<Hit>> {
|
| 129 |
+
let policy = policy.validate()?;
|
| 130 |
+
if !max_distance.is_finite() || max_distance < 0. {
|
| 131 |
+
return Err(Error::InvalidDimensions);
|
| 132 |
+
}
|
| 133 |
+
if !world.computed {
|
| 134 |
+
return Err(Error::InconsistentData);
|
| 135 |
+
}
|
| 136 |
+
let mut q = Query {
|
| 137 |
+
ray,
|
| 138 |
+
max: max_distance,
|
| 139 |
+
policy,
|
| 140 |
+
budget: Budget {
|
| 141 |
+
limits,
|
| 142 |
+
stats: Stats::default(),
|
| 143 |
+
},
|
| 144 |
+
hit: None,
|
| 145 |
+
vertices: 0,
|
| 146 |
+
};
|
| 147 |
+
for (bi, b) in world.brushes.iter().enumerate() {
|
| 148 |
+
q.budget.work(1)?;
|
| 149 |
+
for (pi, p) in b.planes.iter().enumerate() {
|
| 150 |
+
q.budget.work(1)?;
|
| 151 |
+
let vertices = p.polygon.as_ref().ok_or(Error::InconsistentData)?;
|
| 152 |
+
if vertices.len() < 3 {
|
| 153 |
+
return Err(Error::InvalidBrush);
|
| 154 |
+
}
|
| 155 |
+
q.vertices(vertices.len())?;
|
| 156 |
+
for i in 1..vertices.len() - 1 {
|
| 157 |
+
q.test(
|
| 158 |
+
[
|
| 159 |
+
from_f32(vertices[0])?,
|
| 160 |
+
from_f32(vertices[i])?,
|
| 161 |
+
from_f32(vertices[i + 1])?,
|
| 162 |
+
],
|
| 163 |
+
Target::Brush {
|
| 164 |
+
brush: bi,
|
| 165 |
+
plane: pi,
|
| 166 |
+
},
|
| 167 |
+
)?;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
for (pi, p) in world.patches.iter().enumerate() {
|
| 172 |
+
q.budget.work(1)?;
|
| 173 |
+
let data = p.computed.as_ref().ok_or(Error::InconsistentData)?;
|
| 174 |
+
let u = (p.anchors_u as usize / 2)
|
| 175 |
+
.checked_mul(p.quads_u as usize)
|
| 176 |
+
.and_then(|n| n.checked_add(1))
|
| 177 |
+
.ok_or(Error::Overflow)?;
|
| 178 |
+
let v = (p.anchors_v as usize / 2)
|
| 179 |
+
.checked_mul(p.quads_v as usize)
|
| 180 |
+
.and_then(|n| n.checked_add(1))
|
| 181 |
+
.ok_or(Error::Overflow)?;
|
| 182 |
+
let n = u.checked_mul(v).ok_or(Error::Overflow)?;
|
| 183 |
+
if data.points.len() != n || data.normals.len() != n {
|
| 184 |
+
return Err(Error::InconsistentData);
|
| 185 |
+
}
|
| 186 |
+
q.vertices(n)?;
|
| 187 |
+
for point in &data.points {
|
| 188 |
+
q.budget.work(1)?;
|
| 189 |
+
from_f32(*point)?;
|
| 190 |
+
}
|
| 191 |
+
for y in 0..v - 1 {
|
| 192 |
+
for x in 0..u - 1 {
|
| 193 |
+
let i = x + y * u;
|
| 194 |
+
let quad = x + y * (u - 1);
|
| 195 |
+
for (t, ids) in [[i, i + u, i + u + 1], [i, i + u + 1, i + 1]]
|
| 196 |
+
.into_iter()
|
| 197 |
+
.enumerate()
|
| 198 |
+
{
|
| 199 |
+
q.test(
|
| 200 |
+
[
|
| 201 |
+
from_f32(data.points[ids[0]])?,
|
| 202 |
+
from_f32(data.points[ids[1]])?,
|
| 203 |
+
from_f32(data.points[ids[2]])?,
|
| 204 |
+
],
|
| 205 |
+
Target::Patch {
|
| 206 |
+
patch: pi,
|
| 207 |
+
quad,
|
| 208 |
+
triangle: t,
|
| 209 |
+
},
|
| 210 |
+
)?;
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
Ok(q.hit)
|
| 216 |
+
}
|
| 217 |
+
#[cfg(test)]
|
| 218 |
+
mod tests {
|
| 219 |
+
use super::*;
|
| 220 |
+
use mb3_dat::{Limits, World};
|
| 221 |
+
#[test]
|
| 222 |
+
fn cube_nearest_inside_miss_and_stable_ties() {
|
| 223 |
+
let source = World {
|
| 224 |
+
brushes: vec![crate::brush::cube(), crate::brush::cube()],
|
| 225 |
+
..Default::default()
|
| 226 |
+
};
|
| 227 |
+
let (w, _) = crate::world::compute(
|
| 228 |
+
&source,
|
| 229 |
+
Policy::default(),
|
| 230 |
+
ComputeLimits::default(),
|
| 231 |
+
Limits::default(),
|
| 232 |
+
)
|
| 233 |
+
.unwrap();
|
| 234 |
+
let ray = Ray::new([0., 0., 3.], [0., 0., -2.]).unwrap();
|
| 235 |
+
let hit = nearest(&w, ray, 10., Policy::default(), ComputeLimits::default())
|
| 236 |
+
.unwrap()
|
| 237 |
+
.unwrap();
|
| 238 |
+
assert_eq!(hit.distance, 2.);
|
| 239 |
+
assert_eq!(hit.target, Target::Brush { brush: 0, plane: 5 });
|
| 240 |
+
assert_eq!(hit.normal, [0., 0., 1.]);
|
| 241 |
+
assert!(
|
| 242 |
+
nearest(&w, ray, 1., Policy::default(), ComputeLimits::default())
|
| 243 |
+
.unwrap()
|
| 244 |
+
.is_none()
|
| 245 |
+
);
|
| 246 |
+
let inside = nearest(
|
| 247 |
+
&w,
|
| 248 |
+
Ray::new([0.; 3], [1., 0., 0.]).unwrap(),
|
| 249 |
+
10.,
|
| 250 |
+
Policy::default(),
|
| 251 |
+
ComputeLimits::default(),
|
| 252 |
+
)
|
| 253 |
+
.unwrap()
|
| 254 |
+
.unwrap();
|
| 255 |
+
assert_eq!(inside.distance, 1.);
|
| 256 |
+
assert!(
|
| 257 |
+
nearest(
|
| 258 |
+
&w,
|
| 259 |
+
Ray::new([0., 0., 3.], [0., 0., 1.]).unwrap(),
|
| 260 |
+
10.,
|
| 261 |
+
Policy::default(),
|
| 262 |
+
ComputeLimits::default()
|
| 263 |
+
)
|
| 264 |
+
.unwrap()
|
| 265 |
+
.is_none()
|
| 266 |
+
);
|
| 267 |
+
}
|
| 268 |
+
#[test]
|
| 269 |
+
fn patch_hit_backfaces_and_budget() {
|
| 270 |
+
let source = World {
|
| 271 |
+
patches: vec![crate::patch::flat(3, 3)],
|
| 272 |
+
..Default::default()
|
| 273 |
+
};
|
| 274 |
+
let (w, _) = crate::world::compute(
|
| 275 |
+
&source,
|
| 276 |
+
Policy::default(),
|
| 277 |
+
ComputeLimits::default(),
|
| 278 |
+
Limits::default(),
|
| 279 |
+
)
|
| 280 |
+
.unwrap();
|
| 281 |
+
for (origin, dir) in [
|
| 282 |
+
([0.5, 3., 0.5], [0., -1., 0.]),
|
| 283 |
+
([0.5, -3., 0.5], [0., 1., 0.]),
|
| 284 |
+
] {
|
| 285 |
+
let ray = Ray::new(origin, dir).unwrap();
|
| 286 |
+
let hit = nearest(&w, ray, 10., Policy::default(), ComputeLimits::default())
|
| 287 |
+
.unwrap()
|
| 288 |
+
.unwrap();
|
| 289 |
+
assert_eq!(hit.distance, 3.);
|
| 290 |
+
assert_eq!(hit.normal, [0., 1., 0.]);
|
| 291 |
+
assert!(matches!(hit.target, Target::Patch { patch: 0, .. }));
|
| 292 |
+
assert!(
|
| 293 |
+
nearest(
|
| 294 |
+
&w,
|
| 295 |
+
ray,
|
| 296 |
+
10.,
|
| 297 |
+
Policy::default(),
|
| 298 |
+
ComputeLimits {
|
| 299 |
+
max_work: 1,
|
| 300 |
+
..Default::default()
|
| 301 |
+
}
|
| 302 |
+
)
|
| 303 |
+
.is_err()
|
| 304 |
+
);
|
| 305 |
+
}
|
| 306 |
+
assert!(Ray::new([0.; 3], [0.; 3]).is_err());
|
| 307 |
+
assert!(
|
| 308 |
+
nearest(
|
| 309 |
+
&w,
|
| 310 |
+
Ray::new([0.; 3], [1., 0., 0.]).unwrap(),
|
| 311 |
+
f64::INFINITY,
|
| 312 |
+
Policy::default(),
|
| 313 |
+
ComputeLimits::default()
|
| 314 |
+
)
|
| 315 |
+
.is_err()
|
| 316 |
+
);
|
| 317 |
+
}
|
| 318 |
+
}
|
docs/GEOMETRY.md
CHANGED
|
@@ -23,3 +23,9 @@ Anchor dimensions must be odd and at least three, and quads per tile at least tw
|
|
| 23 |
`world::compute` returns a new world, preserving source tables, slots, bases and anchors. It fallibly copies editable data, rebuilds every brush/patch, checks f64→f32 range, and sets computed=true only after all objects succeed. Prior derived arrays are intentionally replaced. Properties are copied unchanged, not normalized; codec-specific validation still happens at save. No source mutation or partial world escapes on a late error.
|
| 24 |
|
| 25 |
Work and generated-position budgets apply across the whole world. Requested copy/geometry/output allocations are charged cumulatively (including scratch already freed, conservatively); model element/string limits also apply. CLI `convert/preflight --compute` opts into this native policy before target analysis/encoding. Defaults cap 64 planes per brush, 100,000 generated positions, 10 million work units, and the lower of codec/geometry storage limits. Empty worlds compute successfully; malformed/redundant/open brushes and degenerate patches fail instead of being dropped. Mixed cube/patch, property preservation, DAT round trip, late failure, global-budget and CLI cube-to-OBJ tests pass.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
`world::compute` returns a new world, preserving source tables, slots, bases and anchors. It fallibly copies editable data, rebuilds every brush/patch, checks f64→f32 range, and sets computed=true only after all objects succeed. Prior derived arrays are intentionally replaced. Properties are copied unchanged, not normalized; codec-specific validation still happens at save. No source mutation or partial world escapes on a late error.
|
| 24 |
|
| 25 |
Work and generated-position budgets apply across the whole world. Requested copy/geometry/output allocations are charged cumulatively (including scratch already freed, conservatively); model element/string limits also apply. CLI `convert/preflight --compute` opts into this native policy before target analysis/encoding. Defaults cap 64 planes per brush, 100,000 generated positions, 10 million work units, and the lower of codec/geometry storage limits. Empty worlds compute successfully; malformed/redundant/open brushes and degenerate patches fail instead of being dropped. Mixed cube/patch, property preservation, DAT round trip, late failure, global-budget and CLI cube-to-OBJ tests pass.
|
| 26 |
+
|
| 27 |
+
## Nearest-hit queries
|
| 28 |
+
|
| 29 |
+
`ray::nearest` scans explicit computed brush polygons (convex fans) and patch quads split along the first-to-third diagonal. Queries are double-sided. Ray directions are normalized, so hit parameters are world distances. Negative distances within distance epsilon clamp to zero; larger negative hits and hits beyond finite max distance are excluded. Parallel/degenerate triangles produce no hit; non-finite geometry and malformed arrays fail.
|
| 30 |
+
|
| 31 |
+
Ties within distance epsilon use stable target order: brush before patch, then ascending object/plane or quad/triangle indices. Returned normals follow polygon/quad winding and do not flip toward the ray. Work/vertex caps apply to the whole linear query, including parents and patch point scans. No acceleration, visibility, selection, near-clip or callback semantics are implied; unused properties/stored normals are not revalidated. Tests cover nearest/inside/miss/max-distance, duplicate-object ties, front/back patch hits, direction normalization and invalid/budget cases.
|
docs/PROGRESS_2026-10-26.md
CHANGED
|
@@ -12,5 +12,5 @@ Scope: extend native headless formats and geometry. Each stage is independently
|
|
| 12 |
| 6 | P5.16 | Convex brush contours | Complete: bounded half-space intersections, recession rejection, deterministic outward contours, closed-edge and volume checks. Cube/translation/winding/invalid/redundant/budget tests pass. |
|
| 13 |
| 7 | P5.17 | Patch9 tessellation | Complete: tiled tensor-quadratic positions, analytic derivatives, OBJ-consistent normals, shared-position seam policy and bounded allocations/work. Planar/curved/multi-tile/degenerate/limit tests pass. |
|
| 14 |
| 8 | P5.18 | Computed world pipeline/CLI | Complete: fallible editable-data copying, aggregate work/storage/position budgets, complete-or-error derived data and explicit `--compute`. Mixed-world/property/DAT/late-failure/global-limit and CLI cube→OBJ tests pass. |
|
| 15 |
-
| 9 | P5.19 | Nearest-hit ray queries |
|
| 16 |
| 10 | P5.20 | Property table edits/reindexing | Pending |
|
|
|
|
| 12 |
| 6 | P5.16 | Convex brush contours | Complete: bounded half-space intersections, recession rejection, deterministic outward contours, closed-edge and volume checks. Cube/translation/winding/invalid/redundant/budget tests pass. |
|
| 13 |
| 7 | P5.17 | Patch9 tessellation | Complete: tiled tensor-quadratic positions, analytic derivatives, OBJ-consistent normals, shared-position seam policy and bounded allocations/work. Planar/curved/multi-tile/degenerate/limit tests pass. |
|
| 14 |
| 8 | P5.18 | Computed world pipeline/CLI | Complete: fallible editable-data copying, aggregate work/storage/position budgets, complete-or-error derived data and explicit `--compute`. Mixed-world/property/DAT/late-failure/global-limit and CLI cube→OBJ tests pass. |
|
| 15 |
+
| 9 | P5.19 | Nearest-hit ray queries | Complete: bounded double-sided computed-surface queries, normalized rays, stable object/face/quad ties and winding normals. Cube/inside/miss/range/tie/patch/backface/budget tests pass. |
|
| 16 |
| 10 | P5.20 | Property table edits/reindexing | Pending |
|
docs/TODO.md
CHANGED
|
@@ -134,6 +134,8 @@ Each task ID is stable. Split tasks into child IDs as evidence grows. Record dep
|
|
| 134 |
|
| 135 |
- [x] P5.18 Integrate bounded complete-or-error computed worlds and explicit CLI geometry generation ([GEOMETRY.md](GEOMETRY.md)).
|
| 136 |
|
|
|
|
|
|
|
| 137 |
## P6 — File formats and converters
|
| 138 |
|
| 139 |
**Depends on:** P3 oracle where available, P5 document model. **Exit:** standalone format specifications, declared compatibility subsets, fixture corpus, and native codecs.
|
|
|
|
| 134 |
|
| 135 |
- [x] P5.18 Integrate bounded complete-or-error computed worlds and explicit CLI geometry generation ([GEOMETRY.md](GEOMETRY.md)).
|
| 136 |
|
| 137 |
+
- [x] P5.19 Implement bounded deterministic native nearest-hit ray queries over computed brush/patch surfaces ([GEOMETRY.md](GEOMETRY.md)).
|
| 138 |
+
|
| 139 |
## P6 — File formats and converters
|
| 140 |
|
| 141 |
**Depends on:** P3 oracle where available, P5 document model. **Exit:** standalone format specifications, declared compatibility subsets, fixture corpus, and native codecs.
|