geometry: analytic primitives
Browse files- Cargo.lock +7 -0
- Cargo.toml +1 -1
- crates/nanobrush-geometry/Cargo.toml +10 -0
- crates/nanobrush-geometry/src/lib.rs +29 -0
- crates/nanobrush-geometry/src/math.rs +253 -0
- docs/GEOMETRY.md +7 -0
- docs/PROGRESS_2026-10-26.md +1 -1
- docs/TODO.md +2 -0
Cargo.lock
CHANGED
|
@@ -29,3 +29,10 @@ dependencies = [
|
|
| 29 |
"mb3-obj",
|
| 30 |
"mb3-txt",
|
| 31 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
"mb3-obj",
|
| 30 |
"mb3-txt",
|
| 31 |
]
|
| 32 |
+
|
| 33 |
+
[[package]]
|
| 34 |
+
name = "nanobrush-geometry"
|
| 35 |
+
version = "0.1.0"
|
| 36 |
+
dependencies = [
|
| 37 |
+
"mb3-dat",
|
| 38 |
+
]
|
Cargo.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
[workspace]
|
| 2 |
-
members = ["crates/mb3-dat", "crates/mb3-txt", "crates/nanobrush-cli", "crates/mb3-obj"]
|
| 3 |
resolver = "3"
|
| 4 |
|
| 5 |
[workspace.package]
|
|
|
|
| 1 |
[workspace]
|
| 2 |
+
members = ["crates/mb3-dat", "crates/mb3-txt", "crates/nanobrush-cli", "crates/mb3-obj", "crates/nanobrush-geometry"]
|
| 3 |
resolver = "3"
|
| 4 |
|
| 5 |
[workspace.package]
|
crates/nanobrush-geometry/Cargo.toml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[package]
|
| 2 |
+
name = "nanobrush-geometry"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
edition.workspace = true
|
| 5 |
+
rust-version.workspace = true
|
| 6 |
+
publish = false
|
| 7 |
+
[dependencies]
|
| 8 |
+
mb3-dat = { path = "../mb3-dat" }
|
| 9 |
+
[lints]
|
| 10 |
+
workspace = true
|
crates/nanobrush-geometry/src/lib.rs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Native analytic geometry policy; original-editor numerical parity is unverified.
|
| 2 |
+
pub mod math;
|
| 3 |
+
pub use math::{Aabb, DVec3, Equation, Policy};
|
| 4 |
+
use std::fmt;
|
| 5 |
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
| 6 |
+
pub enum Error {
|
| 7 |
+
NonFinite,
|
| 8 |
+
Degenerate,
|
| 9 |
+
InvalidPolicy,
|
| 10 |
+
InvalidDimensions,
|
| 11 |
+
InvalidBrush,
|
| 12 |
+
Limit,
|
| 13 |
+
Allocation,
|
| 14 |
+
Overflow,
|
| 15 |
+
FloatRange,
|
| 16 |
+
InconsistentData,
|
| 17 |
+
}
|
| 18 |
+
impl fmt::Display for Error {
|
| 19 |
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
| 20 |
+
write!(f, "geometry: {self:?}")
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
impl std::error::Error for Error {}
|
| 24 |
+
pub type Result<T> = std::result::Result<T, Error>;
|
| 25 |
+
pub(crate) fn alloc<T>(n: usize) -> Result<Vec<T>> {
|
| 26 |
+
let mut out = Vec::new();
|
| 27 |
+
out.try_reserve_exact(n).map_err(|_| Error::Allocation)?;
|
| 28 |
+
Ok(out)
|
| 29 |
+
}
|
crates/nanobrush-geometry/src/math.rs
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use crate::{Error, Result};
|
| 2 |
+
pub type DVec3 = [f64; 3];
|
| 3 |
+
#[derive(Clone, Copy, Debug)]
|
| 4 |
+
pub struct Policy {
|
| 5 |
+
pub distance_epsilon: f64,
|
| 6 |
+
pub angular_epsilon: f64,
|
| 7 |
+
}
|
| 8 |
+
impl Default for Policy {
|
| 9 |
+
fn default() -> Self {
|
| 10 |
+
Self {
|
| 11 |
+
distance_epsilon: 1e-6,
|
| 12 |
+
angular_epsilon: 1e-10,
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
impl Policy {
|
| 17 |
+
pub fn validate(self) -> Result<Self> {
|
| 18 |
+
if !self.distance_epsilon.is_finite()
|
| 19 |
+
|| self.distance_epsilon <= 0.
|
| 20 |
+
|| !self.angular_epsilon.is_finite()
|
| 21 |
+
|| self.angular_epsilon <= 0.
|
| 22 |
+
|| self.angular_epsilon >= 1.
|
| 23 |
+
{
|
| 24 |
+
Err(Error::InvalidPolicy)
|
| 25 |
+
} else {
|
| 26 |
+
Ok(self)
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
pub fn add(a: DVec3, b: DVec3) -> DVec3 {
|
| 31 |
+
std::array::from_fn(|i| a[i] + b[i])
|
| 32 |
+
}
|
| 33 |
+
pub fn sub(a: DVec3, b: DVec3) -> DVec3 {
|
| 34 |
+
std::array::from_fn(|i| a[i] - b[i])
|
| 35 |
+
}
|
| 36 |
+
pub fn scale(a: DVec3, b: f64) -> DVec3 {
|
| 37 |
+
a.map(|x| x * b)
|
| 38 |
+
}
|
| 39 |
+
pub fn dot(a: DVec3, b: DVec3) -> f64 {
|
| 40 |
+
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
| 41 |
+
}
|
| 42 |
+
pub fn cross(a: DVec3, b: DVec3) -> DVec3 {
|
| 43 |
+
[
|
| 44 |
+
a[1] * b[2] - a[2] * b[1],
|
| 45 |
+
a[2] * b[0] - a[0] * b[2],
|
| 46 |
+
a[0] * b[1] - a[1] * b[0],
|
| 47 |
+
]
|
| 48 |
+
}
|
| 49 |
+
pub fn finite(v: DVec3) -> Result<DVec3> {
|
| 50 |
+
if v.iter().all(|x| x.is_finite()) {
|
| 51 |
+
Ok(v)
|
| 52 |
+
} else {
|
| 53 |
+
Err(Error::NonFinite)
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
pub fn length(v: DVec3) -> f64 {
|
| 57 |
+
v[0].hypot(v[1]).hypot(v[2])
|
| 58 |
+
}
|
| 59 |
+
pub fn unit(v: DVec3) -> Result<DVec3> {
|
| 60 |
+
finite(v)?;
|
| 61 |
+
let n = length(v);
|
| 62 |
+
if !n.is_finite() {
|
| 63 |
+
return Err(Error::NonFinite);
|
| 64 |
+
}
|
| 65 |
+
if n == 0. {
|
| 66 |
+
return Err(Error::Degenerate);
|
| 67 |
+
}
|
| 68 |
+
finite(scale(v, 1. / n))
|
| 69 |
+
}
|
| 70 |
+
pub fn from_f32(v: mb3_dat::Vec3) -> Result<DVec3> {
|
| 71 |
+
finite(v.map(f64::from))
|
| 72 |
+
}
|
| 73 |
+
pub fn to_f32(v: DVec3) -> Result<mb3_dat::Vec3> {
|
| 74 |
+
finite(v)?;
|
| 75 |
+
let out = v.map(|n| n as f32);
|
| 76 |
+
if out.iter().all(|n| n.is_finite()) {
|
| 77 |
+
Ok(out)
|
| 78 |
+
} else {
|
| 79 |
+
Err(Error::FloatRange)
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
pub fn map_to_native(v: DVec3) -> DVec3 {
|
| 83 |
+
[v[0], v[2], -v[1]]
|
| 84 |
+
}
|
| 85 |
+
pub fn native_to_map(v: DVec3) -> DVec3 {
|
| 86 |
+
[v[0], -v[2], v[1]]
|
| 87 |
+
}
|
| 88 |
+
#[derive(Clone, Copy, Debug)]
|
| 89 |
+
pub struct Equation {
|
| 90 |
+
normal: DVec3,
|
| 91 |
+
d: f64,
|
| 92 |
+
}
|
| 93 |
+
impl Equation {
|
| 94 |
+
pub fn new(normal: DVec3, d: f64) -> Result<Self> {
|
| 95 |
+
let n = length(finite(normal)?);
|
| 96 |
+
if n == 0. {
|
| 97 |
+
return Err(Error::Degenerate);
|
| 98 |
+
}
|
| 99 |
+
if !n.is_finite() || !d.is_finite() {
|
| 100 |
+
return Err(Error::NonFinite);
|
| 101 |
+
}
|
| 102 |
+
let out = Self {
|
| 103 |
+
normal: scale(normal, 1. / n),
|
| 104 |
+
d: d / n,
|
| 105 |
+
};
|
| 106 |
+
finite(out.normal)?;
|
| 107 |
+
if !out.d.is_finite() {
|
| 108 |
+
return Err(Error::NonFinite);
|
| 109 |
+
}
|
| 110 |
+
Ok(out)
|
| 111 |
+
}
|
| 112 |
+
pub fn from_basis(p: &mb3_dat::Plane, policy: Policy) -> Result<Self> {
|
| 113 |
+
let policy = policy.validate()?;
|
| 114 |
+
let origin = from_f32(p.p0)?;
|
| 115 |
+
let n = cross(unit(from_f32(p.u)?)?, unit(from_f32(p.v)?)?);
|
| 116 |
+
if length(n) <= policy.angular_epsilon {
|
| 117 |
+
return Err(Error::Degenerate);
|
| 118 |
+
}
|
| 119 |
+
let n = unit(n)?;
|
| 120 |
+
Self::new(n, dot(n, origin))
|
| 121 |
+
}
|
| 122 |
+
pub fn normal(self) -> DVec3 {
|
| 123 |
+
self.normal
|
| 124 |
+
}
|
| 125 |
+
pub fn offset(self) -> f64 {
|
| 126 |
+
self.d
|
| 127 |
+
}
|
| 128 |
+
pub fn distance(self, p: DVec3) -> Result<f64> {
|
| 129 |
+
let d = dot(self.normal, finite(p)?) - self.d;
|
| 130 |
+
if d.is_finite() {
|
| 131 |
+
Ok(d)
|
| 132 |
+
} else {
|
| 133 |
+
Err(Error::NonFinite)
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
pub fn intersection(a: Self, b: Self, c: Self, policy: Policy) -> Result<Option<DVec3>> {
|
| 137 |
+
let policy = policy.validate()?;
|
| 138 |
+
let det = dot(a.normal, cross(b.normal, c.normal));
|
| 139 |
+
if det.abs() <= policy.angular_epsilon {
|
| 140 |
+
return Ok(None);
|
| 141 |
+
}
|
| 142 |
+
finite(scale(
|
| 143 |
+
add(
|
| 144 |
+
add(
|
| 145 |
+
scale(cross(b.normal, c.normal), a.d),
|
| 146 |
+
scale(cross(c.normal, a.normal), b.d),
|
| 147 |
+
),
|
| 148 |
+
scale(cross(a.normal, b.normal), c.d),
|
| 149 |
+
),
|
| 150 |
+
1. / det,
|
| 151 |
+
))
|
| 152 |
+
.map(Some)
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
| 156 |
+
pub struct Aabb {
|
| 157 |
+
pub min: DVec3,
|
| 158 |
+
pub max: DVec3,
|
| 159 |
+
}
|
| 160 |
+
impl Aabb {
|
| 161 |
+
pub fn new(min: DVec3, max: DVec3) -> Result<Self> {
|
| 162 |
+
finite(min)?;
|
| 163 |
+
finite(max)?;
|
| 164 |
+
if (0..3).any(|i| min[i] > max[i]) {
|
| 165 |
+
return Err(Error::InvalidDimensions);
|
| 166 |
+
}
|
| 167 |
+
Ok(Self { min, max })
|
| 168 |
+
}
|
| 169 |
+
pub fn from_points(points: impl IntoIterator<Item = DVec3>) -> Result<Option<Self>> {
|
| 170 |
+
let mut out: Option<Self> = None;
|
| 171 |
+
for p in points {
|
| 172 |
+
finite(p)?;
|
| 173 |
+
if let Some(b) = &mut out {
|
| 174 |
+
for (i, value) in p.iter().enumerate() {
|
| 175 |
+
b.min[i] = b.min[i].min(*value);
|
| 176 |
+
b.max[i] = b.max[i].max(*value);
|
| 177 |
+
}
|
| 178 |
+
} else {
|
| 179 |
+
out = Some(Self { min: p, max: p });
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
Ok(out)
|
| 183 |
+
}
|
| 184 |
+
pub fn contains(self, p: DVec3, policy: Policy) -> Result<bool> {
|
| 185 |
+
let e = policy.validate()?.distance_epsilon;
|
| 186 |
+
Self::new(self.min, self.max)?;
|
| 187 |
+
finite(p)?;
|
| 188 |
+
Ok((0..3).all(|i| p[i] >= self.min[i] - e && p[i] <= self.max[i] + e))
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
#[cfg(test)]
|
| 192 |
+
mod tests {
|
| 193 |
+
use super::*;
|
| 194 |
+
#[test]
|
| 195 |
+
fn basis_orientation_and_intersections() {
|
| 196 |
+
let p = mb3_dat::Plane {
|
| 197 |
+
p0: [2., 0., 0.],
|
| 198 |
+
u: [0., 1., 0.],
|
| 199 |
+
v: [0., 0., 1.],
|
| 200 |
+
..Default::default()
|
| 201 |
+
};
|
| 202 |
+
let a = Equation::from_basis(&p, Policy::default()).unwrap();
|
| 203 |
+
assert_eq!(a.normal(), [1., 0., 0.]);
|
| 204 |
+
assert_eq!(a.distance([3., 0., 0.]).unwrap(), 1.);
|
| 205 |
+
let b = Equation::new([0., 1., 0.], 3.).unwrap();
|
| 206 |
+
let c = Equation::new([0., 0., 1.], 4.).unwrap();
|
| 207 |
+
assert_eq!(
|
| 208 |
+
Equation::intersection(a, b, c, Policy::default()).unwrap(),
|
| 209 |
+
Some([2., 3., 4.])
|
| 210 |
+
);
|
| 211 |
+
assert!(
|
| 212 |
+
Equation::intersection(a, a, c, Policy::default())
|
| 213 |
+
.unwrap()
|
| 214 |
+
.is_none()
|
| 215 |
+
);
|
| 216 |
+
}
|
| 217 |
+
#[test]
|
| 218 |
+
fn boxes_axes_and_empty() {
|
| 219 |
+
assert_eq!(Aabb::from_points([]).unwrap(), None);
|
| 220 |
+
let b = Aabb::from_points([[-3., 2., 1.], [4., -1., 8.]])
|
| 221 |
+
.unwrap()
|
| 222 |
+
.unwrap();
|
| 223 |
+
assert_eq!(b.min, [-3., -1., 1.]);
|
| 224 |
+
assert!(b.contains([0., 0., 2.], Policy::default()).unwrap());
|
| 225 |
+
let v = [1., 2., 3.];
|
| 226 |
+
assert_eq!(native_to_map(map_to_native(v)), v);
|
| 227 |
+
}
|
| 228 |
+
#[test]
|
| 229 |
+
fn invalid_and_scaled_bases() {
|
| 230 |
+
assert!(unit([0.; 3]).is_err());
|
| 231 |
+
assert!(Equation::new([f64::NAN, 0., 0.], 1.).is_err());
|
| 232 |
+
assert!(to_f32([f64::MAX, 0., 0.]).is_err());
|
| 233 |
+
assert!(
|
| 234 |
+
Policy {
|
| 235 |
+
distance_epsilon: 0.,
|
| 236 |
+
..Policy::default()
|
| 237 |
+
}
|
| 238 |
+
.validate()
|
| 239 |
+
.is_err()
|
| 240 |
+
);
|
| 241 |
+
let p = mb3_dat::Plane {
|
| 242 |
+
u: [0., 1e-20, 0.],
|
| 243 |
+
v: [0., 0., 1e20],
|
| 244 |
+
..Default::default()
|
| 245 |
+
};
|
| 246 |
+
assert_eq!(
|
| 247 |
+
Equation::from_basis(&p, Policy::default())
|
| 248 |
+
.unwrap()
|
| 249 |
+
.normal(),
|
| 250 |
+
[1., 0., 0.]
|
| 251 |
+
);
|
| 252 |
+
}
|
| 253 |
+
}
|
docs/GEOMETRY.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Native geometry contract
|
| 2 |
+
|
| 3 |
+
This is an independently authored native policy, not verified editor parity. The supplied `GeometrySolver.ixpl` uses `U×V` for signed plane distance; it does not prove which side the editor regards as interior. We adopt outward normals and interior `n·x <= d + epsilon`. Computation uses f64; persisted results must remain finite f32. Unit normals are formed from normalized U/V to avoid scale-dependent basis degeneracy.
|
| 4 |
+
|
| 5 |
+
Default distance tolerance is 1e-6 world units; angular/determinant tolerance is 1e-10. Both must be finite and positive, with angular tolerance below one. These are explicit native choices, not a claim to reproduce the source helper’s raw determinant threshold. Parallel/nearly parallel triples return no intersection. AABBs retain finite min/max values and include an explicit empty result. MAP/native axes follow the converter mapping `(x,y,z) -> (x,z,-y)` and its inverse. Invalid vectors/policies/ranges return errors.
|
| 6 |
+
|
| 7 |
+
Tests cover signed basis orientation, asymmetric three-plane intersections, parallel triples, scaled bases, boxes, empty bounds, axis round trips and invalid values. See the [ten-task ledger](PROGRESS_2026-10-26.md) for staged extensions and remaining parity limits.
|
docs/PROGRESS_2026-10-26.md
CHANGED
|
@@ -8,7 +8,7 @@ Scope: extend native headless formats and geometry. Each stage is independently
|
|
| 8 |
| 2 | P6.24 | Explicit Latin-1 TXT input | Complete: ISO-8859-1 API and `--text-encoding latin1`, no guessing, conflicting UTF-8 BOM rejected; transcoded bytes and scratch ownership are bounded. UTF-8 remains default and output. Encoding and CLI unit suites pass. |
|
| 9 |
| 3 | P6.25 | Bounded computed OBJ writer | Complete: explicit contours and patch grids, stable per-brush exact position deduplication, global 1-based indices and quad winding. Four E02 OBJ outputs match exact bytes; invalid geometry/limits tests pass. |
|
| 10 |
| 4 | P6.26 | CLI OBJ with loss acknowledgement | Complete: `--to obj`, grouped preflight losses and explicit `--allow-lossy`; missing computed data remains blocking. Existing atomic publication is reused. CLI acknowledgement/preflight/regression tests pass. |
|
| 11 |
-
| 5 | P5.15 | Geometry primitives/bounds |
|
| 12 |
| 6 | P5.16 | Convex brush contours | Pending |
|
| 13 |
| 7 | P5.17 | Patch9 tessellation | Pending |
|
| 14 |
| 8 | P5.18 | Computed world pipeline/CLI | Pending |
|
|
|
|
| 8 |
| 2 | P6.24 | Explicit Latin-1 TXT input | Complete: ISO-8859-1 API and `--text-encoding latin1`, no guessing, conflicting UTF-8 BOM rejected; transcoded bytes and scratch ownership are bounded. UTF-8 remains default and output. Encoding and CLI unit suites pass. |
|
| 9 |
| 3 | P6.25 | Bounded computed OBJ writer | Complete: explicit contours and patch grids, stable per-brush exact position deduplication, global 1-based indices and quad winding. Four E02 OBJ outputs match exact bytes; invalid geometry/limits tests pass. |
|
| 10 |
| 4 | P6.26 | CLI OBJ with loss acknowledgement | Complete: `--to obj`, grouped preflight losses and explicit `--allow-lossy`; missing computed data remains blocking. Existing atomic publication is reused. CLI acknowledgement/preflight/regression tests pass. |
|
| 11 |
+
| 5 | P5.15 | Geometry primitives/bounds | Complete: f64 vector helpers, checked unit plane equations/triple intersections, f32 boundary checks, MAP axes and AABBs. Analytic/degenerate/scaled-basis tests pass; native epsilon/interior policy documented separately from legacy parity. |
|
| 12 |
| 6 | P5.16 | Convex brush contours | Pending |
|
| 13 |
| 7 | P5.17 | Patch9 tessellation | Pending |
|
| 14 |
| 8 | P5.18 | Computed world pipeline/CLI | Pending |
|
docs/TODO.md
CHANGED
|
@@ -126,6 +126,8 @@ Each task ID is stable. Split tasks into child IDs as evidence grows. Record dep
|
|
| 126 |
- [ ] P5.13 Build analytic cases: boxes, wedges, slivers, coplanar/parallel planes, huge/tiny coordinates, reflected solids, tangent rays, patch seams, invalid brushes, and overlapping selections.
|
| 127 |
- [ ] P5.14 Implement and validate a headless Rust geometry kernel with property/metamorphic tests, differential fixtures, deterministic topology, and bounded failure behavior.
|
| 128 |
|
|
|
|
|
|
|
| 129 |
## P6 — File formats and converters
|
| 130 |
|
| 131 |
**Depends on:** P3 oracle where available, P5 document model. **Exit:** standalone format specifications, declared compatibility subsets, fixture corpus, and native codecs.
|
|
|
|
| 126 |
- [ ] P5.13 Build analytic cases: boxes, wedges, slivers, coplanar/parallel planes, huge/tiny coordinates, reflected solids, tangent rays, patch seams, invalid brushes, and overlapping selections.
|
| 127 |
- [ ] P5.14 Implement and validate a headless Rust geometry kernel with property/metamorphic tests, differential fixtures, deterministic topology, and bounded failure behavior.
|
| 128 |
|
| 129 |
+
- [x] P5.15 Implement native checked geometry primitives, plane equations/intersections, numeric policy and bounds ([GEOMETRY.md](GEOMETRY.md)).
|
| 130 |
+
|
| 131 |
## P6 — File formats and converters
|
| 132 |
|
| 133 |
**Depends on:** P3 oracle where available, P5 document model. **Exit:** standalone format specifications, declared compatibility subsets, fixture corpus, and native codecs.
|