@bldrs-ai/conway 1.427.1287 → 1.429.1291

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/compiled/examples/browser-bundled.cjs +1 -1
  2. package/compiled/examples/cli-bundled.cjs +1 -1
  3. package/compiled/examples/cli-step-bundled.cjs +15 -1
  4. package/compiled/examples/validator-bundled.cjs +1 -1
  5. package/compiled/src/AP214E3_2010/ap214_geometry_extraction.d.ts +8 -0
  6. package/compiled/src/AP214E3_2010/ap214_geometry_extraction.d.ts.map +1 -1
  7. package/compiled/src/AP214E3_2010/ap214_geometry_extraction.js +14 -0
  8. package/compiled/src/compat/web-ifc/ap214_streamed_open.test.js +40 -0
  9. package/compiled/src/compat/web-ifc/ifc_api.d.ts +17 -0
  10. package/compiled/src/compat/web-ifc/ifc_api.d.ts.map +1 -1
  11. package/compiled/src/compat/web-ifc/ifc_api.js +23 -0
  12. package/compiled/src/compat/web-ifc/ifc_api_deferred_open.test.js +36 -0
  13. package/compiled/src/compat/web-ifc/ifc_api_model_passthrough.d.ts +3 -0
  14. package/compiled/src/compat/web-ifc/ifc_api_model_passthrough.d.ts.map +1 -1
  15. package/compiled/src/compat/web-ifc/ifc_api_preview_channel.test.js +2 -2
  16. package/compiled/src/compat/web-ifc/ifc_api_proxy_ap214.d.ts +13 -0
  17. package/compiled/src/compat/web-ifc/ifc_api_proxy_ap214.d.ts.map +1 -1
  18. package/compiled/src/compat/web-ifc/ifc_api_proxy_ap214.js +70 -1
  19. package/compiled/src/compat/web-ifc/ifc_api_proxy_ifc.d.ts +18 -0
  20. package/compiled/src/compat/web-ifc/ifc_api_proxy_ifc.d.ts.map +1 -1
  21. package/compiled/src/compat/web-ifc/ifc_api_proxy_ifc.js +60 -2
  22. package/compiled/src/compat/web-ifc/streamed_preview_channel.d.ts +117 -38
  23. package/compiled/src/compat/web-ifc/streamed_preview_channel.d.ts.map +1 -1
  24. package/compiled/src/compat/web-ifc/streamed_preview_channel.js +224 -105
  25. package/compiled/src/version/version.js +1 -1
  26. package/compiled/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +1 -1
@@ -2,6 +2,8 @@ import { CanonicalMeshType } from "../../index.js";
2
2
  import IfcStepModel from "../../ifc/ifc_step_model.js";
3
3
  import { IfcGeometryExtraction } from "../../ifc/ifc_geometry_extraction.js";
4
4
  import { IfcProduct } from "../../ifc/ifc4_gen/index.js";
5
+ import AP214StepModel from "../../AP214E3_2010/ap214_step_model.js";
6
+ import { AP214GeometryExtraction, } from "../../AP214E3_2010/ap214_geometry_extraction.js";
5
7
  import * as glmatrix from "gl-matrix";
6
8
  /* eslint-disable no-magic-numbers */
7
9
  /** Ms between preview pump ticks (interleaves with the parse's yields). */
@@ -15,43 +17,151 @@ const FIRST_GENERATION_MIN_RECORDS = 1024;
15
17
  /** A new generation only when the index grew this much past the previous
16
18
  * snapshot (bounds snapshot copies to O(GROWTH/(GROWTH-1)) of the file). */
17
19
  const GENERATION_GROWTH_FACTOR = 1.5;
18
- /** Default cap on products the preview channel ever extracts. Preview
20
+ /** Default cap on units the preview channel ever extracts. Preview
19
21
  * generations are throwaway extractions whose native geometry is not
20
22
  * reclaimed until page teardown (the shim never frees classic scenes
21
23
  * either — see closeModel), so the cap bounds that one-time cost. */
22
- const DEFAULT_MAX_PREVIEW_PRODUCTS = 4096;
24
+ const DEFAULT_MAX_PREVIEW_UNITS = 4096;
23
25
  /** Default cap on total payload bytes copied out to the consumer. */
24
26
  const DEFAULT_MAX_PREVIEW_BYTES = 48 * 1024 * 1024;
25
27
  const FLOATS_PER_VERTEX = 6;
26
28
  const BYTES_PER_FLOAT = 4;
