@bldrs-ai/conway 1.426.1283 → 1.428.1289

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 +175 -21
  4. package/compiled/examples/validator-bundled.cjs +1 -1
  5. package/compiled/src/AP214E3_2010/ap214_geometry_extraction.d.ts +69 -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 +187 -31
  8. package/compiled/src/AP214E3_2010/ap214_model_geometry.d.ts +6 -0
  9. package/compiled/src/AP214E3_2010/ap214_model_geometry.d.ts.map +1 -1
  10. package/compiled/src/AP214E3_2010/ap214_model_geometry.js +9 -0
  11. package/compiled/src/compat/web-ifc/ap214_streamed_open.test.d.ts +2 -0
  12. package/compiled/src/compat/web-ifc/ap214_streamed_open.test.d.ts.map +1 -0
  13. package/compiled/src/compat/web-ifc/ap214_streamed_open.test.js +190 -0
  14. package/compiled/src/compat/web-ifc/ifc_api_model_passthrough_factory.d.ts.map +1 -1
  15. package/compiled/src/compat/web-ifc/ifc_api_model_passthrough_factory.js +19 -0
  16. package/compiled/src/compat/web-ifc/ifc_api_preview_channel.test.js +2 -2
  17. package/compiled/src/compat/web-ifc/ifc_api_proxy_ap214.d.ts +105 -0
  18. package/compiled/src/compat/web-ifc/ifc_api_proxy_ap214.d.ts.map +1 -1
  19. package/compiled/src/compat/web-ifc/ifc_api_proxy_ap214.js +360 -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 +2 -2
  22. package/compiled/src/compat/web-ifc/streamed_preview_channel.d.ts +111 -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 +176 -106
  25. package/compiled/src/version/version.js +1 -1
  26. package/compiled/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +1 -1
