nmarshall commited on
Commit
ab47e29
·
1 Parent(s): 4536b63

geometry: compute worlds

Browse files
Cargo.lock CHANGED
@@ -28,6 +28,7 @@ dependencies = [
28
  "mb3-dat",
29
  "mb3-obj",
30
  "mb3-txt",
 
31
  ]
32
 
33
  [[package]]
 
28
  "mb3-dat",
29
  "mb3-obj",
30
  "mb3-txt",
31
+ "nanobrush-geometry",
32
  ]
33
 
34
  [[package]]
crates/nanobrush-cli/Cargo.toml CHANGED
@@ -16,5 +16,7 @@ mb3-txt = { path = "../mb3-txt" }
16
 
17
  mb3-obj = { path = "../mb3-obj" }
18
 
 
 
19
  [lints]
20
  workspace = true
 
16
 
17
  mb3-obj = { path = "../mb3-obj" }
18
 
19
+ nanobrush-geometry = { path = "../nanobrush-geometry" }
20
+
21
  [lints]
22
  workspace = true
crates/nanobrush-cli/src/lib.rs CHANGED
@@ -8,7 +8,7 @@ use std::io::{self, BufRead, BufReader, Write};
8
  use std::path::{Path, PathBuf};
9
  use std::sync::atomic::{AtomicU64, Ordering};
10
 
11
- const HELP: &str = "nanobrush preflight INPUT --to txt|dat-201501280|dat-201512200|obj [LIMITS]\nnanobrush inspect INPUT [--from auto|txt|dat] [LIMITS]\nnanobrush validate INPUT [--from auto|txt|dat] [LIMITS]\nnanobrush convert INPUT OUTPUT --to txt|dat-201501280|dat-201512200|obj [--from auto|txt|dat] [--force] [LIMITS]\nLIMITS: --max-bytes N --max-items N --max-string-bytes N --max-owned-bytes N\nOBJ requires --allow-lossy when editable data is discarded.\nTXT option: --text-encoding utf8|latin1 (default utf8).\nUse -- before positional paths beginning with a dash. TXT input is UTF-8; output has a BOM.\nExit: 0 success/help, 1 invalid data or I/O failure, 2 usage error.\n";
12
  #[derive(Debug)]