27
29
  const DEFAULT_COLOR = [0.8, 0.8, 0.8, 1];
28
- // Matches the shim proxy's NormalizeMat (Z-up -> Y-up).
30
+ // Matches the shim proxies' NormalizeMat (Z-up -> Y-up).
29
31
  const NORMALIZE_MAT = glmatrix.mat4.fromValues(1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1);
30
32
  /**
31
- * Parse-time preview channel (demand/tiled rendering slice A2): while the
32
- * deferred streamed open is still parsing, periodically snapshot the growing
33
- * columnar index into a PREFIX model, extract a bounded number of products
34
- * through a throwaway extraction, and emit self-contained mesh payloads —
35
- * first pixels within the first seconds of a large parse instead of after
36
- * it.
33
+ * IFC adapter: units are IfcProducts in localID order (stable across
34
+ * prefix growth), extracted through the per-product demand seam.
37
35
  *
38
- * Preview quality, by construction: IFC relationship records
39
- * (IfcRelVoidsElement, IfcRelAssociatesMaterial, styled items) spread to the
40
- * very end of real files (measured ~92–97% depth), so a prefix extraction
41
- * can miss openings and materials. That is why these extractions are
42
- * throwaway: the durable batch pump after the parse re-extracts every
43
- * product with the full model and REPLACES the preview — final geometry
44
- * parity is untouched by this channel.
36
+ * @return {PreviewSchemaAdapter} The adapter.
37
+ */
38
+ export function ifcPreviewAdapter() {
39
+ return {
40
+ buildGeneration(data, conwaywasm, columns) {
41
+ const model = new IfcStepModel(data, columns);
42
+ const products = [];
43
+ for (const product of model.types(IfcProduct)) {
44
+ products.push(product.localID);
45
+ }
46
+ if (products.length === 0) {
47
+ return void 0;
48
+ }
49
+ const extraction = new IfcGeometryExtraction(conwaywasm, model);
50
+ return {
51
+ scene: extraction.scene,
52
+ unitCount: products.length,
53
+ get linearScalingFactor() {
54
+ return extraction.getLinearScalingFactor();
55
+ },
56
+ runUnits: (from, count) => {
57
+ const end = Math.min(from + count, products.length);
58
+ let executed = 0;
59
+ for (let where = from; where < end; ++where) {
60
+ try {
61
+ if (extraction.extractProductGeometryByLocalID(products[where])) {
62
+ ++executed;
63
+ }
64
+ }
65
+ catch {
66
+ // Unparsed forward reference — the durable pump extracts
67
+ // this product from the full model later.
68
+ }
69
+ }
70
+ return executed;
71
+ },
72
+ geometryExpressID: (geometryLocalID) => model.getElementByLocalID(geometryLocalID)?.expressID,
73
+ recenter: true,
74
+ dispose: () => {
75
+ releaseModelGeometry(model.geometry);
76
+ },
77
+ };
78
+ },
79
+ };
80
+ }
81
+ /**
82
+ * Free every buffer-geometry canonical mesh a model geometry cache
83
+ * holds (native embind objects), leaving the cache empty. Safe on an
84
+ * already-released cache.
85
+ *
86
+ * @param geometry The model geometry cache (iterable of canonical
87
+ * meshes with a delete(localID)).
88
+ */
89
+ function releaseModelGeometry(geometry) {
90
+ const localIDs = [];
91
+ for (const mesh of geometry) {
92
+ localIDs.push(mesh.localID);
93
+ }
94
+ for (const localID of localIDs) {
95
+ try {
96
+ geometry.delete(localID);
97
+ }
98
+ catch {
99
+ // Never let a free break a load — leaked is better than crashed.
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * AP214 adapter: units are assembly-tree units (see
105
+ * AP214GeometryExtraction.prepareDemandExtraction). Unit ordinals are
106
+ * only approximately stable across prefix growth (a root's child list
107
+ * can grow, shifting later ordinals) — for a preview that is
108
+ * acceptable: a shifted ordinal re-emits an instance at an identical
109
+ * placement (invisible overlap) or skips one (the durable pump renders
110
+ * it later).
111
+ *
112
+ * @return {PreviewSchemaAdapter} The adapter.
113
+ */
114
+ export function ap214PreviewAdapter() {
115
+ return {
116
+ buildGeneration(data, conwaywasm, columns) {
117
+ const model = new AP214StepModel(data, columns);
118
+ const extraction = new AP214GeometryExtraction(conwaywasm, model);
119
+ extraction.prepareDemandExtraction();
120
+ if (extraction.demandUnitCount === 0) {
121
+ return void 0;
122
+ }
123
+ return {
124
+ scene: extraction.scene,
125
+ unitCount: extraction.demandUnitCount,
126
+ get linearScalingFactor() {
127
+ return extraction.getLinearScalingFactor();
128
+ },
129
+ runUnits: (from, count) => {
130
+ if (extraction.demandUnitCursor < from) {
131
+ extraction.skipDemandUnits(from - extraction.demandUnitCursor);
132
+ }
133
+ return extraction.extractDemandUnitBatch(count);
134
+ },
135
+ geometryExpressID: (geometryLocalID) => model.getElementByLocalID(geometryLocalID)?.expressID,
136
+ recenter: false,
137
+ dispose: () => {
138
+ releaseModelGeometry(model.geometry);
139
+ },
140
+ };
141
+ },
142
+ };
143
+ }
144
+ /**
145
+ * Parse-time preview channel (demand/tiled rendering slice A2): while a
146
+ * deferred streamed open is still parsing, periodically snapshot the
147
+ * growing columnar index into a PREFIX model, extract a bounded number of
148
+ * units through a throwaway extraction, and emit self-contained mesh
149
+ * payloads — first pixels within the first seconds of a large parse
150
+ * instead of after it. Schema knowledge lives in the
151
+ * {@link PreviewSchemaAdapter}; the channel owns scheduling, generations,
152
+ * watermarks, payload copies, caps and the coordination pin.
45
153
  *
46
- * Scheduling: the cooperative parse yields to the event loop via macrotasks
47
- * every ~50ms; each pump tick runs in one of those gaps under a hard time
48
- * budget, so the parse keeps the bulk of the main thread.
154
+ * Preview quality, by construction: relationship records (IFC voids,
155
+ * materials, styled items) spread to the very end of real files
156
+ * (measured ~92–97% depth), so a prefix extraction can miss
157
+ * openings and materials. That is why these extractions are throwaway:
158
+ * the durable batch pump after the parse re-extracts every unit with the
159
+ * full model and REPLACES the preview — final geometry parity is
160
+ * untouched by this channel.
49
161
  *
50
- * Product cursor: top-level localIDs are stable across snapshots (dense
51
- * parse order) and `types(IfcProduct)` iterates in localID order, so a
52
- * single ordinal cursor advances across generations without re-extracting.
53
- * Products whose extraction throws mid-parse (unparsed forward references)
54
- * are skipped — the durable pump extracts them correctly later.
162
+ * Scheduling: the cooperative parse yields to the event loop via
163
+ * macrotasks every ~50ms; each pump tick runs in one of those gaps under
164
+ * a hard time budget, so the parse keeps the bulk of the main thread.
55
165
  */
56
166
  export class StreamedPreviewChannel {
57
167
  /**
@@ -60,27 +170,30 @@ export class StreamedPreviewChannel {
60
170
  * — ticks run between parse yields, never concurrently with the durable
61
171
  * extraction, which is created after the parse completes).
62
172
  * @param sink The live columnar sink the streamed parse is filling.
173
+ * @param adapter The schema adapter building prefix generations.
63
174
  * @param coordinateToOrigin The open's COORDINATE_TO_ORIGIN setting.
64
175
  * @param onMesh Consumer callback for each preview payload.
65
- * @param maxProducts Cap on products ever preview-extracted.
176
+ * @param maxUnits Cap on units ever preview-extracted.
66
177
  * @param maxBytes Cap on total payload bytes copied out.
67
178
  * @param firstGenerationMinRecords Records required before the first
68
179
  * snapshot (tests lower it for tiny fixtures).
69
180
  */
70
- constructor(data, conwaywasm, sink, coordinateToOrigin, onMesh, maxProducts = DEFAULT_MAX_PREVIEW_PRODUCTS, maxBytes = DEFAULT_MAX_PREVIEW_BYTES, firstGenerationMinRecords = FIRST_GENERATION_MIN_RECORDS) {
181
+ constructor(data, conwaywasm, sink, adapter, coordinateToOrigin, onMesh, maxUnits = DEFAULT_MAX_PREVIEW_UNITS, maxBytes = DEFAULT_MAX_PREVIEW_BYTES, firstGenerationMinRecords = FIRST_GENERATION_MIN_RECORDS) {
71
182
  this.data = data;
72
183
  this.conwaywasm = conwaywasm;
73
184
  this.sink = sink;
185
+ this.adapter = adapter;
74
186
  this.coordinateToOrigin = coordinateToOrigin;
75
187
  this.onMesh = onMesh;
76
- this.maxProducts = maxProducts;
188
+ this.maxUnits = maxUnits;
77
189
  this.maxBytes = maxBytes;
78
190
  this.firstGenerationMinRecords = firstGenerationMinRecords;
79
191
  this.stopped_ = false;
80
- this.emittedProducts_ = 0;
192
+ this.emittedUnits_ = 0;
81
193
  this.emittedBytes_ = 0;
82
- /** Ordinal cursor into the (append-stable) product list. */
83
- this.productOrdinal_ = 0;
194
+ /** Ordinal cursor into the unit list (see the adapter's stability
195
+ * notes). */
196
+ this.unitOrdinal_ = 0;
84
197
  /** Geometry expressIDs whose payload has been emitted (cross-generation
85
198
  * dedup for mapped/shared geometry). */
86
199
  this.emittedGeometry_ = new Set();
@@ -101,10 +214,20 @@ export class StreamedPreviewChannel {
101
214
  clearTimeout(this.timer_);
102
215
  this.timer_ = void 0;
103
216
  }
217
+ // Payloads are copies — a stopped channel's throwaway scenes hold
218
+ // nothing anyone can reference. Free them so repeated loads in one
219
+ // tab reuse the wasm pages instead of stacking preview scenes.
220
+ try {
221
+ this.generation_?.generation.dispose();
222
+ }
223
+ catch {
224
+ // Never let a free break the open.
225
+ }
226
+ this.generation_ = void 0;
104
227
  }
105
228
  /** True when a cap was hit and the channel retired itself early. */
106
229
  get capped() {
107
- return this.emittedProducts_ >= this.maxProducts ||
230
+ return this.emittedUnits_ >= this.maxUnits ||
108
231
  this.emittedBytes_ >= this.maxBytes;
109
232
  }
110
233
  // eslint-disable-next-line require-jsdoc
@@ -126,7 +249,7 @@ export class StreamedPreviewChannel {
126
249
  }, TICK_INTERVAL_MS);
127
250
  }
128
251
  /**
129
- * One pump tick: ensure a generation with pending products exists, then
252
+ * One pump tick: ensure a generation with pending units exists, then
130
253
  * extract + capture under the time budget.
131
254
  */
132
255
  tick_() {
@@ -134,87 +257,81 @@ export class StreamedPreviewChannel {
134
257
  if (!this.ensureGeneration_()) {
135
258
  return;
136
259
  }
137
- const generation = this.generation_;
138
- const { extraction, products } = generation;
260
+ const active = this.generation_;
261
+ const { generation } = active;
139
262
  let extractedThisTick = 0;
140
- while (generation.nextIndex < products.length &&
141
- this.emittedProducts_ + extractedThisTick < this.maxProducts &&
263
+ while (this.unitOrdinal_ < generation.unitCount &&
264
+ this.emittedUnits_ + extractedThisTick < this.maxUnits &&
142
265
  Date.now() < deadline) {
143
- const localID = products[generation.nextIndex++];
144
- ++this.productOrdinal_;
145
- try {
146
- if (extraction.extractProductGeometryByLocalID(localID)) {
147
- ++extractedThisTick;
148
- }
149
- }
150
- catch {
151
- // Unparsed forward reference (or any other mid-parse gap): skip.
152
- // The durable pump extracts this product from the full model.
153
- }
266
+ const executed = generation.runUnits(this.unitOrdinal_, 1);
267
+ ++this.unitOrdinal_;
268
+ extractedThisTick += executed;
154
269
  }
155
270
  if (extractedThisTick > 0) {
156
271
  this.captureNewInstances_();
157
- this.emittedProducts_ += extractedThisTick;
272
+ this.emittedUnits_ += extractedThisTick;
158
273
  }
159
274
  }
160
275
  /**
161
- * Ensure a generation with pending products: keep the current one while
162
- * it has work; otherwise snapshot a fresh prefix model once the index has
163
- * grown enough to be worth the copy.
276
+ * Ensure a generation with pending units: keep the current one while
277
+ * it has work; otherwise snapshot a fresh prefix model once the index
278
+ * has grown enough to be worth the copy.
164
279
  *
165
- * @return {boolean} True when a generation with pending products exists.
280
+ * @return {boolean} True when a generation with pending units exists.
166
281
  */
167
282
  ensureGeneration_() {
168
- const generation = this.generation_;
169
- if (generation !== void 0 && generation.nextIndex < generation.products.length) {
283
+ const active = this.generation_;
284
+ if (active !== void 0 && this.unitOrdinal_ < active.generation.unitCount) {
170
285
  return true;
171
286
  }
172
287
  const records = this.sink.topLevelCount;
173
288
  if (records < this.firstGenerationMinRecords) {
174
289
  return false;
175
290
  }
176
- if (generation !== void 0 &&
291
+ if (active !== void 0 &&
177
292
  records < this.lastSnapshotRecords_ * GENERATION_GROWTH_FACTOR) {
178
293
  return false;
179
294
  }
180
295
  const columns = this.sink.snapshot();
181
- const model = new IfcStepModel(this.data, columns);
182
- const products = [];
183
- for (const product of model.types(IfcProduct)) {
184
- products.push(product.localID);
296
+ const generation = this.adapter.buildGeneration(this.data, this.conwaywasm, columns);
297
+ this.lastSnapshotRecords_ = records;
298
+ // The outgoing generation's instances are all captured (a
299
+ // generation is only replaced once exhausted + captured) — free its
300
+ // native scenes before adopting the new one.
301
+ if (generation !== void 0 && generation.unitCount > this.unitOrdinal_) {
302
+ try {
303
+ active?.generation.dispose();
304
+ }
305
+ catch {
306
+ // Never let a free break the open.
307
+ }
185
308
  }
186
- if (products.length <= this.productOrdinal_) {
187
- // Prefix grew but produced no new products — wait for more records.
188
- this.lastSnapshotRecords_ = records;
309
+ if (generation === void 0 || generation.unitCount <= this.unitOrdinal_) {
310
+ // Prefix grew but produced no new units — wait for more records.
189
311
  return false;
190
312
  }
191
- const extraction = new IfcGeometryExtraction(this.conwaywasm, model);
192
313
  this.generation_ = {
193
- model,
194
- extraction,
195
- products,
196
- nextIndex: this.productOrdinal_,
314
+ generation,
197
315
  capturedCounts: new Map(),
198
- snapshotRecords: records,
199
316
  };
200
- this.lastSnapshotRecords_ = records;
201
317
  return true;
202
318
  }
203
319
  /**
204
320
  * Walk the current generation's scene and emit every not-yet-captured
205
- * placed instance as a payload — the preview twin of the shim's durable
206
- * delta capture, with the same placed-geometry math (normalize, scaling,
207
- * coordination) so preview and durable placements coincide, but copying
208
- * geometry OUT of the wasm heap instead of retaining native references.
321
+ * placed instance as a payload — the preview twin of the durable delta
322
+ * captures, with the same placed-geometry math per schema (recentering
323
+ * for IFC, bare composition for AP214) so preview and durable
324
+ * placements coincide, but copying geometry OUT of the wasm heap
325
+ * instead of retaining native references.
209
326
  */
210
327
  captureNewInstances_() {
211
- const generation = this.generation_;
212
- const { model, extraction, capturedCounts } = generation;
213
- const scene = extraction.scene;
214
- const linearScalingFactor = extraction.getLinearScalingFactor();
328
+ const active = this.generation_;
329
+ const { generation, capturedCounts } = active;
330
+ const { scene, recenter } = generation;
331
+ const linearScalingFactor = generation.linearScalingFactor;
215
332
  const seenThisPass = new Map();
216
- // eslint-disable-next-line no-unused-vars
217
- for (const [_, nativeTransform, geometry, material, entity] of scene.walk()) {
333
+ for (const walked of scene.walk()) {
334
+ const [, nativeTransform, geometry, material, entity] = walked;
218
335
  if (entity?.localID === void 0 || entity.expressID === void 0) {
219
336
  continue;
220
337
  }
@@ -238,14 +355,16 @@ export class StreamedPreviewChannel {
238
355
  if (this.coordinationMatrix === void 0 && this.coordinateToOrigin) {
239
356
  nativePt = geometry.geometry.getPoint(0);
240
357
  }
241
- const geomCenter = glmatrix.vec3.create();
242
- const center = geometry.geometry.normalize();
243
- geomCenter[0] = center.x;
244
- geomCenter[1] = center.y;
245
- geomCenter[2] = center.z;
246
358
  const translationMatrixGeomMin = glmatrix.mat4.create();
247
- glmatrix.mat4.fromTranslation(translationMatrixGeomMin, geomCenter);
248
- const geometryExpressID = model.getElementByLocalID(geometry.localID)?.expressID;
359
+ if (recenter) {
360
+ const geomCenter = glmatrix.vec3.create();
361
+ const center = geometry.geometry.normalize();
362
+ geomCenter[0] = center.x;
363
+ geomCenter[1] = center.y;
364
+ geomCenter[2] = center.z;
365
+ glmatrix.mat4.fromTranslation(translationMatrixGeomMin, geomCenter);
366
+ }
367
+ const geometryExpressID = generation.geometryExpressID(geometry.localID);
249
368
  const geometryTransform = nativeTransform?.getValues();
250
369
  let newMatrix;
251
370
  if (geometryTransform !== void 0) {
@@ -268,11 +387,18 @@ export class StreamedPreviewChannel {
268
387
  }
269
388
  const coordination = this.coordinationMatrix ?? glmatrix.mat4.create();
270
389
  const newTransform = glmatrix.mat4.create();
271
- const scaleMatrix = glmatrix.mat4.create();
272
- const scaleVec = glmatrix.vec3.fromValues(linearScalingFactor, linearScalingFactor, linearScalingFactor);
273
- glmatrix.mat4.scale(scaleMatrix, scaleMatrix, scaleVec);
274
- glmatrix.mat4.multiply(newTransform, coordination, newMatrix);
275
- glmatrix.mat4.multiply(newTransform, newTransform, translationMatrixGeomMin);
390
+ if (recenter) {
391
+ const scaleMatrix = glmatrix.mat4.create();
392
+ const scaleVec = glmatrix.vec3.fromValues(linearScalingFactor, linearScalingFactor, linearScalingFactor);
393
+ glmatrix.mat4.scale(scaleMatrix, scaleMatrix, scaleVec);
394
+ glmatrix.mat4.multiply(newTransform, coordination, newMatrix);
395
+ glmatrix.mat4.multiply(newTransform, newTransform, translationMatrixGeomMin);
396
+ }
397
+ else {
398
+ // Bare composition — no per-leaf recenter (AP214 instances share
399
+ // one geometry buffer, issue #308), matching its durable capture.
400
+ glmatrix.mat4.multiply(newTransform, coordination, newMatrix);
401
+ }
276
402
  const payload = {
277
403
  expressID: entity.expressID,
278
404
  geometryExpressID,
@@ -305,7 +431,7 @@ export class StreamedPreviewChannel {
305
431
  }
306
432
  /**
307
433
  * Test seam: run generation building + extraction + capture synchronously
308
- * until either every product currently in the sink is attempted or a cap
434
+ * until either every unit currently in the sink is attempted or a cap
309
435
  * is hit — what the timer-driven ticks do, without the timers.
310
436
  */
311
437
  drainForTest() {
@@ -313,22 +439,15 @@ export class StreamedPreviewChannel {
313
439
  if (this.capped || !this.ensureGeneration_()) {
314
440
  return;
315
441
  }
316
- const generation = this.generation_;
317
- if (generation.nextIndex >= generation.products.length) {
442
+ const active = this.generation_;
443
+ if (this.unitOrdinal_ >= active.generation.unitCount) {
318
444
  return;
319
445
  }
320
- const localID = generation.products[generation.nextIndex++];
321
- ++this.productOrdinal_;
322
- let extracted = false;
323
- try {
324
- extracted = generation.extraction.extractProductGeometryByLocalID(localID);
325
- }
326
- catch {
327
- // Skip — mirrors tick_.
328
- }
329
- if (extracted) {
446
+ const executed = active.generation.runUnits(this.unitOrdinal_, 1);
447
+ ++this.unitOrdinal_;
448
+ if (executed > 0) {
330
449
  this.captureNewInstances_();
331
- ++this.emittedProducts_;
450
+ ++this.emittedUnits_;
332
451
  }
333
452
  }
334
453
  }
@@ -5,5 +5,5 @@
5
5
  // only the first segment (major) is meaningful and is the one CI carries forward.
6
6
  // Must stay in `vN.N.N` shape: the CI stamp regex, scripts/updateVersion.mjs, and
7
7
  // statistics.ts all match `v\d+\.\d+\.\d+`.
8
- const versionString = 'Conway v1.427.1287';
8
+ const versionString = 'Conway v1.429.1291';
9
9
  export { versionString };