@@ -88,6 +88,7 @@ export class AP214GeometryExtraction {
88
88
  * Statistics by the loader/proxies after extraction.
89
89
  */
90
90
  this.geometryTypeCounts = new Map();
91
+ this.demandCursor_ = 0;
91
92
  this.csgMemoization = !this.lowMemoryMode;
92
93
  this.materials = model.materials;
93
94
  this.scene = new AP214SceneBuilder(model, conwayModel, this.materials);
@@ -2902,16 +2903,27 @@ export class AP214GeometryExtraction {
2902
2903
  * @return {[ExtractResult, AP214SceneBuilder]} - Enum indicating extraction result
2903
2904
  * + Geometry array
2904
2905
  */
2905
- extractAP214GeometryData(logTime = false) {
2906
- let result = ExtractResult.INCOMPLETE;
2907
- const startTime = Date.now();
2908
- // this.extractLinearScalingFactor()
2909
- const previousMemoizationState = this.model.elementMemoization;
2910
- // populate relMaterialsMap
2911
- // const relAssociatesMaterials = this.model.types(AP214RelAssociatesMaterial)
2906
+ /**
2907
+ * Prepare per-unit demand extraction (STEP demand parity phase 2)
2908
+ * without executing any geometry work: builds the assembly tree and
2909
+ * thunks exactly like the whole-model walk, but the root executions
2910
+ * are captured as an ordered list of UNITS — a childless root is one
2911
+ * unit; a root with children is flattened one level (one unit per
2912
+ * immediate child, in order, then one for the root's own items), so
2913
+ * single-root assemblies still pump progressively per part. Executing
2914
+ * every unit in order then calling {@link finishDemandExtraction}
2915
+ * reproduces the whole-model walk exactly — the classic
2916
+ * {@link extractAP214GeometryData} runs through this same path.
2917
+ * Idempotent.
2918
+ */
2919
+ // eslint-disable-next-line max-lines-per-function
2920
+ prepareDemandExtraction() {
2921
+ if (this.demandUnits_ !== void 0) {
2922
+ return;
2923
+ }
2912
2924
  const model = this.model;
2913
2925
  const treeMap = new Map();
2914
- const makeThunk = (representation, owningElementLocalID, mappedTreeNode) => {
2926
+ const makeThunk = (representation, owningElementLocalID, mappedTreeNode, slice) => {
2915
2927
  return (owningLocalID, parentTransform) => {
2916
2928
  owningLocalID ??= owningElementLocalID;
2917
2929
  const mappedItem = mappedTreeNode !== void 0 || parentTransform !== void 0;
@@ -2920,8 +2932,12 @@ export class AP214GeometryExtraction {
2920
2932
  if (parentTransform !== void 0) {
2921
2933
  this.scene.addTransform(representation.localID, parentTransform.getValues(), parentTransform, true);
2922
2934
  }
2923
- if (mappedTreeNode?.children !== void 0) {
2924
- for (const [childLocalID, childOwningLocalID, childTransform] of mappedTreeNode.children) {
2935
+ if (mappedTreeNode?.children !== void 0 &&
2936
+ (slice === void 0 || slice.childStart !== slice.childEnd)) {
2937
+ const sliceChildren = slice !== void 0 ?
2938
+ mappedTreeNode.children.slice(slice.childStart, slice.childEnd) :
2939
+ mappedTreeNode.children;
2940
+ for (const [childLocalID, childOwningLocalID, childTransform] of sliceChildren) {
2925
2941
  const mappedChild = treeMap.get(childLocalID);
2926
2942
  const enterChildStackDepth = this.scene.stackLength;
2927
2943
  const enterChildParent = this.scene.currentParent;
@@ -2971,7 +2987,8 @@ export class AP214GeometryExtraction {
2971
2987
  }
2972
2988
  }
2973
2989
  }
2974
- for (const item of representation.items) {
2990
+ const includeItems = slice?.includeItems ?? true;
2991
+ for (const item of includeItems ? representation.items : []) {
2975
2992
  try {
2976
2993
  if (item instanceof placement) {
2977
2994
  this.extractPlacement(item, mappedItem);
@@ -3010,11 +3027,19 @@ export class AP214GeometryExtraction {
3010
3027
  }
3011
3028
  };
3012
3029
  };
3013
- try {
3030
+ // Roots the whole-model walk would execute, in execution order —
3031
+ // expanded into units at the end of preparation (children arrays
3032
+ // are final only once every edge loop has run).
3033
+ const pendingRoots = [];
3034
+ {
3014
3035
  this.scene.clearParentStack();
3015
3036
  // 256 meg limit for memoization - smaller models get a big
3016
3037
  // win from memoization, but much larger models it uses far too much heap.
3017
3038
  const MEMOIZATION_THRESHOLD = 256 * 1024 * 1024;
3039
+ // Remembered for finishDemandExtraction / the classic wrapper's
3040
+ // finally — the pump spans many calls, so restoration cannot live
3041
+ // in a single method's finally anymore.
3042
+ this.demandMemoizationRestore_ = this.model.elementMemoization;
3018
3043
  if (this.lowMemoryMode || model.bufferBytesize > MEMOIZATION_THRESHOLD) {
3019
3044
  this.model.elementMemoization = false;
3020
3045
  }
@@ -3089,6 +3114,8 @@ export class AP214GeometryExtraction {
3089
3114
  sourceNode = { parents: 1 };
3090
3115
  treeMap.set(sourceID, sourceNode);
3091
3116
  sourceNode.thunk = makeThunk(sourceShape, owningLocalID, sourceNode);
3117
+ sourceNode.rep = sourceShape;
3118
+ sourceNode.owningLocalID = owningLocalID;
3092
3119
  }
3093
3120
  else {
3094
3121
  sourceNode.parents ??= 0;
@@ -3135,6 +3162,8 @@ export class AP214GeometryExtraction {
3135
3162
  ++sourceNode.parents;
3136
3163
  }
3137
3164
  sourceNode.thunk = makeThunk(sourceShape, owningLocalID, sourceNode);
3165
+ sourceNode.rep = sourceShape;
3166
+ sourceNode.owningLocalID = owningLocalID;
3138
3167
  let targetNode = treeMap.get(targetID);
3139
3168
  if (targetNode === void 0) {
3140
3169
  targetNode = { parents: 0, children: [[sourceID, owningLocalID, transform]] };
@@ -3166,6 +3195,8 @@ export class AP214GeometryExtraction {
3166
3195
  const mappedTreeNode = treeNode;
3167
3196
  const thunk = makeThunk(shapeRepresentation, owningElementLocalID, mappedTreeNode);
3168
3197
  mappedTreeNode.thunk = thunk;
3198
+ mappedTreeNode.rep = shapeRepresentation;
3199
+ mappedTreeNode.owningLocalID = owningElementLocalID;
3169
3200
  if (!hasMappedNode) {
3170
3201
  mappedTreeNode.processed = true;
3171
3202
  const sourceShapeContext = shapeRepresentation.context_of_items.findVariant(global_unit_assigned_context)?.units?.
@@ -3176,8 +3207,14 @@ export class AP214GeometryExtraction {
3176
3207
  scaleTransform =
3177
3208
  this.uniformScaleAffine(this.identity3DNativeMatrix, 1 / sourceUnitInM);
3178
3209
  }
3179
- // not an assembly mapped item, just extract the representation
3180
- thunk(void 0, scaleTransform);
3210
+ // not an assembly mapped item capture as a pending root
3211
+ // instead of executing inline (these nodes are childless by
3212
+ // construction: they were absent from the tree before this
3213
+ // loop, and all edges were added by the earlier loops).
3214
+ pendingRoots.push({
3215
+ node: mappedTreeNode,
3216
+ scaleTransform,
3217
+ });
3181
3218
  continue;
3182
3219
  }
3183
3220
  }
@@ -3195,12 +3232,14 @@ export class AP214GeometryExtraction {
3195
3232
  const mappedTreeNode = treeNode;
3196
3233
  const owningElementLocalID = shapeRepresentation.localID;
3197
3234
  mappedTreeNode.thunk = makeThunk(shapeRepresentation, owningElementLocalID, mappedTreeNode);
3235
+ mappedTreeNode.rep = shapeRepresentation;
3236
+ mappedTreeNode.owningLocalID = owningElementLocalID;
3198
3237
  }
3199
- // All thunks are set, now we can execute the full
3200
- // assembly tree.
3238
+ // All thunks are set capture remaining assembly-tree roots as
3239
+ // pending roots in the exact order the whole-model walk executed
3240
+ // them.
3201
3241
  for (const [sourceID, mappedNode] of treeMap.entries()) {
3202
3242
  if ((mappedNode.parents ?? 0) === 0 && mappedNode.thunk !== void 0 && mappedNode.processed !== true) {
3203
- //this.scene.clearParentStack()
3204
3243
  const shapeRepresentation = this.model.getTypedElementByLocalID(sourceID, shape_representation);
3205
3244
  const sourceShapeContext = shapeRepresentation.context_of_items.findVariant(global_unit_assigned_context)?.units?.
3206
3245
  find(unit => unit.findVariant(length_unit))?.findVariant(length_unit);
@@ -3210,30 +3249,147 @@ export class AP214GeometryExtraction {
3210
3249
  scaleTransform =
3211
3250
  this.uniformScaleAffine(this.identity3DNativeMatrix, 1 / sourceUnitInM);
3212
3251
  }
3213
- // not an assembly mapped item, just extract the representation
3214
- mappedNode.thunk(void 0, scaleTransform);
3252
+ pendingRoots.push({ node: mappedNode, scaleTransform });
3215
3253
  }
3216
3254
  }
3217
- // Run any deferred face tessellation across the native thread pool
3218
- // before geometry can be read or temporaries freed.
3219
- this.finalizeStagedFaces();
3220
- if (RegressionCaptureState.memoization !== MemoizationCapture.FULL) {
3221
- this.model.geometry.deleteTemporaries();
3255
+ // Expand pending roots into ordered execution units: childless
3256
+ // roots run their original thunk whole; roots with children are
3257
+ // flattened one level — one unit per immediate child (in order),
3258
+ // then one for the root's own representation items, matching the
3259
+ // thunk body's children-then-items order exactly.
3260
+ const units = [];
3261
+ for (const root of pendingRoots) {
3262
+ const node = root.node;
3263
+ const scaleTransform = root.scaleTransform;
3264
+ const childCount = node.children?.length ?? 0;
3265
+ if (childCount === 0 || node.rep === void 0) {
3266
+ units.push(() => node.thunk(void 0, scaleTransform));
3267
+ continue;
3268
+ }
3269
+ for (let child = 0; child < childCount; ++child) {
3270
+ const sliced = makeThunk(node.rep, node.owningLocalID, node, { childStart: child, childEnd: child + 1, includeItems: false });
3271
+ units.push(() => sliced(void 0, scaleTransform));
3272
+ }
3273
+ const itemsOnly = makeThunk(node.rep, node.owningLocalID, node, { childStart: 0, childEnd: 0, includeItems: true });
3274
+ units.push(() => itemsOnly(void 0, scaleTransform));
3275
+ }
3276
+ this.demandUnits_ = units;
3277
+ }
3278
+ }
3279
+ /**
3280
+ * Total demand units (see prepareDemandExtraction).
3281
+ *
3282
+ * @return {number} The unit count (0 before preparation).
3283
+ */
3284
+ get demandUnitCount() {
3285
+ return this.demandUnits_?.length ?? 0;
3286
+ }
3287
+ /**
3288
+ * Units already executed by extractDemandUnitBatch.
3289
+ *
3290
+ * @return {number} The cursor.
3291
+ */
3292
+ get demandUnitCursor() {
3293
+ return this.demandCursor_;
3294
+ }
3295
+ /**
3296
+ * Advance the unit cursor WITHOUT executing (parse-time preview
3297
+ * generations resume a global ordinal on a fresh prefix extraction —
3298
+ * earlier units were already emitted from earlier generations).
3299
+ *
3300
+ * @param count Units to skip.
3301
+ */
3302
+ skipDemandUnits(count) {
3303
+ const units = this.demandUnits_;
3304
+ if (units === void 0) {
3305
+ return;
3306
+ }
3307
+ this.demandCursor_ = Math.min(this.demandCursor_ + Math.max(count, 0), units.length);
3308
+ }
3309
+ /**
3310
+ * Execute the next `count` demand units (STEP demand parity phase 2)
3311
+ * — the per-unit pump behind the shim's ExtractGeometryBatch for
3312
+ * AP214 models. Staged face tessellation is finalized after every
3313
+ * batch so captured geometry is complete and readable.
3314
+ *
3315
+ * @param count Max units to execute this call (min 1).
3316
+ * @return {number} Units actually executed.
3317
+ */
3318
+ extractDemandUnitBatch(count) {
3319
+ const units = this.demandUnits_;
3320
+ if (units === void 0) {
3321
+ return 0;
3322
+ }
3323
+ const end = Math.min(this.demandCursor_ + Math.max(count, 1), units.length);
3324
+ let executed = 0;
3325
+ for (; this.demandCursor_ < end; ++this.demandCursor_) {
3326
+ try {
3327
+ units[this.demandCursor_]();
3328
+ ++executed;
3222
3329
  }
3223
- result = ExtractResult.COMPLETE;
3224
- // free buffer at the end if you don't need it anymore
3225
- if (this.pointBuffer?.pointer) {
3226
- this.wasmModule._free(this.pointBuffer.pointer);
3227
- this.pointBuffer = null;
3330
+ catch (ex) {
3331
+ if (ex instanceof Error) {
3332
+ Logger.error(`Error processing demand unit: \n\t${ex.name}\n\t${ex.message}`);
3333
+ }
3334
+ else {
3335
+ Logger.error(`Unknown exception processing demand unit (${ex})`);
3336
+ }
3337
+ }
3338
+ }
3339
+ // Deferred face tessellation must land before geometry is read.
3340
+ this.finalizeStagedFaces();
3341
+ return executed;
3342
+ }
3343
+ /**
3344
+ * Complete a demand extraction after every unit has run: final staged
3345
+ * tessellation, temporaries cleanup, point-buffer release and
3346
+ * memoization restore — the tail the whole-model walk runs.
3347
+ *
3348
+ * @return {[ExtractResult, AP214SceneBuilder, AP214ProductShapeMap]}
3349
+ * The completed extraction result.
3350
+ */
3351
+ finishDemandExtraction() {
3352
+ this.finalizeStagedFaces();
3353
+ if (RegressionCaptureState.memoization !== MemoizationCapture.FULL) {
3354
+ this.model.geometry.deleteTemporaries();
3355
+ }
3356
+ if (this.pointBuffer?.pointer) {
3357
+ this.wasmModule._free(this.pointBuffer.pointer);
3358
+ this.pointBuffer = null;
3359
+ }
3360
+ if (this.demandMemoizationRestore_ !== void 0) {
3361
+ this.model.elementMemoization = this.demandMemoizationRestore_;
3362
+ this.demandMemoizationRestore_ = void 0;
3363
+ }
3364
+ return [ExtractResult.COMPLETE, this.scene, this.productShapeMap];
3365
+ }
3366
+ /**
3367
+ * Whole-model extraction: prepare + run every demand unit + finish —
3368
+ * a single code path with the per-unit pump, so pumped and classic
3369
+ * extractions are identical by construction.
3370
+ *
3371
+ * @param logTime Unused (kept for call compatibility).
3372
+ * @return {[ExtractResult, AP214SceneBuilder, AP214ProductShapeMap]}
3373
+ * The completed extraction result.
3374
+ */
3375
+ // eslint-disable-next-line no-unused-vars
3376
+ extractAP214GeometryData(logTime = false) {
3377
+ try {
3378
+ this.prepareDemandExtraction();
3379
+ while (this.demandCursor_ < this.demandUnitCount) {
3380
+ this.extractDemandUnitBatch(this.demandUnitCount);
3228
3381
  }
3229
- return [result, this.scene, this.productShapeMap];
3382
+ return this.finishDemandExtraction();
3230
3383
  }
3231
3384
  finally {
3232
3385
  // Ensure no staged face jobs outlive extraction (their target
3233
3386
  // geometry may be freed once the model is invalidated), even if
3234
3387
  // extraction exits through an exception.
3235
3388
  this.finalizeStagedFaces();
3236
- this.model.elementMemoization = previousMemoizationState;
3389
+ if (this.demandMemoizationRestore_ !== void 0) {
3390
+ this.model.elementMemoization = this.demandMemoizationRestore_;
3391
+ this.demandMemoizationRestore_ = void 0;
3392
+ }
3237
3393
  }
3238
3394
  }
3239
3395
  /**
@@ -13,6 +13,12 @@ export declare class AP214ModelGeometry implements ModelGeometry {
13
13
  /**
14
14
  * Add a mesh to this geometry collection.
15
15
  *
16
+ * Freezes the native float vertex mirror at add time, exactly like
17
+ * IfcModelGeometry.add (see its note): consumers read float vertices
18
+ * whose frame must match the scene transforms, which are authored
19
+ * against the geometry AS CREATED — later count-preserving native
20
+ * mutations (normalize's centering) must not shift the served frame.
21
+ *
16
22
  * @param mesh
17
23
  * @param parentLocalID
18
24
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ap214_model_geometry.d.ts","sourceRoot":"","sources":["../../../src/AP214E3_2010/ap214_model_geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAqB,MAAM,wBAAwB,CAAA;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAG7C;;GAEG;AACH,qBAAa,kBAAmB,YAAW,aAAa;IAEtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAE1D;;OAEG;IACH,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;;;;OAKG;IACI,GAAG,CAAE,IAAI,EAAE,aAAa,EAAE,aAAa,CAAC,EAAG,MAAM,GAAI,IAAI;IAkBhE;;;;OAIG;IACI,MAAM,CAAE,OAAO,EAAE,MAAM;IAe9B;;OAEG;IACI,iBAAiB,IAAI,IAAI;IAgBhC;;;;;OAKG;IACI,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IAIlE;;;;OAIG;IACI,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI/D;;;OAGG;IACI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,aAAa,CAAC;IAI3D;;;OAGG;IACI,qBAAqB,IAAI,MAAM;CAoBvC"}
1
+ {"version":3,"file":"ap214_model_geometry.d.ts","sourceRoot":"","sources":["../../../src/AP214E3_2010/ap214_model_geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAqB,MAAM,wBAAwB,CAAA;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAG7C;;GAEG;AACH,qBAAa,kBAAmB,YAAW,aAAa;IAEtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAE1D;;OAEG;IACH,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;;;;;;;;;;OAWG;IACI,GAAG,CAAE,IAAI,EAAE,aAAa,EAAE,aAAa,CAAC,EAAG,MAAM,GAAI,IAAI;IAsBhE;;;;OAIG;IACI,MAAM,CAAE,OAAO,EAAE,MAAM;IAe9B;;OAEG;IACI,iBAAiB,IAAI,IAAI;IAgBhC;;;;;OAKG;IACI,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IAIlE;;;;OAIG;IACI,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI/D;;;OAGG;IACI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,aAAa,CAAC;IAI3D;;;OAGG;IACI,qBAAqB,IAAI,MAAM;CAoBvC"}
@@ -16,10 +16,19 @@ export class AP214ModelGeometry {
16
16
  /**
17
17
  * Add a mesh to this geometry collection.
18
18
  *
19
+ * Freezes the native float vertex mirror at add time, exactly like
20
+ * IfcModelGeometry.add (see its note): consumers read float vertices
21
+ * whose frame must match the scene transforms, which are authored
22
+ * against the geometry AS CREATED — later count-preserving native
23
+ * mutations (normalize's centering) must not shift the served frame.
24
+ *
19
25
  * @param mesh
20
26
  * @param parentLocalID
21
27
  */
22
28
  add(mesh, parentLocalID) {
29
+ if (mesh.type === CanonicalMeshType.BUFFER_GEOMETRY) {
30
+ mesh.geometry.GetVertexData();
31
+ }
23
32
  this.meshes_.set(mesh.localID, mesh);
24
33
  if (parentLocalID !== void 0) {
25
34
  let children = this.children_.get(parentLocalID);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ap214_streamed_open.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ap214_streamed_open.test.d.ts","sourceRoot":"","sources":["../../../../src/compat/web-ifc/ap214_streamed_open.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,190 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ // STEP demand parity phase 1: the streamed columnar open for AP214 must
3
+ // reproduce the classic open exactly — same entities, same placed
4
+ // instances and transforms, and byte-identical GetGeometry vertex
5
+ // content (the served float frame must match the scene transforms; see
6
+ // AP214ModelGeometry.add's mirror-freeze note).
7
+ import * as fs from "fs";
8
+ import { beforeAll, describe, expect, test } from "@jest/globals";
9
+ import { IfcAPI } from "./ifc_api.js";
10
+ const SETTINGS = { COORDINATE_TO_ORIGIN: true, USE_FAST_BOOLS: true };
11
+ // Fixtures with real served geometry (as1-assembly is structure-only —
12
+ // zero meshes even classically; ap214-multibody-part's geometry ids
13
+ // serve no vertex data through GetGeometry on the classic path either).
14
+ const FIXTURES = [
15
+ "data/nema-23-76mm.step",
16
+ "data/a-gear-with-3-inch-diameter-and-20-curved-teeth.step",
17
+ ];
18
+ let api;
19
+ /**
20
+ * Capture per-entity placed instances from a model.
21
+ *
22
+ * @param modelID The open model.
23
+ * @return {Map} expressID -> {geometryExpressID, flatTransformation}[].
24
+ */
25
+ function capture(modelID) {
26
+ const instances = new Map();
27
+ api.StreamAllMeshes(modelID, (mesh) => {
28
+ const list = instances.get(mesh.expressID) ?? [];
29
+ for (let where = 0; where < mesh.geometries.size(); ++where) {
30
+ const placed = mesh.geometries.get(where);
31
+ list.push({
32
+ geometryExpressID: placed.geometryExpressID,
33
+ flatTransformation: [...placed.flatTransformation],
34
+ });
35
+ }
36
+ instances.set(mesh.expressID, list);
37
+ });
38
+ return instances;
39
+ }
40
+ beforeAll(async () => {
41
+ api = new IfcAPI();
42
+ await api.Init();
43
+ }, 120000);
44
+ describe("OpenModelStreamed on AP214 STEP input", () => {
45
+ test.each(FIXTURES)("streamed open matches classic exactly: %s", async (fixture) => {
46
+ const data = new Uint8Array(fs.readFileSync(fixture));
47
+ const classicID = api.OpenModel(data, SETTINGS);
48
+ const classic = capture(classicID);
49
+ expect(classic.size).toBeGreaterThan(0);
50
+ const streamedID = await api.OpenModelStreamed(data, SETTINGS);
51
+ expect(streamedID).toBeGreaterThanOrEqual(0);
52
+ const streamed = capture(streamedID);
53
+ expect(streamed.size).toBe(classic.size);
54
+ for (const [expressID, classicList] of classic) {
55
+ const streamedList = streamed.get(expressID);
56
+ expect(streamedList).toBeDefined();
57
+ expect(streamedList.length).toBe(classicList.length);
58
+ for (let where = 0; where < classicList.length; ++where) {
59
+ expect(streamedList[where].geometryExpressID)
60
+ .toBe(classicList[where].geometryExpressID);
61
+ expect(streamedList[where].flatTransformation)
62
+ .toEqual(classicList[where].flatTransformation);
63
+ }
64
+ }
65
+ // Vertex content: the streamed open's GetGeometry must serve
66
+ // byte-identical floats to classic for every placed geometry.
67
+ const geometryIDs = new Set();
68
+ for (const list of classic.values()) {
69
+ for (const placed of list) {
70
+ geometryIDs.add(placed.geometryExpressID);
71
+ }
72
+ }
73
+ let compared = 0;
74
+ for (const geometryID of geometryIDs) {
75
+ const classicGeometry = api.GetGeometry(classicID, geometryID);
76
+ const streamedGeometry = api.GetGeometry(streamedID, geometryID);
77
+ const classicSize = classicGeometry.GetVertexDataSize();
78
+ if (classicSize === 0) {
79
+ continue;
80
+ }
81
+ expect(streamedGeometry.GetVertexDataSize()).toBe(classicSize);
82
+ expect(api.GetVertexArray(streamedGeometry.GetVertexData(), classicSize))
83
+ .toEqual(api.GetVertexArray(classicGeometry.GetVertexData(), classicSize));
84
+ ++compared;
85
+ }
86
+ expect(compared).toBeGreaterThan(0);
87
+ // No CloseModel here: closing destroys the shared wasm processor
88
+ // and poisons later opens (pre-existing multi-open quirk).
89
+ }, 240000);
90
+ test.each(FIXTURES)("deferred unit pump to completion matches classic exactly: %s", async (fixture) => {
91
+ const data = new Uint8Array(fs.readFileSync(fixture));
92
+ const classicID = api.OpenModel(data, SETTINGS);
93
+ const classic = capture(classicID);
94
+ expect(classic.size).toBeGreaterThan(0);
95
+ const deferredID = await api.OpenModelStreamed(data, { ...SETTINGS, DEFER_GEOMETRY: true });
96
+ expect(deferredID).toBeGreaterThanOrEqual(0);
97
+ // Pump in deliberately small unit batches, accumulating delta
98
+ // emissions additively per entity.
99
+ const pumped = new Map();
100
+ let rounds = 0;
101
+ for (;;) {
102
+ const { extracted, remaining } = api.ExtractGeometryBatch(deferredID, 2, (mesh) => {
103
+ const list = pumped.get(mesh.expressID) ?? [];
104
+ for (let where = 0; where < mesh.geometries.size(); ++where) {
105
+ const placed = mesh.geometries.get(where);
106
+ list.push({
107
+ geometryExpressID: placed.geometryExpressID,
108
+ flatTransformation: [...placed.flatTransformation],
109
+ });
110
+ }
111
+ pumped.set(mesh.expressID, list);
112
+ });
113
+ ++rounds;
114
+ if (remaining === 0 && extracted === 0) {
115
+ break;
116
+ }
117
+ }
118
+ expect(rounds).toBeGreaterThan(1);
119
+ expect(pumped.size).toBe(classic.size);
120
+ for (const [expressID, classicList] of classic) {
121
+ const pumpedList = pumped.get(expressID);
122
+ expect(pumpedList).toBeDefined();
123
+ expect(pumpedList.length).toBe(classicList.length);
124
+ // Instance sets match (order may differ across unit batches):
125
+ // every classic instance has an exactly-matching pumped one.
126
+ const unmatched = pumpedList.map((entry) => entry);
127
+ for (const reference of classicList) {
128
+ const matchIndex = unmatched.findIndex((candidate) => candidate.geometryExpressID === reference.geometryExpressID &&
129
+ candidate.flatTransformation.every((value, where) => Math.abs(value - reference.flatTransformation[where]) < 1e-9));
130
+ expect(matchIndex).toBeGreaterThanOrEqual(0);
131
+ unmatched.splice(matchIndex, 1);
132
+ }
133
+ }
134
+ // No CloseModel: closing destroys the shared wasm processor and
135
+ // degrades later opens (pre-existing multi-open quirk).
136
+ }, 240000);
137
+ test("ON_PREVIEW_MESH emits classic-parity payloads for STEP (phase 3)", async () => {
138
+ // The gear parses in one cooperative slice, so the timer-driven
139
+ // channel never fires during a real open here — drive the channel
140
+ // directly over a finished sink (prefix == whole file), like the
141
+ // IFC channel test: a full drain must reproduce the classic
142
+ // instance set with the AP214 capture math (bare composition).
143
+ const fs2 = await import("fs");
144
+ const { ConwayGeometry } = await import("../../../dependencies/conway-geom/index.js");
145
+ const { buildIndexStreaming } = await import("../../step/parsing/streaming_index_builder.js");
146
+ const { ColumnarIndexSink } = await import("../../step/parsing/columnar_index.js");
147
+ const { BufferByteSource } = await import("../../step/parsing/byte_source.js");
148
+ const AP214StepParser = (await import("../../AP214E3_2010/ap214_step_parser.js")).default;
149
+ const { ap214PreviewAdapter, StreamedPreviewChannel } = await import("./streamed_preview_channel.js");
150
+ const data = new Uint8Array(fs2.readFileSync(FIXTURES[1]));
151
+ const classicID = api.OpenModel(data, SETTINGS);
152
+ const classic = capture(classicID);
153
+ let classicTotal = 0;
154
+ for (const list of classic.values()) {
155
+ classicTotal += list.length;
156
+ }
157
+ expect(classicTotal).toBeGreaterThan(0);
158
+ const conwayGeometry = new ConwayGeometry();
159
+ expect(await conwayGeometry.initialize()).toBe(true);
160
+ const sink = new ColumnarIndexSink();
161
+ const { result } = buildIndexStreaming(new BufferByteSource(data), AP214StepParser.Instance, 1024 * 1024, void 0, sink);
162
+ expect(result).toBe(0);
163
+ const payloads = [];
164
+ const channel = new StreamedPreviewChannel(data, conwayGeometry, sink, ap214PreviewAdapter(), true, (mesh) => payloads.push(mesh), void 0, void 0, 1);
165
+ channel.drainForTest();
166
+ expect(payloads.length).toBe(classicTotal);
167
+ for (const payload of payloads) {
168
+ const candidates = classic.get(payload.expressID);
169
+ expect(candidates).toBeDefined();
170
+ const matchIndex = candidates.findIndex((candidate) => candidate.geometryExpressID === payload.geometryExpressID &&
171
+ candidate.flatTransformation.every((value, where) => Math.abs(value - payload.flatTransformation[where]) < 0.000001));
172
+ expect(matchIndex).toBeGreaterThanOrEqual(0);
173
+ }
174
+ const carriers = payloads.filter((payload) => payload.vertexData !== void 0);
175
+ expect(carriers.length).toBeGreaterThan(0);
176
+ }, 240000);
177
+ test("StreamAllMeshes on a deferred STEP model drains the pump and matches classic", async () => {
178
+ const data = new Uint8Array(fs.readFileSync(FIXTURES[0]));
179
+ const classicID = api.OpenModel(data, SETTINGS);
180
+ const classic = capture(classicID);
181
+ const deferredID = await api.OpenModelStreamed(data, { ...SETTINGS, DEFER_GEOMETRY: true });
182
+ // No pump calls at all — the whole-model consumer must still get
183
+ // the complete mesh set (the proxy drains internally).
184
+ const drained = capture(deferredID);
185
+ expect(drained.size).toBe(classic.size);
186
+ for (const [expressID, classicList] of classic) {
187
+ expect(drained.get(expressID)?.length).toBe(classicList.length);
188
+ }
189
+ }, 240000);
190
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"ifc_api_model_passthrough_factory.d.ts","sourceRoot":"","sources":["../../../../src/compat/web-ifc/ifc_api_model_passthrough_factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAKpE;;GAEG;AACH,qBAAa,6BAA6B;IAExC;;;;;;;OAOG;WACW,IAAI,CACd,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,GAAG,EACf,QAAQ,CAAC,EAAE,cAAc,GAAI,sBAAsB,GAAG,SAAS;IA2EnE;;;;;;;;;;;;;;OAcG;WACiB,YAAY,CAC5B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,GAAG,EACf,QAAQ,CAAC,EAAE,cAAc,GAAI,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC;IAyB5E;;;;;;;;;;;;OAYG;WACiB,SAAS,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,GAAG,EACf,QAAQ,CAAC,EAAE,cAAc,GAAI,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC;CA+C7E"}
1
+ {"version":3,"file":"ifc_api_model_passthrough_factory.d.ts","sourceRoot":"","sources":["../../../../src/compat/web-ifc/ifc_api_model_passthrough_factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAKpE;;GAEG;AACH,qBAAa,6BAA6B;IAExC;;;;;;;OAOG;WACW,IAAI,CACd,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,GAAG,EACf,QAAQ,CAAC,EAAE,cAAc,GAAI,sBAAsB,GAAG,SAAS;IA2EnE;;;;;;;;;;;;;;OAcG;WACiB,YAAY,CAC5B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,GAAG,EACf,QAAQ,CAAC,EAAE,cAAc,GAAI,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC;IAkD5E;;;;;;;;;;;;OAYG;WACiB,SAAS,CACzB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,GAAG,EACf,QAAQ,CAAC,EAAE,cAAc,GAAI,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC;CA+C7E"}
@@ -95,6 +95,25 @@ export class IfcApiModelPassthroughFactory {
95
95
  `falling back to classic open: ${message}`);
96
96
  }
97
97
  }
98
+ // STEP demand parity: AP214 (and the AP203/AP242 fall-throughs)
99
+ // get the streamed columnar open — index columnar from birth, no
100
+ // per-record object phase — and, with DEFER_GEOMETRY, the deferred
101
+ // assembly-tree unit pump (phase 2), so STEP models stream
102
+ // progressively through ExtractGeometryBatch just like IFC.
103
+ if (modelFormat === ModelFormatType.AP214 ||
104
+ modelFormat === ModelFormatType.AP203 ||
105
+ modelFormat === ModelFormatType.AP242) {
106
+ try {
107
+ return settings?.DEFER_GEOMETRY === true ?
108
+ await IfcApiProxyAP214.createDeferred(modelID, data, wasmModule, settings) :
109
+ await IfcApiProxyAP214.createStreamed(modelID, data, wasmModule, settings);
110
+ }
111
+ catch (e) {
112
+ const message = e instanceof Error ? e.message : String(e);
113
+ Logger.warning(`Streamed STEP open failed for model ${modelID}, ` +
114
+ `falling back to classic open: ${message}`);
115
+ }
116
+ }
98
117
  return IfcApiModelPassthroughFactory.fromAsync(modelID, data, wasmModule, settings);
99
118
  }
100
119
  /**
@@ -14,7 +14,7 @@ import { ColumnarIndexSink, } from "../../step/parsing/columnar_index.js";
14
14
  import { ParseResult } from "../../step/parsing/step_parser.js";
15
15
  import { buildIndexStreaming } from "../../step/parsing/streaming_index_builder.js";
16
16
  import { IfcAPI } from "./ifc_api.js";
17
- import { StreamedPreviewChannel, } from "./streamed_preview_channel.js";
17
+ import { ifcPreviewAdapter, StreamedPreviewChannel, } from "./streamed_preview_channel.js";
18
18
  const SETTINGS = { COORDINATE_TO_ORIGIN: true, USE_FAST_BOOLS: true };
19
19
  const POOL = 1024 * 1024;
20
20
  let api;
@@ -115,7 +115,7 @@ describe("StreamedPreviewChannel", () => {
115
115
  // drain must be a complete, classic-parity extraction.
116
116
  const sink = buildSink();
117
117
  const payloads = [];
118
- const channel = new StreamedPreviewChannel(data, conwayGeometry, sink, true, (mesh) => payloads.push(mesh), void 0, void 0, 1);
118
+ const channel = new StreamedPreviewChannel(data, conwayGeometry, sink, ifcPreviewAdapter(), true, (mesh) => payloads.push(mesh), void 0, void 0, 1);
119
119
  channel.drainForTest();
120
120
  expect(payloads.length).toBe(classicTotal);
121
121
  // Every payload matches a classic instance of the same entity and