@bldrs-ai/conway 1.578.1546 → 1.579.1547

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.
@@ -30,7 +30,7 @@ var import_node_process = require("node:process");
30
30
  var readline = __toESM(require("node:readline"), 1);
31
31
 
32
32
  // compiled/src/version/version.js
33
- var versionString = "Conway v1.578.1546";
33
+ var versionString = "Conway v1.579.1547";
34
34
 
35
35
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
36
36
  var wasmType = "";
@@ -14965,7 +14965,7 @@ ${t5.join("\n")}` : "";
14965
14965
  var import_process = require("process");
14966
14966
 
14967
14967
  // compiled/src/version/version.js
14968
- var versionString = "Conway v1.578.1546";
14968
+ var versionString = "Conway v1.579.1547";
14969
14969
 
14970
14970
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
14971
14971
  function pThreadsAllowed() {
@@ -15943,7 +15943,7 @@ var ParsingBuffer = class {
15943
15943
  };
15944
15944
 
15945
15945
  // compiled/src/version/version.js
15946
- var versionString = "Conway v1.578.1546";
15946
+ var versionString = "Conway v1.579.1547";
15947
15947
 
15948
15948
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
15949
15949
  function pThreadsAllowed() {
@@ -82528,6 +82528,9 @@ var AP214SceneBuilder = class {
82528
82528
  // compiled/src/AP214E3_2010/ap214_geometry_extraction.js
82529
82529
  var MINIMUM_BOUND_POINTS = 3;
82530
82530
  var MAXIMUM_PCURVE_SPAN_SAMPLES = 256;
82531
+ var AP214_ITEMS_PER_DEMAND_UNIT = 4;
82532
+ var AP214_MAX_ITEM_UNITS_PER_REPRESENTATION = 256;
82533
+ var AP214_MAX_REPLAYED_PLACEMENTS = 8;
82531
82534
  function isWholeCurveEdge(basisCurve, edgeStart, edgeEnd) {
82532
82535
  if (!(edgeStart instanceof vertex_point) || !(edgeEnd instanceof vertex_point)) {
82533
82536
  return false;
@@ -82586,6 +82589,7 @@ var AP214GeometryExtraction = class _AP214GeometryExtraction {
82586
82589
  this.csgDepth = 0;
82587
82590
  this.freeVectorPolygonalFaces_ = [];
82588
82591
  this.geometryTypeCounts = /* @__PURE__ */ new Map();
82592
+ this.demandItemsPerUnit = AP214_ITEMS_PER_DEMAND_UNIT;
82589
82593
  this.demandCursor_ = 0;
82590
82594
  this.csgMemoization = !this.lowMemoryMode;
82591
82595
  this.quietRecoverableLogging = false;
@@ -84208,31 +84212,14 @@ var AP214GeometryExtraction = class _AP214GeometryExtraction {
84208
84212
  this.scene.addTransform(from.localID, nativeTransform.getValues(), nativeTransform, true);
84209
84213
  ++pushedTransforms;
84210
84214
  };
84211
- if (mappingTarget instanceof cartesian_transformation_operator_3d) {
84212
- const nativeCartesianTransform = this.extractCartesianTransformOperator3D(mappingTarget);
84213
- const originTransform = mappingOrigin instanceof placement ? this.extractRawPlacement(mappingOrigin) : void 0;
84214
- let combinedTransform;
84215
- if (originTransform !== void 0) {
84216
- const from2 = originTransform.invert();
84217
- const to = nativeCartesianTransform;
84218
- const params = {
84219
- useRelPlacement: true,
84220
- axis2Placement: from2,
84221
- relPlacement: to
84222
- };
84223
- combinedTransform = this.conwayModel.getLocalPlacement(params);
84224
- } else {
84225
- combinedTransform = nativeCartesianTransform;
84226
- }
84227
- pushTransform(combinedTransform);
84228
- } else if (mappingTarget instanceof placement) {
84229
- const targetTransform = this.extractRawPlacement(mappingTarget);
84230
- const originTransform = mappingOrigin instanceof placement ? this.extractRawPlacement(mappingOrigin) : void 0;
84231
- if (targetTransform !== void 0) {
84215
+ try {
84216
+ if (mappingTarget instanceof cartesian_transformation_operator_3d) {
84217
+ const nativeCartesianTransform = this.extractCartesianTransformOperator3D(mappingTarget);
84218
+ const originTransform = mappingOrigin instanceof placement ? this.extractRawPlacement(mappingOrigin) : void 0;
84232
84219
  let combinedTransform;
84233
84220
  if (originTransform !== void 0) {
84234
84221
  const from2 = originTransform.invert();
84235
- const to = targetTransform;
84222
+ const to = nativeCartesianTransform;
84236
84223
  const params = {
84237
84224
  useRelPlacement: true,
84238
84225
  axis2Placement: from2,
@@ -84240,44 +84227,64 @@ var AP214GeometryExtraction = class _AP214GeometryExtraction {
84240
84227
  };
84241
84228
  combinedTransform = this.conwayModel.getLocalPlacement(params);
84242
84229
  } else {
84243
- combinedTransform = targetTransform;
84230
+ combinedTransform = nativeCartesianTransform;
84244
84231
  }
84245
84232
  pushTransform(combinedTransform);
84233
+ } else if (mappingTarget instanceof placement) {
84234
+ const targetTransform = this.extractRawPlacement(mappingTarget);
84235
+ const originTransform = mappingOrigin instanceof placement ? this.extractRawPlacement(mappingOrigin) : void 0;
84236
+ if (targetTransform !== void 0) {
84237
+ let combinedTransform;
84238
+ if (originTransform !== void 0) {
84239
+ const from2 = originTransform.invert();
84240
+ const to = targetTransform;
84241
+ const params = {
84242
+ useRelPlacement: true,
84243
+ axis2Placement: from2,
84244
+ relPlacement: to
84245
+ };
84246
+ combinedTransform = this.conwayModel.getLocalPlacement(params);
84247
+ } else {
84248
+ combinedTransform = targetTransform;
84249
+ }
84250
+ pushTransform(combinedTransform);
84251
+ }
84246
84252
  }
84247
- }
84248
- for (const representationItem of representationMap.mapped_representation.items) {
84249
- if (representationItem instanceof mapped_item) {
84250
- this.extractMappedItem(representationItem, owningElementLocalID, parents !== void 0 ? [from, ...parents] : [from]);
84251
- } else {
84252
- this.extractRepresentationItem(representationItem, owningElementLocalID);
84253
- const styledItemLocalID_ = this.materials.styledItemMap.get(representationItem.localID);
84254
- let materialOverrideID = void 0;
84255
- if (styledItemLocalID_ !== void 0) {
84256
- const styledItem_ = this.model.getElementByLocalID(styledItemLocalID_);
84257
- this.extractStyledItem(styledItem_);
84253
+ for (const representationItem of representationMap.mapped_representation.items) {
84254
+ if (representationItem instanceof mapped_item) {
84255
+ this.extractMappedItem(representationItem, owningElementLocalID, parents !== void 0 ? [from, ...parents] : [from]);
84258
84256
  } else {
84259
- let styledItemParentLocalID = this.materials.styledItemMap.get(from.localID);
84260
- let styleParent = from;
84261
- if (parents !== void 0) {
84262
- for (const parent of parents) {
84263
- if (styledItemParentLocalID !== void 0) {
84264
- break;
84257
+ this.extractRepresentationItem(representationItem, owningElementLocalID);
84258
+ const styledItemLocalID_ = this.materials.styledItemMap.get(representationItem.localID);
84259
+ let materialOverrideID = void 0;
84260
+ if (styledItemLocalID_ !== void 0) {
84261
+ const styledItem_ = this.model.getElementByLocalID(styledItemLocalID_);
84262
+ this.extractStyledItem(styledItem_);
84263
+ } else {
84264
+ let styledItemParentLocalID = this.materials.styledItemMap.get(from.localID);
84265
+ let styleParent = from;
84266
+ if (parents !== void 0) {
84267
+ for (const parent of parents) {
84268
+ if (styledItemParentLocalID !== void 0) {
84269
+ break;
84270
+ }
84271
+ styledItemParentLocalID = this.materials.styledItemMap.get(parent.localID);
84272
+ styleParent = parent;
84265
84273
  }
84266
- styledItemParentLocalID = this.materials.styledItemMap.get(parent.localID);
84267
- styleParent = parent;
84274
+ }
84275
+ if (styledItemParentLocalID !== void 0) {
84276
+ const styledItemParent = this.model.getElementByLocalID(styledItemParentLocalID);
84277
+ this.extractStyledItem(styledItemParent, representationItem);
84278
+ materialOverrideID = styleParent.localID;
84268
84279
  }
84269
84280
  }
84270
- if (styledItemParentLocalID !== void 0) {
84271
- const styledItemParent = this.model.getElementByLocalID(styledItemParentLocalID);
84272
- this.extractStyledItem(styledItemParent, representationItem);
84273
- materialOverrideID = styleParent.localID;
84274
- }
84281
+ this.scene.addGeometry(representationItem.localID, owningElementLocalID, materialOverrideID);
84275
84282
  }
84276
- this.scene.addGeometry(representationItem.localID, owningElementLocalID, materialOverrideID);
84277
84283
  }
84278
- }
84279
- for (; pushedTransforms > 0; --pushedTransforms) {
84280
- this.scene.popTransform();
84284
+ } finally {
84285
+ for (; pushedTransforms > 0; --pushedTransforms) {
84286
+ this.scene.popTransform();
84287
+ }
84281
84288
  }
84282
84289
  }
84283
84290
  /**
@@ -85430,14 +85437,18 @@ ${error.stack} - expressID: #${face_.expressID}`);
85430
85437
  * Prepare per-unit demand extraction (STEP demand parity phase 2)