13
  pub struct Error {
14
  pub usage: bool,
@@ -57,6 +57,7 @@ struct Options {
57
  to: Option<To>,
58
  force: bool,
59
  allow_lossy: bool,
 
60
  limits: Limits,
61
  encoding: mb3_txt::Encoding,
62
  }
@@ -84,6 +85,7 @@ fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Option<Options>> {
84
  to: None,
85
  force: false,
86
  allow_lossy: false,
 
87
  limits: Limits::default(),
88
  encoding: mb3_txt::Encoding::Utf8,
89
  };
@@ -102,6 +104,10 @@ fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Option<Options>> {
102
  if !seen.insert(flag.to_owned()) {
103
  return Err(usage(format!("duplicate option {flag}")));
104
  }
 
 
 
 
105
  if flag == "--allow-lossy" {
106
  options.allow_lossy = true;
107
  continue;
@@ -191,6 +197,9 @@ fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Option<Options>> {
191
  {
192
  return Err(usage("--allow-lossy requires convert --to obj"));
193
  }
 
 
 
194
  Ok(Some(options))
195
  }
196
  struct Input {
@@ -325,7 +334,18 @@ pub fn run(args: impl IntoIterator<Item = OsString>, stdout: &mut dyn Write) ->
325
  stdout.write_all(HELP.as_bytes())?;
326
  return Ok(());
327
  };
328
- let input = load(&o.paths[0], o.from, o.encoding, &o.limits)?;
 
 
 
 
 
 
 
 
 
 
 
329
  match o.command.as_str() {
330
  "preflight" => {
331
  let target = match o.to.ok_or_else(|| usage("missing --to"))? {
 
8
  use std::path::{Path, PathBuf};
9
  use std::sync::atomic::{AtomicU64, Ordering};
10
 
11
+ const HELP: &str = "nanobrush preflight INPUT --to txt|dat-201501280|dat-201512200|obj [LIMITS]\nnanobrush inspect INPUT [--from auto|txt|dat] [LIMITS]\nnanobrush validate INPUT [--from auto|txt|dat] [LIMITS]\nnanobrush convert INPUT OUTPUT --to txt|dat-201501280|dat-201512200|obj [--from auto|txt|dat] [--force] [LIMITS]\nLIMITS: --max-bytes N --max-items N --max-string-bytes N --max-owned-bytes N\nUse --compute with convert/preflight to regenerate native geometry.\nOBJ requires --allow-lossy when editable data is discarded.\nTXT option: --text-encoding utf8|latin1 (default utf8).\nUse -- before positional paths beginning with a dash. TXT input is UTF-8; output has a BOM.\nExit: 0 success/help, 1 invalid data or I/O failure, 2 usage error.\n";
12
  #[derive(Debug)]
13
  pub struct Error {
14
  pub usage: bool,
 
57
  to: Option<To>,
58
  force: bool,
59
  allow_lossy: bool,
60
+ compute: bool,
61
  limits: Limits,
62
  encoding: mb3_txt::Encoding,
63
  }
 
85
  to: None,
86
  force: false,
87
  allow_lossy: false,
88
+ compute: false,
89
  limits: Limits::default(),
90
  encoding: mb3_txt::Encoding::Utf8,
91
  };
 
104
  if !seen.insert(flag.to_owned()) {
105
  return Err(usage(format!("duplicate option {flag}")));
106
  }
107
+ if flag == "--compute" {
108
+ options.compute = true;
109
+ continue;
110
+ }
111
  if flag == "--allow-lossy" {
112
  options.allow_lossy = true;
113
  continue;
 
197
  {
198
  return Err(usage("--allow-lossy requires convert --to obj"));
199
  }
200
+ if options.compute && !matches!(options.command.as_str(), "convert" | "preflight") {
201
+ return Err(usage("--compute requires convert or preflight"));
202
+ }
203
  Ok(Some(options))
204
  }
205
  struct Input {
 
334
  stdout.write_all(HELP.as_bytes())?;
335
  return Ok(());
336
  };
337
+ let mut input = load(&o.paths[0], o.from, o.encoding, &o.limits)?;
338
+ if o.compute {
339
+ let limits = nanobrush_geometry::ComputeLimits {
340
+ max_vertices: o.limits.max_items.min(100_000),
341
+ max_owned_bytes: o.limits.max_owned_bytes.min(128 * 1024 * 1024),
342
+ ..Default::default()
343
+ };
344
+ input.world =
345
+ nanobrush_geometry::world::compute(&input.world, Default::default(), limits, o.limits)
346
+ .map_err(|e| fail(e.to_string()))?
347
+ .0;
348
+ }
349
  match o.command.as_str() {
350
  "preflight" => {
351
  let target = match o.to.ok_or_else(|| usage("missing --to"))? {
crates/nanobrush-cli/src/tests.rs CHANGED
@@ -231,3 +231,37 @@ fn obj_acknowledgement_and_preflight() {
231
  .contains("editable_geometry")
232
  );
233
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  .contains("editable_geometry")
232
  );
233
  }
234
+
235
+ #[test]
236
+ fn compute_cube_to_obj() {
237
+ let d = Dir::new();
238
+ let mut w = World::default();
239
+ let mut planes = Vec::new();
240
+ for axis in 0..3 {
241
+ for sign in [-1., 1.] {
242
+ let mut p = mb3_dat::Plane::default();
243
+ p.p0[axis] = sign;
244
+ p.u[(axis + 1) % 3] = 1.;
245
+ p.v[(axis + 2) % 3] = sign;
246
+ planes.push(p);
247
+ }
248
+ }
249
+ w.brushes.push(mb3_dat::Brush { planes });
250
+ let bytes = mb3_txt::encode(&w, &Limits::default()).unwrap();
251
+ let input = d.file("cube.txt", &bytes);
252
+ let output = d.0.join("cube.obj");
253
+ call(&[
254
+ "convert".as_ref(),
255
+ input.as_os_str(),
256
+ output.as_os_str(),
257
+ "--to".as_ref(),
258
+ "obj".as_ref(),
259
+ "--compute".as_ref(),
260
+ "--allow-lossy".as_ref(),
261
+ ])
262
+ .unwrap();
263
+ let obj = fs::read_to_string(output).unwrap();
264
+ assert_eq!(obj.lines().filter(|l| l.starts_with("v ")).count(), 8);
265
+ assert_eq!(obj.lines().filter(|l| l.starts_with("f ")).count(), 6);
266
+ assert_eq!(fs::read(input).unwrap(), bytes);
267
+ }
crates/nanobrush-geometry/src/lib.rs CHANGED
@@ -103,3 +103,4 @@ impl Budget {
103
  }
104
  }
105
  pub mod patch;
 
 
103
  }
104
  }
105
  pub mod patch;
106
+ pub mod world;
crates/nanobrush-geometry/src/world.rs ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::*;
2
+ use mb3_dat::{Brush, Limits, Patch9, PatchComputed, Plane, Property, Uv, World};
3
+ struct CopyBudget {
4
+ items: usize,
5
+ bytes: usize,
6
+ limits: Limits,
7
+ }
8
+ impl CopyBudget {
9
+ fn bytes(&mut self, n: usize) -> Result<()> {
10
+ self.bytes = self.bytes.checked_add(n).ok_or(Error::Overflow)?;
11
+ if self.bytes > self.limits.max_owned_bytes {
12
+ Err(Error::Limit)
13
+ } else {
14
+ Ok(())
15
+ }
16
+ }
17
+ fn vec<T>(&mut self, n: usize) -> Result<Vec<T>> {
18
+ self.items = self.items.checked_add(n).ok_or(Error::Overflow)?;
19
+ if self.items > self.limits.max_items {
20
+ return Err(Error::Limit);
21
+ }
22
+ self.bytes(
23
+ n.checked_mul(std::mem::size_of::<T>())
24
+ .ok_or(Error::Overflow)?,
25
+ )?;
26
+ alloc(n)
27
+ }
28
+ fn string(&mut self, s: &str) -> Result<String> {
29
+ if s.len() > self.limits.max_string_bytes {
30
+ return Err(Error::Limit);
31
+ }
32
+ self.bytes(s.len())?;
33
+ let mut out = String::new();
34
+ out.try_reserve_exact(s.len())
35
+ .map_err(|_| Error::Allocation)?;
36
+ out.push_str(s);
37
+ Ok(out)
38
+ }
39
+ fn names(&mut self, names: &[String]) -> Result<Vec<String>> {
40
+ let mut out = self.vec(names.len())?;
41
+ for s in names {
42
+ out.push(self.string(s)?);
43
+ }
44
+ Ok(out)
45
+ }
46
+ fn base(&mut self, w: &World) -> Result<World> {
47
+ let string_properties = self.names(&w.string_properties)?;
48
+ let uv_properties = self.names(&w.uv_properties)?;
49
+ let mut brushes = self.vec(w.brushes.len())?;
50
+ for b in &w.brushes {
51
+ let mut planes = self.vec(b.planes.len())?;
52
+ for p in &b.planes {
53
+ let mut strings = self.vec(p.strings.len())?;
54
+ for s in &p.strings {
55
+ strings.push(Property {
56
+ index: s.index,
57
+ value: self.string(&s.value)?,
58
+ });
59
+ }
60
+ let mut uv: Vec<Property<Uv>> = self.vec(p.uv.len())?;
61
+ uv.extend(p.uv.iter().cloned());
62
+ planes.push(Plane {
63
+ p0: p.p0,
64
+ u: p.u,
65
+ v: p.v,
66
+ strings,
67
+ uv,
68
+ polygon: None,
69
+ });
70
+ }
71
+ brushes.push(Brush { planes });
72
+ }
73
+ let mut patches = self.vec(w.patches.len())?;
74
+ for p in &w.patches {
75
+ let mut anchors = self.vec(p.anchors.len())?;
76
+ anchors.extend_from_slice(&p.anchors);
77
+ patches.push(Patch9 {
78
+ anchors_u: p.anchors_u,
79
+ anchors_v: p.anchors_v,
80
+ quads_u: p.quads_u,
81
+ quads_v: p.quads_v,
82
+ anchors,
83
+ computed: None,
84
+ });
85
+ }
86
+ Ok(World {
87
+ computed: false,
88
+ string_properties,
89
+ uv_properties,
90
+ brushes,
91
+ patches,
92
+ })
93
+ }
94
+ fn vectors(&mut self, source: &[DVec3]) -> Result<Vec<mb3_dat::Vec3>> {
95
+ let mut out = self.vec(source.len())?;
96
+ for v in source {
97
+ out.push(math::to_f32(*v)?);
98
+ }
99
+ Ok(out)
100
+ }
101
+ }
102
+ /// Transactional by ownership: source is never mutated, and no partial world is
103
+ /// returned. Existing derived arrays are recomputed rather than copied.
104
+ pub fn compute(
105
+ source: &World,
106
+ policy: Policy,
107
+ limits: ComputeLimits,
108
+ codec: Limits,
109
+ ) -> Result<(World, Stats)> {
110
+ policy.validate()?;
111
+ let mut b = CopyBudget {
112
+ items: 0,
113
+ bytes: 0,
114
+ limits: Limits {
115
+ max_owned_bytes: codec.max_owned_bytes.min(limits.max_owned_bytes),
116
+ ..codec
117
+ },
118
+ };
119
+ let limits = ComputeLimits {
120
+ max_owned_bytes: b.limits.max_owned_bytes,
121
+ ..limits
122
+ };
123
+ let mut out = b.base(source)?;
124
+ let mut stats = Stats::default();
125
+ for brush in &mut out.brushes {
126
+ let remaining = remaining(limits, &stats, b.bytes)?;
127
+ let mesh = crate::brush::contours(brush, policy, remaining)?;
128
+ stats.work = stats
129
+ .work
130
+ .checked_add(mesh.stats.work)
131
+ .ok_or(Error::Overflow)?;
132
+ stats.vertices = stats
133
+ .vertices
134
+ .checked_add(mesh.stats.vertices)
135
+ .ok_or(Error::Overflow)?;
136
+ b.bytes(mesh.stats.owned)?;
137
+ for (p, polygon) in brush.planes.iter_mut().zip(&mesh.contours) {
138
+ p.polygon = Some(b.vectors(polygon)?);
139
+ }
140
+ }
141
+ for patch in &mut out.patches {
142
+ let remaining = remaining(limits, &stats, b.bytes)?;
143
+ let mesh = crate::patch::tessellate(patch, policy, remaining)?;
144
+ stats.work = stats
145
+ .work
146
+ .checked_add(mesh.stats.work)
147
+ .ok_or(Error::Overflow)?;
148
+ stats.vertices = stats
149
+ .vertices
150
+ .checked_add(mesh.stats.vertices)
151
+ .ok_or(Error::Overflow)?;
152
+ b.bytes(mesh.stats.owned)?;
153
+ patch.computed = Some(PatchComputed {
154
+ points: b.vectors(&mesh.points)?,
155
+ normals: b.vectors(&mesh.normals)?,
156
+ });
157
+ }
158
+ out.computed = true;
159
+ stats.owned = b.bytes;
160
+ Ok((out, stats))
161
+ }
162
+ fn remaining(limits: ComputeLimits, stats: &Stats, owned: usize) -> Result<ComputeLimits> {
163
+ Ok(ComputeLimits {
164
+ max_vertices: limits
165
+ .max_vertices
166
+ .checked_sub(stats.vertices)
167
+ .ok_or(Error::Limit)?,
168
+ max_work: limits
169
+ .max_work
170
+ .checked_sub(stats.work)
171
+ .ok_or(Error::Limit)?,
172
+ max_owned_bytes: limits
173
+ .max_owned_bytes
174
+ .checked_sub(owned)
175
+ .ok_or(Error::Limit)?,
176
+ ..limits
177
+ })
178
+ }
179
+ #[cfg(test)]
180
+ mod tests {
181
+ use super::*;
182
+ #[test]
183
+ fn mixed_world_roundtrip_properties_and_source_immutable() {
184
+ let mut w = World {
185
+ brushes: vec![crate::brush::cube()],
186
+ patches: vec![crate::patch::flat(3, 3)],
187
+ string_properties: vec!["name".into()],
188
+ ..World::default()
189
+ };
190
+ w.brushes[0].planes[0].strings.push(Property {
191
+ index: 0,
192
+ value: "value".into(),
193
+ });
194
+ let before = w.clone();
195
+ let (out, s) = compute(
196
+ &w,
197
+ Policy::default(),
198
+ ComputeLimits::default(),
199
+ Limits::default(),
200
+ )
201
+ .unwrap();
202
+ assert_eq!(w, before);
203
+ assert!(out.computed);
204
+ assert_eq!(s.vertices, 33);
205
+ assert_eq!(
206
+ out.brushes[0].planes[0].strings,
207
+ w.brushes[0].planes[0].strings
208
+ );
209
+ let d = mb3_dat::Document {
210
+ world: out,
211
+ version: mb3_dat::Version::V201512200,
212
+ };
213
+ assert_eq!(
214
+ mb3_dat::decode(
215
+ &mb3_dat::encode(&d, &Limits::default()).unwrap(),
216
+ &Limits::default()
217
+ )
218
+ .unwrap(),
219
+ d
220
+ );
221
+ }
222
+ #[test]
223
+ fn late_failure_and_global_budgets() {
224
+ let w = World {
225
+ brushes: vec![crate::brush::cube(), Brush { planes: vec![] }],
226
+ ..World::default()
227
+ };
228
+ let before = w.clone();
229
+ assert!(
230
+ compute(
231
+ &w,
232
+ Policy::default(),
233
+ ComputeLimits::default(),
234
+ Limits::default()
235
+ )
236
+ .is_err()
237
+ );
238
+ assert_eq!(w, before);
239
+ let w = World {
240
+ brushes: vec![crate::brush::cube(), crate::brush::cube()],
241
+ ..World::default()
242
+ };
243
+ assert!(
244
+ compute(
245
+ &w,
246
+ Policy::default(),
247
+ ComputeLimits {
248
+ max_vertices: 30,
249
+ ..Default::default()
250
+ },
251
+ Limits::default()
252
+ )
253
+ .is_err()
254
+ );
255
+ assert!(
256
+ compute(
257
+ &w,
258
+ Policy::default(),
259
+ ComputeLimits::default(),
260
+ Limits {
261
+ max_owned_bytes: 1,
262
+ ..Default::default()
263
+ }
264
+ )
265
+ .is_err()
266
+ );
267
+ }
268
+ }
docs/CLI.md CHANGED
@@ -58,3 +58,5 @@ Eight asset-free unit tests cover options, limits, inspection, both DAT directio
58
  `preflight INPUT --to FORMAT` emits schema-1 compatibility JSON without writing a document. A successful report command exits 0 even when `compatible` is false. Grouped issues cover non-ASCII DAT values, property-bearing old-DAT exports and duplicate TXT table names. Counts refer to affected names/entries, not lost bytes. Full structural/size validation remains the encoder’s responsibility.
59
 
60
  Computed OBJ export: `convert INPUT OUTPUT --to obj --allow-lossy`. `preflight INPUT --to obj` lists discarded editable geometry/properties/normals as `lossy:true`; missing computed data is blocking. Acknowledgement never overrides malformed data or limits. OBJ has no native import route.
 
 
 
58
  `preflight INPUT --to FORMAT` emits schema-1 compatibility JSON without writing a document. A successful report command exits 0 even when `compatible` is false. Grouped issues cover non-ASCII DAT values, property-bearing old-DAT exports and duplicate TXT table names. Counts refer to affected names/entries, not lost bytes. Full structural/size validation remains the encoder’s responsibility.
59
 
60
  Computed OBJ export: `convert INPUT OUTPUT --to obj --allow-lossy`. `preflight INPUT --to obj` lists discarded editable geometry/properties/normals as `lossy:true`; missing computed data is blocking. Acknowledgement never overrides malformed data or limits. OBJ has no native import route.
61
+
62
+ `convert/preflight --compute` explicitly regenerates native brush contours and Patch9 samples before export/reporting. See [native geometry policy](GEOMETRY.md); it is not a claim of editor-identical geometry. It reuses guarded publication, and geometry failure leaves both source and destination untouched.
docs/GEOMETRY.md CHANGED
@@ -17,3 +17,9 @@ Defaults cap 64 planes, 100,000 generated face vertices, 10 million work units a
17
  `patch::sample` evaluates a quadratic 3×3 tile and analytic derivatives; `tessellate` spans odd anchor grids with overlapping tiles every two anchors, U varying fastest. The evaluator uses the tensor product of three-control-point quadratic curves ([W3C quadratic curve definition](https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands)); this tensor-product choice is native policy, not recovered host implementation. Basis is `[(1-t)^2,2t(1-t),t²]`; derivatives are `[-2(1-t),2-4t,2t]`.
18
 
19
  Anchor dimensions must be odd and at least three, and quads per tile at least two, matching source API declarations. Normals use `dv×du` to agree with exported OBJ quad winding. At an interior tile boundary, the right/upper tile supplies the derivative; positional seams are shared but normals are not averaged. Degenerate tangents/normals fail rather than inventing a direction. Sample count/products, work (nine control evaluations per sample) and requested point/normal allocations are bounded. Flat XZ patches, endpoints, exact quadratic midpoints, multi-tile seams and invalid/budget cases pass. Folded/self-intersecting surfaces and legacy seam-normal parity remain open.
 
 
 
 
 
 
 
17
  `patch::sample` evaluates a quadratic 3×3 tile and analytic derivatives; `tessellate` spans odd anchor grids with overlapping tiles every two anchors, U varying fastest. The evaluator uses the tensor product of three-control-point quadratic curves ([W3C quadratic curve definition](https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands)); this tensor-product choice is native policy, not recovered host implementation. Basis is `[(1-t)^2,2t(1-t),t²]`; derivatives are `[-2(1-t),2-4t,2t]`.
18
 
19
  Anchor dimensions must be odd and at least three, and quads per tile at least two, matching source API declarations. Normals use `dv×du` to agree with exported OBJ quad winding. At an interior tile boundary, the right/upper tile supplies the derivative; positional seams are shared but normals are not averaged. Degenerate tangents/normals fail rather than inventing a direction. Sample count/products, work (nine control evaluations per sample) and requested point/normal allocations are bounded. Flat XZ patches, endpoints, exact quadratic midpoints, multi-tile seams and invalid/budget cases pass. Folded/self-intersecting surfaces and legacy seam-normal parity remain open.
20
+
21
+ ## Computed world pipeline
22
+
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.
docs/PROGRESS_2026-10-26.md CHANGED
@@ -11,6 +11,6 @@ Scope: extend native headless formats and geometry. Each stage is independently
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 | 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 | Pending |
15
  | 9 | P5.19 | Nearest-hit ray queries | Pending |
16
  | 10 | P5.20 | Property table edits/reindexing | Pending |
 
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 | 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 | Pending |
16
  | 10 | P5.20 | Property table edits/reindexing | Pending |
docs/TODO.md CHANGED
@@ -132,6 +132,8 @@ Each task ID is stable. Split tasks into child IDs as evidence grows. Record dep
132
 
133
  - [x] P5.17 Implement bounded native Patch9 tensor-quadratic sampling/tessellation and analytic normals ([GEOMETRY.md](GEOMETRY.md)).
134
 
 
 
135
  ## P6 — File formats and converters
136
 
137
  **Depends on:** P3 oracle where available, P5 document model. **Exit:** standalone format specifications, declared compatibility subsets, fixture corpus, and native codecs.
 
132
 
133
  - [x] P5.17 Implement bounded native Patch9 tensor-quadratic sampling/tessellation and analytic normals ([GEOMETRY.md](GEOMETRY.md)).
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.