85431
85438
  * without executing any geometry work: builds the assembly tree and
85432
85439
  * thunks exactly like the whole-model walk, but the root executions
85433
- * are captured as an ordered list of UNITS — a childless root is one
85434
- * unit; a root with children is flattened one level (one unit per
85435
- * immediate child, in order, then one for the root's own items), so
85436
- * single-root assemblies still pump progressively per part. Executing
85437
- * every unit in order then calling {@link finishDemandExtraction}
85438
- * reproduces the whole-model walk exactly — the classic
85439
- * {@link extractAP214GeometryData} runs through this same path.
85440
- * Idempotent.
85440
+ * are captured as an ordered list of UNITS. The tree is flattened
85441
+ * DEPTH FIRST — one unit per child (descending into any child whose
85442
+ * subtree carries more than {@link AP214_ITEMS_PER_DEMAND_UNIT}
85443
+ * items), then one unit per range of a node's own representation
85444
+ * items — so a single-root assembly pumps progressively all the way
85445
+ * down to individual solids rather than per top-level part
85446
+ * (conway#579: Arty's silkscreen is 654 solids sitting two levels
85447
+ * under the only root, which used to make it one 22 s task).
85448
+ * Executing every unit in order then calling
85449
+ * {@link finishDemandExtraction} reproduces the whole-model walk
85450
+ * exactly — the classic {@link extractAP214GeometryData} runs through
85451
+ * this same path. Idempotent.
85441
85452
  */
85442
85453
  // eslint-disable-next-line max-lines-per-function
85443
85454
  prepareDemandExtraction() {
@@ -85471,7 +85482,8 @@ ${error.stack} - expressID: #${face_.expressID}`);
85471
85482
  occurrenceExpressID ??= this.model.getExpressIDByLocalID(childOwningLocalID);
85472
85483
  this.scene.pushOccurrence(occurrenceExpressID ?? childOwningLocalID);
85473
85484
  try {
85474
- mappedChild.thunk(childOwningLocalID, childTransform);
85485
+ const runChild = slice?.childThunk ?? mappedChild.thunk;
85486
+ runChild(childOwningLocalID, childTransform);
85475
85487
  } catch (ex) {
85476
85488
  if (this.quietRecoverableLogging) {
85477
85489
  } else if (ex instanceof Error) {
@@ -85493,7 +85505,22 @@ ${error.stack} - expressID: #${face_.expressID}`);
85493
85505
  }
85494
85506
  }
85495
85507
  const includeItems = slice?.includeItems ?? true;
85496
- for (const item of includeItems ? representation2.items : []) {
85508
+ const items = includeItems ? representation2.items : [];
85509
+ const itemStart = slice?.itemStart ?? 0;
85510
+ const itemEnd = Math.min(slice?.itemEnd ?? items.length, items.length);
85511
+ for (const replayIndex of slice?.replayPlacements ?? []) {
85512
+ const replayItem = items[replayIndex];
85513
+ if (replayItem instanceof placement) {
85514
+ try {
85515
+ this.extractPlacement(replayItem, mappedItem);
85516
+ } catch {
85517
+ }
85518
+ }
85519
+ }
85520
+ const verifySliceNeutrality = slice?.itemStart !== void 0;
85521
+ for (let itemIndex = itemStart; itemIndex < itemEnd; ++itemIndex) {
85522
+ const item = items[itemIndex];
85523
+ const depthBeforeItem = verifySliceNeutrality ? this.scene.stackLength : 0;
85497
85524
  try {
85498
85525
  if (item instanceof placement) {
85499
85526
  this.extractPlacement(item, mappedItem);
@@ -85523,6 +85550,10 @@ ${error.stack} - expressID: #${face_.expressID}`);
85523
85550
  } else {
85524
85551
  logger_default.error(`Unknown exception processing representation item (${ex}) expressID: #${item.expressID}`);
85525
85552
  }
85553
+ } finally {
85554
+ if (verifySliceNeutrality && !(item instanceof placement) && this.scene.stackLength !== depthBeforeItem && !this.quietRecoverableLogging) {
85555
+ logger_default.error(`Representation item left transform state inside a sliced range expressID: #${item.expressID}`);
85556
+ }
85526
85557
  }
85527
85558
  }
85528
85559
  while (this.scene.stackLength > entryTransformDepth) {
@@ -85533,6 +85564,51 @@ ${error.stack} - expressID: #${face_.expressID}`);
85533
85564
  }
85534
85565
  };
85535
85566
  };
85567
+ const itemUnitRanges = (representation2) => {
85568
+ let items;
85569
+ try {
85570
+ items = representation2.items;
85571
+ } catch {
85572
+ return void 0;
85573
+ }
85574
+ const itemCount = items.length;
85575
+ if (itemCount <= this.demandItemsPerUnit) {
85576
+ return void 0;
85577
+ }
85578
+ const itemsPerUnit = Math.max(this.demandItemsPerUnit, Math.ceil(itemCount / AP214_MAX_ITEM_UNITS_PER_REPRESENTATION));
85579
+ const ranges = [];
85580
+ const replayed = [];
85581
+ const pending = [];
85582
+ let start = 0;
85583
+ for (let cursor = 0; cursor < itemCount; ++cursor) {
85584
+ if (items[cursor] instanceof placement) {
85585
+ pending.push(cursor);
85586
+ }
85587
+ if (cursor + 1 - start < itemsPerUnit || cursor + 1 >= itemCount) {
85588
+ continue;
85589
+ }
85590
+ if (replayed.length + pending.length > AP214_MAX_REPLAYED_PLACEMENTS) {
85591
+ break;
85592
+ }
85593
+ ranges.push({
85594
+ itemStart: start,
85595
+ itemEnd: cursor + 1,
85596
+ replayPlacements: replayed.length > 0 ? replayed.slice() : void 0
85597
+ });
85598
+ replayed.push(...pending);
85599
+ pending.length = 0;
85600
+ start = cursor + 1;
85601
+ }
85602
+ if (ranges.length === 0) {
85603
+ return void 0;
85604
+ }
85605
+ ranges.push({
85606
+ itemStart: start,
85607
+ itemEnd: itemCount,
85608
+ replayPlacements: replayed.length > 0 ? replayed.slice() : void 0
85609
+ });
85610
+ return ranges;
85611
+ };
85536
85612
  const pendingRoots = [];
85537
85613
  {
85538
85614
  this.scene.clearParentStack();
@@ -85737,20 +85813,80 @@ ${error.stack} - expressID: #${face_.expressID}`);
85737
85813
  }
85738
85814
  }
85739
85815
  const units = [];
85816
+ const subtreeItemCounts = /* @__PURE__ */ new Map();
85817
+ const ownItemCount = (node) => {
85818
+ if (node.rep === void 0) {
85819
+ return 0;
85820
+ }
85821
+ try {
85822
+ return node.rep.items.length;
85823
+ } catch {
85824
+ return 0;
85825
+ }
85826
+ };
85827
+ const subtreeItemCount = (node, path3) => {
85828
+ const memoized = subtreeItemCounts.get(node);
85829
+ if (memoized !== void 0) {
85830
+ return memoized;
85831
+ }
85832
+ if (path3.has(node)) {
85833
+ return 0;
85834
+ }
85835
+ path3.add(node);
85836
+ let count = ownItemCount(node);
85837
+ for (const [childLocalID] of node.children ?? []) {
85838
+ const child = treeMap.get(childLocalID);
85839
+ if (child !== void 0) {
85840
+ count += subtreeItemCount(child, path3);
85841
+ }
85842
+ }
85843
+ path3.delete(node);
85844
+ subtreeItemCounts.set(node, count);
85845
+ return count;
85846
+ };
85847
+ const expandUnits = (node, path3, wrap) => {
85848
+ const childCount = node.children?.length ?? 0;
85849
+ for (let child = 0; child < childCount; ++child) {
85850
+ const childNode = treeMap.get(node.children[child][0]);
85851
+ const descend = childNode !== void 0 && childNode.rep !== void 0 && childNode.thunk !== void 0 && !path3.has(childNode) && subtreeItemCount(childNode, /* @__PURE__ */ new Set()) > this.demandItemsPerUnit;
85852
+ if (!descend) {
85853
+ wrap({ childStart: child, childEnd: child + 1, includeItems: false });
85854
+ continue;
85855
+ }
85856
+ const descendant = childNode;
85857
+ path3.add(descendant);
85858
+ expandUnits(descendant, path3, (childSlice) => {
85859
+ const childThunk = makeThunk(descendant.rep, descendant.owningLocalID, descendant, childSlice);
85860
+ wrap({
85861
+ childStart: child,
85862
+ childEnd: child + 1,
85863
+ includeItems: false,
85864
+ childThunk
85865
+ });
85866
+ });
85867
+ path3.delete(descendant);
85868
+ }
85869
+ const itemRanges = node.rep !== void 0 ? itemUnitRanges(node.rep) : void 0;
85870
+ if (itemRanges === void 0) {
85871
+ wrap({ childStart: 0, childEnd: 0, includeItems: true });
85872
+ return;
85873
+ }
85874
+ for (const range of itemRanges) {
85875
+ wrap({ childStart: 0, childEnd: 0, includeItems: true, ...range });
85876
+ }
85877
+ };
85740
85878
  for (const root of pendingRoots) {
85741
85879
  const node = root.node;
85742
85880
  const scaleTransform = root.scaleTransform;
85743
- const childCount = node.children?.length ?? 0;
85744
- if (childCount === 0 || node.rep === void 0) {
85881
+ const rep = node.rep;
85882
+ if (rep === void 0) {
85745
85883
  units.push(() => node.thunk(void 0, scaleTransform));
85746
85884
  continue;
85747
85885
  }
85748
- for (let child = 0; child < childCount; ++child) {
85749
- const sliced = makeThunk(node.rep, node.owningLocalID, node, { childStart: child, childEnd: child + 1, includeItems: false });
85886
+ expandUnits(node, /* @__PURE__ */ new Set([node]), (slice) => {
85887
+ const sliced = makeThunk(rep, node.owningLocalID, node, slice);
85750
85888
  units.push(() => sliced(void 0, scaleTransform));
85751
- }
85752
- const itemsOnly = makeThunk(node.rep, node.owningLocalID, node, { childStart: 0, childEnd: 0, includeItems: true });
85753
- units.push(() => itemsOnly(void 0, scaleTransform));
85889
+ });
85754
85890
  }
85755
85891
  this.demandUnits_ = units;
85756
85892
  }
@@ -85791,15 +85927,32 @@ ${error.stack} - expressID: #${face_.expressID}`);
85791
85927
  * AP214 models. Staged face tessellation is finalized after every
85792
85928
  * batch so captured geometry is complete and readable.
85793
85929
  *
85930
+ * `budgetMs` is what keeps an interactive caller's thread responsive
85931
+ * now that a unit is a slice rather than a product. Unit cost is wildly
85932
+ * uneven both across models and inside one: Arty_Z7's 603 units average
85933
+ * 58 ms but run from under a millisecond to 600 ms (a single silkscreen
85934
+ * solid), while DSA2's average 32 ms. No fixed unit-count-per-call
85935
+ * serves that — it either overshoots the frame budget on the expensive
85936
+ * units or burns hundreds of calls on the cheap ones — so a caller with
85937
+ * a frame to hit passes a wall-clock budget and gets as many units as
85938
+ * fit. One unit always runs: a single unit that overruns the budget is
85939
+ * the floor this layer can offer, and shrinking it further is geometry
85940
+ * cost, not granularity (conway#564). Callers that want completion
85941
+ * rather than responsiveness (the whole-model walk, the deferred drain)
85942
+ * pass no budget and keep the pre-conway#579 behaviour exactly.
85943
+ *
85794
85944
  * @param count Max units to execute this call (min 1).
85945
+ * @param budgetMs Wall-clock this call may spend before returning
85946
+ * early; unbounded when absent.
85795
85947
  * @return {number} Units actually executed.
85796
85948
  */
85797
- extractDemandUnitBatch(count) {
85949
+ extractDemandUnitBatch(count, budgetMs) {
85798
85950
  const units = this.demandUnits_;
85799
85951
  if (units === void 0) {
85800
85952
  return 0;
85801
85953
  }
85802
85954
  const end = Math.min(this.demandCursor_ + Math.max(count, 1), units.length);
85955
+ const deadline = budgetMs !== void 0 ? Date.now() + budgetMs : void 0;
85803
85956
  let executed = 0;
85804
85957
  for (; this.demandCursor_ < end; ++this.demandCursor_) {
85805
85958
  try {
@@ -85815,6 +85968,15 @@ ${error.stack} - expressID: #${face_.expressID}`);
85815
85968
  logger_default.error(`Unknown exception processing demand unit (${ex})`);
85816
85969
  }
85817
85970
  }
85971
+ if (deadline !== void 0) {
85972
+ if (this.useStagedFaces) {
85973
+ this.finalizeStagedFaces();
85974
+ }
85975
+ if (Date.now() >= deadline) {
85976
+ ++this.demandCursor_;
85977
+ break;
85978
+ }
85979
+ }
85818
85980
  }
85819
85981
  this.finalizeStagedFaces();
85820
85982
  return executed;
@@ -944,7 +944,7 @@ var EntityTypesIfcCount = 909;
944
944
  var entity_types_ifc_gen_default = EntityTypesIfc;
945
945
 
946
946
  // compiled/src/version/version.js
947
- var versionString = "Conway v1.578.1546";
947
+ var versionString = "Conway v1.579.1547";
948
948
 
949
949
  // compiled/dependencies/conway-geom/interface/conway_geometry.js
950
950
  var wasmType = "";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ap214_demand_unit_slicing.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ap214_demand_unit_slicing.test.d.ts","sourceRoot":"","sources":["../../../src/AP214E3_2010/ap214_demand_unit_slicing.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,170 @@
1
+ import fs from "fs";
2
+ import { describe, expect, test, beforeAll } from "@jest/globals";
3
+ import { AP214GeometryExtraction } from "./ap214_geometry_extraction.js";
4
+ import { ParseResult } from "../step/parsing/step_parser.js";
5
+ import AP214StepParser from "./ap214_step_parser.js";
6
+ import ParsingBuffer from "../parsing/parsing_buffer.js";
7
+ import { ConwayGeometry } from "../../dependencies/conway-geom/index.js";
8
+ import { ExtractResult } from "../core/shared_constants.js";
9
+ /**
10
+ * conway#579: a demand unit used to be a whole product, so one item-heavy
11
+ * representation (Arty_Z7's silkscreen: 654 solids in one
12
+ * ADVANCED_BREP_SHAPE_REPRESENTATION, two levels under the only root) ran
13
+ * as a single 22 s uninterruptible task. prepareDemandExtraction now
14
+ * flattens depth first and cuts a representation's `items` into index
15
+ * ranges, which is only sound if the cut walk visits the same items in the
16
+ * same order with the same transform state — the claim these tests pin.
17
+ *
18
+ * `demandItemsPerUnit` is the granularity knob: `Infinity` reproduces the
19
+ * pre-change flattening (one unit per immediate child of a root, plus one
20
+ * for the root's own items), and small values cut both deeper and finer.
21
+ * Every setting has to emit byte-identical placed geometry.
22
+ */
23
+ /** Assembly with a 13-NAUO occurrence tree and real BREP leaves. */
24
+ const ASSEMBLY_FIXTURE = "data/as1-oc-214.stp";
25
+ /**
26
+ * Single-root assembly whose #14069 ADVANCED_BREP_SHAPE_REPRESENTATION
27
+ * carries ten solids in one `items` array — the multi-item representation
28
+ * the issue is about, at a size the default granularity actually cuts.
29
+ */
30
+ const MULTI_ITEM_FIXTURE = "data/nema-23-76mm.step";
31
+ /**
32
+ * Derived from `create-a-tube.step`: one
33
+ * ADVANCED_BREP_SHAPE_REPRESENTATION holding a NON-IDENTITY
34
+ * AXIS2_PLACEMENT_3D followed by three copies of the tube solid. That
35
+ * leading placement is the state the item loop carries — it pushes onto
36
+ * the scene transform stack and places every item after it — so a range
37
+ * that starts past it lands its solids at the wrong place unless the
38
+ * range replays it. It is also the shape of the worst real case:
39
+ * DSA2.step's single root is one placement followed by 28,674 solids.
40
+ */
41
+ const LEADING_PLACEMENT_FIXTURE = "data/ap214-sliced-item-ranges.step";
42
+ /**
43
+ * Six items, the first of which is a MAPPED_ITEM that pushes its mapping
44
+ * transform and then throws (its REPRESENTATION_MAP's
45
+ * `mapped_representation` points at a CARTESIAN_POINT, so the dereference
46
+ * fails after the push).
47
+ *
48
+ * That failure path is the one way a NON-placement item can leave
49
+ * transform state behind: extractMappedItem's pop loop is unreachable once
50
+ * the walk between the pushes throws. Unsliced, the leak displaces every
51
+ * later item in the representation; sliced, only the ones sharing its
52
+ * range — so the geometry depended on where the cut fell until
53
+ * extractMappedItem was made to pop in a `finally`. No model in the
54
+ * regression corpus contains a MAPPED_ITEM at all, so this fixture is the
55
+ * only thing standing between that fix and a silent regression.
56
+ */
57
+ const MAPPED_ITEM_FAILURE_FIXTURE = "data/ap214-mapped-item-failure.step";
58
+ /* Wasm init, and the per-test budgets: these extractions run real BREPs. */
59
+ const WASM_INIT_TIMEOUT_MS = 60000;
60
+ const EXTRACT_TIMEOUT_MS = 120000;
61
+ const MULTI_EXTRACT_TIMEOUT_MS = 300000;
62
+ // Column-major 4x4: the translation column sits at 12, 13, 14.
63
+ const TRANSLATION_START = 12;
64
+ const TRANSLATION_END = 15;
65
+ let conwayGeometry;
66
+ beforeAll(async () => {
67
+ conwayGeometry = new ConwayGeometry();
68
+ expect(await conwayGeometry.initialize()).toBe(true);
69
+ }, WASM_INIT_TIMEOUT_MS);
70
+ /**
71
+ * Extract the fixture at one granularity and reduce the resulting scene to
72
+ * a comparable digest of every placed geometry instance, in walk order.
73
+ *
74
+ * The model is re-parsed per call because extraction memoizes geometry into
75
+ * it — two granularities have to start from equally cold models or the
76
+ * second one measures the first one's cache.
77
+ *
78
+ * @param fixture Path to the STEP file.
79
+ * @param itemsPerUnit Value for `demandItemsPerUnit`.
80
+ * @return {[PlacedDigest[], number]} The digest and the unit count.
81
+ */
82
+ function extractAt(fixture, itemsPerUnit) {
83
+ const parser = AP214StepParser.Instance;
84
+ const buffer = new ParsingBuffer(fs.readFileSync(fixture));
85
+ expect(parser.parseHeader(buffer)[1]).toBe(ParseResult.COMPLETE);
86
+ const [, model] = parser.parseDataToModel(buffer);
87
+ expect(model).not.toBe(void 0);
88
+ const extraction = new AP214GeometryExtraction(conwayGeometry, model);
89
+ extraction.demandItemsPerUnit = itemsPerUnit;
90
+ extraction.prepareDemandExtraction();
91
+ const unitCount = extraction.demandUnitCount;
92
+ const [result, scene] = extraction.extractAP214GeometryData();
93
+ expect(result).toBe(ExtractResult.COMPLETE);
94
+ return [placedDigest(scene), unitCount];
95
+ }
96
+ /**
97
+ * Reduce a scene to one row per placed geometry instance: what it is, where
98
+ * the walk put it, and the contents of its vertex and index buffers.
99
+ *
100
+ * @param scene The extracted scene.
101
+ * @return {PlacedDigest[]} One row per instance, in walk order.
102
+ */
103
+ function placedDigest(scene) {
104
+ const wasm = conwayGeometry.wasmModule;
105
+ const rows = [];
106
+ for (const [transform, , mesh, , entity, occurrencePath] of scene.walkWithOccurrence()) {
107
+ const geometry = mesh.geometry;
108
+ if (geometry === void 0 || typeof geometry.GetVertexDataSize !== "function") {
109
+ continue;
110
+ }
111
+ // Read into copies immediately — HEAPF32/HEAPU32 views detach on wasm
112
+ // memory growth and a held view silently reads zeroes.
113
+ const floatCount = geometry.GetVertexDataSize();
114
+ const vertexData = wasm.HEAPF32.slice(geometry.GetVertexData() / 4, (geometry.GetVertexData() / 4) + floatCount);
115
+ const indexCount = geometry.GetIndexDataSize();
116
+ const indexData = wasm.HEAPU32.slice(geometry.GetIndexData() / 4, (geometry.GetIndexData() / 4) + indexCount);
117
+ rows.push({
118
+ expressID: entity?.expressID,
119
+ occurrencePath: occurrencePath.join("/"),
120
+ transform: (transform ?? []).join(","),
121
+ vertices: `${floatCount}:${vertexData.join(",")}`,
122
+ indices: `${indexCount}:${indexData.join(",")}`,
123
+ });
124
+ }
125
+ return rows;
126
+ }
127
+ describe("AP214 demand unit granularity (conway#579)", () => {
128
+ test("an assembly extracts identically sliced and unsliced", () => {
129
+ const [unsliced, unslicedUnits] = extractAt(ASSEMBLY_FIXTURE, Infinity);
130
+ const [sliced, slicedUnits] = extractAt(ASSEMBLY_FIXTURE, 1);
131
+ // Without this the equality below would hold trivially.
132
+ expect(slicedUnits).toBeGreaterThan(unslicedUnits);
133
+ expect(unsliced.length).toBeGreaterThan(0);
134
+ expect(sliced).toStrictEqual(unsliced);
135
+ }, EXTRACT_TIMEOUT_MS);
136
+ test("a ten-solid representation extracts identically at every granularity", () => {
137
+ const [unsliced, unslicedUnits] = extractAt(MULTI_ITEM_FIXTURE, Infinity);
138
+ const [atDefault, defaultUnits] = extractAt(MULTI_ITEM_FIXTURE, 4);
139
+ const [perItem, perItemUnits] = extractAt(MULTI_ITEM_FIXTURE, 1);
140
+ expect(defaultUnits).toBeGreaterThan(unslicedUnits);
141
+ expect(perItemUnits).toBeGreaterThan(defaultUnits);
142
+ expect(unsliced.length).toBeGreaterThan(0);
143
+ expect(atDefault).toStrictEqual(unsliced);
144
+ expect(perItem).toStrictEqual(unsliced);
145
+ }, MULTI_EXTRACT_TIMEOUT_MS);
146
+ test("ranges after a leading placement are still placed by it", () => {
147
+ const [unsliced, unslicedUnits] = extractAt(LEADING_PLACEMENT_FIXTURE, Infinity);
148
+ const [sliced, slicedUnits] = extractAt(LEADING_PLACEMENT_FIXTURE, 1);
149
+ expect(slicedUnits).toBeGreaterThan(unslicedUnits);
150
+ // Three solids, and the placement they inherit is not the identity —
151
+ // both of which have to hold or the equality below proves nothing.
152
+ expect(unsliced.length).toBe(3);
153
+ expect(unsliced[0].transform).not.toBe("");
154
+ expect(unsliced[0].transform.split(",").slice(TRANSLATION_START, TRANSLATION_END))
155
+ .not.toStrictEqual(["0", "0", "0"]);
156
+ expect(sliced).toStrictEqual(unsliced);
157
+ }, EXTRACT_TIMEOUT_MS);
158
+ test("a mapped item that throws after pushing does not shift later items", () => {
159
+ const [unsliced, unslicedUnits] = extractAt(MAPPED_ITEM_FAILURE_FIXTURE, Infinity);
160
+ const [atDefault, defaultUnits] = extractAt(MAPPED_ITEM_FAILURE_FIXTURE, 4);
161
+ const [perItem, perItemUnits] = extractAt(MAPPED_ITEM_FAILURE_FIXTURE, 1);
162
+ expect(defaultUnits).toBeGreaterThan(unslicedUnits);
163
+ expect(perItemUnits).toBeGreaterThan(defaultUnits);
164
+ // Five solids follow the failing mapped item; if the fixture ever stops
165
+ // producing them the equalities below stop meaning anything.
166
+ expect(unsliced.length).toBe(5);
167
+ expect(atDefault).toStrictEqual(unsliced);
168
+ expect(perItem).toStrictEqual(unsliced);
169
+ }, EXTRACT_TIMEOUT_MS);
170
+ });