@bldrs-ai/conway 1.374.1181 → 1.376.1184

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.
@@ -70042,7 +70042,7 @@ IfcStepParser.Instance = new IfcStepParser();
70042
70042
  var ifc_step_parser_default = IfcStepParser;
70043
70043
 
70044
70044
  // compiled/src/version/version.js
70045
- var versionString = "Conway Web-Ifc Shim v1.374.1181";
70045
+ var versionString = "Conway Web-Ifc Shim v1.376.1184";
70046
70046
 
70047
70047
  // compiled/src/statistics/statistics.js
70048
70048
  var Statistics = class {
@@ -86283,7 +86283,7 @@ var IfcSceneBuilder = class {
86283
86283
  };
86284
86284
 
86285
86285
  // compiled/src/version/version.js
86286
- var versionString = "Conway Web-Ifc Shim v1.374.1181";
86286
+ var versionString = "Conway Web-Ifc Shim v1.376.1184";
86287
86287
 
86288
86288
  // compiled/src/statistics/statistics.js
86289
86289
  var Statistics = class {
@@ -79734,7 +79734,7 @@ var ExtractResult;
79734
79734
  })(ExtractResult || (ExtractResult = {}));
79735
79735
 
79736
79736
  // compiled/src/version/version.js
79737
- var versionString = "Conway Web-Ifc Shim v1.374.1181";
79737
+ var versionString = "Conway Web-Ifc Shim v1.376.1184";
79738
79738
 
79739
79739
  // compiled/src/statistics/statistics.js
79740
79740
  var Statistics = class {
@@ -70040,7 +70040,7 @@ IfcStepParser.Instance = new IfcStepParser();
70040
70040
  var ifc_step_parser_default = IfcStepParser;
70041
70041
 
70042
70042
  // compiled/src/version/version.js
70043
- var versionString = "Conway Web-Ifc Shim v1.374.1181";
70043
+ var versionString = "Conway Web-Ifc Shim v1.376.1184";
70044
70044
 
70045
70045
  // compiled/src/statistics/statistics.js
70046
70046
  var Statistics = class {
@@ -23,7 +23,10 @@ export interface ProductStructureNode {
23
23
  * `product_definition` express id.
24
24
  */
25
25
  expressID: number;
26
- /** Readable node kind: `'product'` for roots, `'product_occurrence'` for NAUO nodes. */
26
+ /**
27
+ * Readable node kind: `'product'` for roots, `'product_occurrence'` for NAUO
28
+ * nodes, `'solid'` for ephemeral sub-product solid nodes.
29
+ */
27
30
  type: string;
28
31
  /** Display label: `product.name`, falling back to the NAUO name / reference designator. */
29
32
  name: string;
@@ -43,9 +46,54 @@ export interface ProductStructureNode {
43
46
  * scene geometry for NavTree-click ⇄ viewport-pick round-tripping.
44
47
  */
45
48
  shapeRepresentationIds: number[];
46
- /** Child occurrence nodes. */
49
+ /** Child occurrence nodes (and, when opted in, ephemeral solid nodes). */
47
50
  children: ProductStructureNode[];
51
+ /**
52
+ * True for ephemeral (non-product) nodes — pickable geometry that carries
53
+ * identity in the file but has no product semantics, e.g. one named solid of
54
+ * a multibody part. Consumers should render these lighter-weight,
55
+ * selectable-but-not-product. See `design/new/step-nonproduct-semantics.md`.
56
+ */
57
+ ephemeral?: boolean;
58
+ /**
59
+ * Number of this node's solids suppressed by the ephemeral-layer limits
60
+ * (unnamed-soup suppression or the per-product cap), so a consumer can
61
+ * render an "N more…" affordance instead of silently truncating.
62
+ */
63
+ droppedSolids?: number;
48
64
  }
65
+ /**
66
+ * Options for {@link AP214ProductStructureExtraction.extractProductStructure}.
67
+ */
68
+ export interface ProductStructureOptions {
69
+ /**
70
+ * Surface an ephemeral layer of solid-level nodes beneath each multibody
71
+ * product (default false). A product only gets solid children when its shape
72
+ * representation holds at least two solids — a single-solid product already
73
+ * maps 1:1 onto its node — and an all-unnamed set larger than
74
+ * {@link maxUnnamedSolidsPerProduct} is suppressed as meaningless "solid
75
+ * soup" (the DSA2 case: 28k unnamed single-face shells under one product).
76
+ */
77
+ includeSolids?: boolean;
78
+ /**
79
+ * Hard cap on emitted solid children per product occurrence
80
+ * (default {@link DEFAULT_MAX_SOLIDS_PER_PRODUCT}); overflow is reported via
81
+ * {@link ProductStructureNode.droppedSolids}.
82
+ */
83
+ maxSolidsPerProduct?: number;
84
+ /**
85
+ * When a product's solids are *all* unnamed and outnumber this (default
86
+ * {@link DEFAULT_MAX_UNNAMED_SOLIDS_PER_PRODUCT}), emit none of them:
87
+ * large anonymous solid dumps (ECAD merged-component products, tessellated
88
+ * surface soup) carry no navigable semantics. Named solids are never
89
+ * suppressed by this limit.
90
+ */
91
+ maxUnnamedSolidsPerProduct?: number;
92
+ }
93
+ /** Default hard cap on emitted solid children per product occurrence. */
94
+ export declare const DEFAULT_MAX_SOLIDS_PER_PRODUCT = 256;
95
+ /** Default suppression threshold for a product whose solids are all unnamed. */
96
+ export declare const DEFAULT_MAX_UNNAMED_SOLIDS_PER_PRODUCT = 32;
49
97
  /**
50
98
  * Extracts the STEP product / assembly structure from a populated
51
99
  * {@link AP214StepModel} into a nested, named, occurrence-keyed tree.
@@ -63,6 +111,11 @@ export declare class AP214ProductStructureExtraction {
63
111
  private readonly childProductDefIds_;
64
112
  private readonly productDefById_;
65
113
  private readonly shapeRepsByProductDef_;
114
+ private readonly solidsByProductDef_;
115
+ private readonly solidIdsByProductDef_;
116
+ private includeSolids_;
117
+ private maxSolidsPerProduct_;
118
+ private maxUnnamedSolidsPerProduct_;
66
119
  /**
67
120
  * @param model The populated AP214/AP242 step model to walk.
68
121
  * @param productShapeMap Optional product↔shape map populated during geometry
@@ -75,11 +128,14 @@ export declare class AP214ProductStructureExtraction {
75
128
  /**
76
129
  * Build the product-structure tree.
77
130
  *
131
+ * @param options Optional {@link ProductStructureOptions}; pass
132
+ * `{ includeSolids: true }` to add the ephemeral solid layer beneath
133
+ * multibody products.
78
134
  * @return {ProductStructureNode[]} The roots of the assembly forest. A
79
135
  * single-part file yields one root; a multi-level assembly (e.g. `as1`) yields
80
136
  * one root whose descendants are the NAUO occurrences.
81
137
  */
82
- extractProductStructure(): ProductStructureNode[];
138
+ extractProductStructure(options?: ProductStructureOptions): ProductStructureNode[];
83
139
  /**
84
140
  * Index every `product_definition` by express id for O(1) lookup during the
85
141
  * recursive walk.
@@ -98,6 +154,26 @@ export declare class AP214ProductStructureExtraction {
98
154
  * geometry-extraction product↔shape map.
99
155
  */
100
156
  private indexShapeRepresentations;
157
+ /**
158
+ * Index the solid-level bodies of each product definition for the ephemeral
159
+ * layer: walk each `shape_definition_representation`'s representation, plus
160
+ * any representation reachable over a *plain* `shape_representation_relationship`
161
+ * (SolidWorks binds a part's multibody `advanced_brep_shape_representation`
162
+ * to its SDR-bound representation that way — the NEMA motor pattern), and
163
+ * collect the solid items. Transformation-bearing relationship variants are
164
+ * assembly placements (parent rep ↔ child rep), so following them would leak
165
+ * every child part's solids into its parent assembly; they are skipped.
166
+ */
167
+ private indexSolids;
168
+ /**
169
+ * Record a solid for a product definition, de-duplicating by the solid's
170
+ * express id (a body can be reachable both directly and over a
171
+ * representation relationship).
172
+ *
173
+ * @param productDefId The owning product definition express id.
174
+ * @param solid The solid to record.
175
+ */
176
+ private addSolid;
101
177
  /**
102
178
  * Record a shape-representation id for a product definition, de-duplicating.
103
179
  *
@@ -118,6 +194,16 @@ export declare class AP214ProductStructureExtraction {
118
194
  * @return {ProductStructureNode} The built node, with children.
119
195
  */
120
196
  private buildNode;
197
+ /**
198
+ * Append this node's ephemeral solid children, applying the layer's
199
+ * heuristics (see {@link ProductStructureOptions}): nothing for a
200
+ * single-solid product, full suppression for oversized all-unnamed sets,
201
+ * and the hard per-product cap — suppressed/overflow counts are surfaced
202
+ * via {@link ProductStructureNode.droppedSolids}.
203
+ *
204
+ * @param node The product/occurrence node to attach solid children to.
205
+ */
206
+ private appendSolidChildren;
121
207
  /**
122
208
  * Resolve a node label, preferring the product name, then the occurrence's
123
209
  * own name / reference designator.
@@ -1 +1 @@
1
- {"version":3,"file":"ap214_product_structure_extraction.d.ts","sourceRoot":"","sources":["../../../src/AP214E3_2010/ap214_product_structure_extraction.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAOhE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,oBAAoB;IAEnC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAA;IAEZ,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAA;IAEZ,2EAA2E;IAC3E,0BAA0B,EAAE,MAAM,CAAA;IAElC,mEAAmE;IACnE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;;;OAIG;IACH,cAAc,EAAE,MAAM,EAAE,CAAA;IAExB;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,EAAE,CAAA;IAEhC,8BAA8B;IAC9B,QAAQ,EAAE,oBAAoB,EAAE,CAAA;CACjC;AAMD;;;;;;;;;GASG;AACH,qBAAa,+BAA+B;IAgBtC,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IAfrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsD;IACrF,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAoB;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAwC;IACxE,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA8B;IAErE;;;;;;;OAOG;gBAEkB,KAAK,EAAE,cAAc,EACrB,eAAe,CAAC,EAAE,oBAAoB,YAAA;IAG3D;;;;;;OAMG;IACI,uBAAuB,IAAI,oBAAoB,EAAE;IA6BxD;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAa/B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IA0B3B;;;;;OAKG;IACH,OAAO,CAAC,yBAAyB;IAiCjC;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAc9B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,SAAS;IAsDjB;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;IA0BpB;;;;;;;;;OASG;IACH,MAAM,CAAC,0BAA0B,CAC7B,UAAU,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,GAAI,MAAM,GAAG,SAAS;CAgBzE"}
1
+ {"version":3,"file":"ap214_product_structure_extraction.d.ts","sourceRoot":"","sources":["../../../src/AP214E3_2010/ap214_product_structure_extraction.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,MAAM,oBAAoB,CAAA;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAkBhE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,oBAAoB;IAEnC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAA;IAEZ,2EAA2E;IAC3E,0BAA0B,EAAE,MAAM,CAAA;IAElC,mEAAmE;IACnE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;;;OAIG;IACH,cAAc,EAAE,MAAM,EAAE,CAAA;IAExB;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,EAAE,CAAA;IAEhC,0EAA0E;IAC1E,QAAQ,EAAE,oBAAoB,EAAE,CAAA;IAEhC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IAEnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IAEtC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IAEvB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;;;;;OAMG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAA;CACpC;AAED,yEAAyE;AACzE,eAAO,MAAM,8BAA8B,MAAM,CAAA;AAEjD,gFAAgF;AAChF,eAAO,MAAM,sCAAsC,KAAK,CAAA;AAmDxD;;;;;;;;;GASG;AACH,qBAAa,+BAA+B;IAqBtC,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IApBrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsD;IACrF,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAoB;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAwC;IACxE,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA8B;IACrE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAoC;IACxE,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAiC;IACvE,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,oBAAoB,CAAiC;IAC7D,OAAO,CAAC,2BAA2B,CAAyC;IAE5E;;;;;;;OAOG;gBAEkB,KAAK,EAAE,cAAc,EACrB,eAAe,CAAC,EAAE,oBAAoB,YAAA;IAG3D;;;;;;;;;OASG;IACI,uBAAuB,CAAE,OAAO,CAAC,EAAE,uBAAuB,GAAI,oBAAoB,EAAE;IAuC3F;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAa/B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IA0B3B;;;;;OAKG;IACH,OAAO,CAAC,yBAAyB;IAiCjC;;;;;;;;;OASG;IACH,OAAO,CAAC,WAAW;IAuEnB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ;IAoBhB;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAc9B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,SAAS;IA2DjB;;;;;;;;OAQG;IACH,OAAO,CAAC,mBAAmB;IA2C3B;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;IA0BpB;;;;;;;;;OASG;IACH,MAAM,CAAC,0BAA0B,CAC7B,UAAU,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,GAAI,MAAM,GAAG,SAAS;CAgBzE"}
@@ -1,10 +1,45 @@
1
+ import { face_based_surface_model } from "./AP214E3_2010_gen/face_based_surface_model.gen.js";
2
+ import { manifold_solid_brep } from "./AP214E3_2010_gen/manifold_solid_brep.gen.js";
1
3
  import { next_assembly_usage_occurrence } from "./AP214E3_2010_gen/next_assembly_usage_occurrence.gen.js";
2
4
  import { product_definition } from "./AP214E3_2010_gen/product_definition.gen.js";
3
5
  import { product_definition_shape } from "./AP214E3_2010_gen/product_definition_shape.gen.js";
6
+ import { representation_relationship_with_transformation, } from "./AP214E3_2010_gen/representation_relationship_with_transformation.gen.js";
4
7
  import { shape_definition_representation } from "./AP214E3_2010_gen/shape_definition_representation.gen.js";
8
+ import { shape_representation_relationship, } from "./AP214E3_2010_gen/shape_representation_relationship.gen.js";
9
+ import { shell_based_surface_model } from "./AP214E3_2010_gen/shell_based_surface_model.gen.js";
10
+ /** Default hard cap on emitted solid children per product occurrence. */
11
+ export const DEFAULT_MAX_SOLIDS_PER_PRODUCT = 256;
12
+ /** Default suppression threshold for a product whose solids are all unnamed. */
13
+ export const DEFAULT_MAX_UNNAMED_SOLIDS_PER_PRODUCT = 32;
14
+ /** Minimum solids in a product before the ephemeral layer adds anything. */
15
+ const MIN_SOLIDS_FOR_EPHEMERAL = 2;
5
16
  /** Readable node-kind strings for {@link ProductStructureNode.type}. */
6
17
  const ROOT_NODE_TYPE = "product";
7
18
  const OCCURRENCE_NODE_TYPE = "product_occurrence";
19
+ const SOLID_NODE_TYPE = "solid";
20
+ /**
21
+ * Whether a representation-item name carries meaning (non-empty and not an
22
+ * exporter placeholder).
23
+ *
24
+ * @param name The item name.
25
+ * @return {boolean} True when the name is worth displaying.
26
+ */
27
+ function isMeaningfulName(name) {
28
+ return name.length > 0 && name !== "NONE" && name !== "UNKNOWN";
29
+ }
30
+ /**
31
+ * Whether a representation item is a solid-level body that can anchor an
32
+ * ephemeral node ({@link manifold_solid_brep} covers its `brep_with_voids` /
33
+ * `faceted_brep` subtypes).
34
+ *
35
+ * @param item The representation item.
36
+ * @return {boolean} True for solid-level items.
37
+ */
38
+ function isSolidItem(item) {
39
+ return item instanceof manifold_solid_brep ||
40
+ item instanceof shell_based_surface_model ||
41
+ item instanceof face_based_surface_model;
42
+ }
8
43
  /**
9
44
  * Extracts the STEP product / assembly structure from a populated
10
45
  * {@link AP214StepModel} into a nested, named, occurrence-keyed tree.
@@ -31,18 +66,34 @@ export class AP214ProductStructureExtraction {
31
66
  this.childProductDefIds_ = new Set();
32
67
  this.productDefById_ = new Map();
33
68
  this.shapeRepsByProductDef_ = new Map();
69
+ this.solidsByProductDef_ = new Map();
70
+ this.solidIdsByProductDef_ = new Map();
71
+ this.includeSolids_ = false;
72
+ this.maxSolidsPerProduct_ = DEFAULT_MAX_SOLIDS_PER_PRODUCT;
73
+ this.maxUnnamedSolidsPerProduct_ = DEFAULT_MAX_UNNAMED_SOLIDS_PER_PRODUCT;
34
74
  }
35
75
  /**
36
76
  * Build the product-structure tree.
37
77
  *
78
+ * @param options Optional {@link ProductStructureOptions}; pass
79
+ * `{ includeSolids: true }` to add the ephemeral solid layer beneath
80
+ * multibody products.
38
81
  * @return {ProductStructureNode[]} The roots of the assembly forest. A
39
82
  * single-part file yields one root; a multi-level assembly (e.g. `as1`) yields
40
83
  * one root whose descendants are the NAUO occurrences.
41
84
  */
42
- extractProductStructure() {
85
+ extractProductStructure(options) {
86
+ this.includeSolids_ = options?.includeSolids ?? false;
87
+ this.maxSolidsPerProduct_ =
88
+ options?.maxSolidsPerProduct ?? DEFAULT_MAX_SOLIDS_PER_PRODUCT;
89
+ this.maxUnnamedSolidsPerProduct_ =
90
+ options?.maxUnnamedSolidsPerProduct ?? DEFAULT_MAX_UNNAMED_SOLIDS_PER_PRODUCT;
43
91
  this.indexProductDefinitions();
44
92
  this.indexAssemblyUsages();
45
93
  this.indexShapeRepresentations();
94
+ if (this.includeSolids_) {
95
+ this.indexSolids();
96
+ }
46
97
  const roots = [];
47
98
  for (const [productDefId, productDef] of this.productDefById_) {
48
99
  if (this.childProductDefIds_.has(productDefId)) {
@@ -124,6 +175,90 @@ export class AP214ProductStructureExtraction {
124
175
  }
125
176
  }
126
177
  }
178
+ /**
179
+ * Index the solid-level bodies of each product definition for the ephemeral
180
+ * layer: walk each `shape_definition_representation`'s representation, plus
181
+ * any representation reachable over a *plain* `shape_representation_relationship`
182
+ * (SolidWorks binds a part's multibody `advanced_brep_shape_representation`
183
+ * to its SDR-bound representation that way — the NEMA motor pattern), and
184
+ * collect the solid items. Transformation-bearing relationship variants are
185
+ * assembly placements (parent rep ↔ child rep), so following them would leak
186
+ * every child part's solids into its parent assembly; they are skipped.
187
+ */
188
+ indexSolids() {
189
+ const relatedRepsByRepId = new Map();
190
+ const relate = (fromId, to) => {
191
+ if (fromId === void 0) {
192
+ return;
193
+ }
194
+ let related = relatedRepsByRepId.get(fromId);
195
+ if (related === void 0) {
196
+ related = [];
197
+ relatedRepsByRepId.set(fromId, related);
198
+ }
199
+ related.push(to);
200
+ };
201
+ for (const element of this.model.types(shape_representation_relationship)) {
202
+ const srr = element;
203
+ if (srr.findVariant(representation_relationship_with_transformation) !== void 0) {
204
+ continue;
205
+ }
206
+ const rep1 = srr.rep_1;
207
+ const rep2 = srr.rep_2;
208
+ if (rep1 === void 0 || rep2 === void 0) {
209
+ continue;
210
+ }
211
+ // The geometry-bearing representation may be on either side; bridge both
212
+ // ways and let solid-item filtering pick out the bodies.
213
+ relate(rep1.expressID, rep2);
214
+ relate(rep2.expressID, rep1);
215
+ }
216
+ for (const element of this.model.types(shape_definition_representation)) {
217
+ const sdr = element;
218
+ const productDefId = AP214ProductStructureExtraction.resolveProductDefinitionId(sdr.definition);
219
+ const usedRep = sdr.used_representation;
220
+ if (productDefId === void 0 || usedRep === void 0) {
221
+ continue;
222
+ }
223
+ const reps = [usedRep, ...(relatedRepsByRepId.get(usedRep.expressID ?? -1) ?? [])];
224
+ for (const rep of reps) {
225
+ for (const item of rep.items) {
226
+ if (!isSolidItem(item) || item.expressID === void 0) {
227
+ continue;
228
+ }
229
+ this.addSolid(productDefId, {
230
+ expressID: item.expressID,
231
+ name: item.name,
232
+ representationId: rep.expressID,
233
+ });
234
+ }
235
+ }
236
+ }
237
+ }
238
+ /**
239
+ * Record a solid for a product definition, de-duplicating by the solid's
240
+ * express id (a body can be reachable both directly and over a
241
+ * representation relationship).
242
+ *
243
+ * @param productDefId The owning product definition express id.
244
+ * @param solid The solid to record.
245
+ */
246
+ addSolid(productDefId, solid) {
247
+ let solids = this.solidsByProductDef_.get(productDefId);
248
+ let solidIds = this.solidIdsByProductDef_.get(productDefId);
249
+ if (solids === void 0 || solidIds === void 0) {
250
+ solids = [];
251
+ solidIds = new Set();
252
+ this.solidsByProductDef_.set(productDefId, solids);
253
+ this.solidIdsByProductDef_.set(productDefId, solidIds);
254
+ }
255
+ // Set-backed dedup: a linear scan here is quadratic over a product's
256
+ // solids, and a single product can hold tens of thousands (DSA2).
257
+ if (!solidIds.has(solid.expressID)) {
258
+ solidIds.add(solid.expressID);
259
+ solids.push(solid);
260
+ }
261
+ }
127
262
  /**
128
263
  * Record a shape-representation id for a product definition, de-duplicating.
129
264
  *
@@ -169,24 +304,69 @@ export class AP214ProductStructureExtraction {
169
304
  children: [],
170
305
  };
171
306
  const childUsages = this.nauosByParent_.get(productDefId);
172
- if (childUsages === void 0 || onPath.has(productDefId)) {
173
- return node;
174
- }
175
- onPath.add(productDefId);
176
- for (const childUsage of childUsages) {
177
- const childDefId = childUsage.related_product_definition?.expressID;
178
- if (childDefId === void 0) {
179
- continue;
180
- }
181
- const childDef = this.productDefById_.get(childDefId);
182
- if (childDef === void 0) {
183
- continue;
307
+ if (childUsages !== void 0 && !onPath.has(productDefId)) {
308
+ onPath.add(productDefId);
309
+ for (const childUsage of childUsages) {
310
+ const childDefId = childUsage.related_product_definition?.expressID;
311
+ if (childDefId === void 0) {
312
+ continue;
313
+ }
314
+ const childDef = this.productDefById_.get(childDefId);
315
+ if (childDef === void 0) {
316
+ continue;
317
+ }
318
+ node.children.push(this.buildNode(childDef, childUsage, occurrencePath, onPath));
184
319
  }
185
- node.children.push(this.buildNode(childDef, childUsage, occurrencePath, onPath));
320
+ onPath.delete(productDefId);
321
+ }
322
+ // Ephemeral solids come after the occurrence children: they are the
323
+ // lighter-weight entries of the two.
324
+ if (this.includeSolids_) {
325
+ this.appendSolidChildren(node);
186
326
  }
187
- onPath.delete(productDefId);
188
327
  return node;
189
328
  }
329
+ /**
330
+ * Append this node's ephemeral solid children, applying the layer's
331
+ * heuristics (see {@link ProductStructureOptions}): nothing for a
332
+ * single-solid product, full suppression for oversized all-unnamed sets,
333
+ * and the hard per-product cap — suppressed/overflow counts are surfaced
334
+ * via {@link ProductStructureNode.droppedSolids}.
335
+ *
336
+ * @param node The product/occurrence node to attach solid children to.
337
+ */
338
+ appendSolidChildren(node) {
339
+ const solids = this.solidsByProductDef_.get(node.productDefinitionExpressID);
340
+ if (solids === void 0 || solids.length < MIN_SOLIDS_FOR_EPHEMERAL) {
341
+ return;
342
+ }
343
+ const hasNamedSolid = solids.some((solid) => isMeaningfulName(solid.name));
344
+ if (!hasNamedSolid && solids.length > this.maxUnnamedSolidsPerProduct_) {
345
+ node.droppedSolids = solids.length;
346
+ return;
347
+ }
348
+ let emitted = solids;
349
+ if (solids.length > this.maxSolidsPerProduct_) {
350
+ emitted = solids.slice(0, this.maxSolidsPerProduct_);
351
+ node.droppedSolids = solids.length - emitted.length;
352
+ }
353
+ for (let index = 0; index < emitted.length; ++index) {
354
+ const solid = emitted[index];
355
+ node.children.push({
356
+ expressID: solid.expressID,
357
+ type: SOLID_NODE_TYPE,
358
+ name: isMeaningfulName(solid.name) ?
359
+ solid.name : `Solid ${index + 1} of ${solids.length}`,
360
+ productDefinitionExpressID: node.productDefinitionExpressID,
361
+ // A solid is not an occurrence: the path stays NAUO-only, and the
362
+ // selection identity is the (occurrencePath, expressID) pair.
363
+ occurrencePath: [...node.occurrencePath],
364
+ shapeRepresentationIds: solid.representationId !== void 0 ? [solid.representationId] : [],
365
+ children: [],
366
+ ephemeral: true,
367
+ });
368
+ }
369
+ }
190
370
  /**
191
371
  * Resolve a node label, preferring the product name, then the occurrence's
192
372
  * own name / reference designator.
@@ -6,19 +6,29 @@ import { ParseResult } from "../step/parsing/step_parser.js";
6
6
  import { AP214ProductStructureExtraction, } from "./ap214_product_structure_extraction.js";
7
7
  const parser = AP214StepParser.Instance;
8
8
  /**
9
- * Parse the hermetic as1 assembly fixture and extract its product structure.
9
+ * Parse a hermetic fixture and extract its product structure.
10
10
  *
11
+ * @param path The fixture path.
12
+ * @param options Optional extraction options (e.g. the ephemeral solid layer).
11
13
  * @return {ProductStructureNode[]} The extracted assembly forest.
12
14
  */
13
- function extractAs1Structure() {
14
- const buffer = fs.readFileSync("data/as1-assembly.step");
15
+ function extractStructure(path, options) {
16
+ const buffer = fs.readFileSync(path);
15
17
  const bufferInput = new ParsingBuffer(buffer);
16
18
  const headerResult = parser.parseHeader(bufferInput)[1];
17
19
  expect(headerResult).toBe(ParseResult.COMPLETE);
18
20
  const [result, model] = parser.parseDataToModel(bufferInput);
19
21
  expect(model).not.toBe(void 0);
20
22
  expect(result === ParseResult.COMPLETE || result === ParseResult.INCOMPLETE).toBe(true);
21
- return new AP214ProductStructureExtraction(model).extractProductStructure();
23
+ return new AP214ProductStructureExtraction(model).extractProductStructure(options);
24
+ }
25
+ /**
26
+ * Parse the hermetic as1 assembly fixture and extract its product structure.
27
+ *
28
+ * @return {ProductStructureNode[]} The extracted assembly forest.
29
+ */
30
+ function extractAs1Structure() {
31
+ return extractStructure("data/as1-assembly.step");
22
32
  }
23
33
  /**
24
34
  * Find a direct child of a node by display name.
@@ -105,3 +115,86 @@ describe("AP214ProductStructureExtraction", () => {
105
115
  expect(root.children.length).toBe(AS1_ROOT_CHILD_COUNT);
106
116
  });
107
117
  });
118
+ const MULTIBODY_FIXTURE = "data/ap214-multibody-part.step";
119
+ const WIDGET_SOLID_COUNT = 3;
120
+ const SOUP_SOLID_COUNT = 3;
121
+ /**
122
+ * Collect the ephemeral solid children of a node.
123
+ *
124
+ * @param node The parent node.
125
+ * @return {ProductStructureNode[]} The `type: 'solid'` children.
126
+ */
127
+ function solidChildren(node) {
128
+ return node.children.filter((candidate) => candidate.type === "solid");
129
+ }
130
+ describe("AP214ProductStructureExtraction ephemeral solid layer", () => {
131
+ test("is off by default: no solid nodes anywhere", () => {
132
+ const root = extractStructure(MULTIBODY_FIXTURE)[0];
133
+ const stack = [root];
134
+ while (stack.length > 0) {
135
+ const node = stack.pop();
136
+ expect(node.type).not.toBe("solid");
137
+ expect(node.ephemeral).toBe(void 0);
138
+ stack.push(...node.children);
139
+ }
140
+ });
141
+ test("surfaces named multibody solids beneath each occurrence", () => {
142
+ const root = extractStructure(MULTIBODY_FIXTURE, { includeSolids: true })[0];
143
+ const widgets = root.children.filter((node) => node.name === "widget");
144
+ expect(widgets.length).toBe(2);
145
+ for (const widget of widgets) {
146
+ const solids = solidChildren(widget);
147
+ expect(solids.length).toBe(WIDGET_SOLID_COUNT);
148
+ // Named bodies keep their file names; the unnamed one gets a
149
+ // positional fallback.
150
+ expect(solids.map((solid) => solid.name))
151
+ .toEqual(["Body1", "Body2", "Solid 3 of 3"]);
152
+ for (const solid of solids) {
153
+ expect(solid.ephemeral).toBe(true);
154
+ expect(solid.children.length).toBe(0);
155
+ expect(solid.productDefinitionExpressID)
156
+ .toBe(widget.productDefinitionExpressID);
157
+ // A solid is not an occurrence: it inherits the parent's NAUO-only
158
+ // path, and (path, expressID) is the selection identity.
159
+ expect(solid.occurrencePath).toEqual(widget.occurrencePath);
160
+ }
161
+ }
162
+ // The same part type under two occurrences repeats the same solid ids —
163
+ // the occurrence path is what tells the instances apart.
164
+ expect(solidChildren(widgets[0]).map((solid) => solid.expressID))
165
+ .toEqual(solidChildren(widgets[1]).map((solid) => solid.expressID));
166
+ expect(widgets[0].occurrencePath).not.toEqual(widgets[1].occurrencePath);
167
+ });
168
+ test("leaves single-solid products and the assembly root alone", () => {
169
+ const root = extractStructure(MULTIBODY_FIXTURE, { includeSolids: true })[0];
170
+ // The widget solids live in a representation bridged to the assembly's
171
+ // representation by a transformation-bearing relationship (an assembly
172
+ // placement); following it would leak child solids into the parent.
173
+ expect(solidChildren(root).length).toBe(0);
174
+ const pin = root.children.find((node) => node.name === "pin");
175
+ // One solid maps 1:1 onto the product node itself; a child adds nothing.
176
+ expect(solidChildren(pin).length).toBe(0);
177
+ expect(pin.droppedSolids).toBe(void 0);
178
+ });
179
+ test("suppresses oversized all-unnamed solid sets and reports the drop", () => {
180
+ const root = extractStructure(MULTIBODY_FIXTURE, { includeSolids: true, maxUnnamedSolidsPerProduct: 2 })[0];
181
+ const soup = root.children.find((node) => node.name === "soup");
182
+ expect(solidChildren(soup).length).toBe(0);
183
+ expect(soup.droppedSolids).toBe(SOUP_SOLID_COUNT);
184
+ });
185
+ test("emits small all-unnamed sets with positional names", () => {
186
+ const root = extractStructure(MULTIBODY_FIXTURE, { includeSolids: true })[0];
187
+ const soup = root.children.find((node) => node.name === "soup");
188
+ expect(solidChildren(soup).map((solid) => solid.name))
189
+ .toEqual(["Solid 1 of 3", "Solid 2 of 3", "Solid 3 of 3"]);
190
+ });
191
+ test("caps per-product solids and reports the overflow", () => {
192
+ const root = extractStructure(MULTIBODY_FIXTURE, { includeSolids: true, maxSolidsPerProduct: 2 })[0];
193
+ const widget = root.children.find((node) => node.name === "widget");
194
+ const solids = solidChildren(widget);
195
+ expect(solids.length).toBe(2);
196
+ expect(widget.droppedSolids).toBe(WIDGET_SOLID_COUNT - 2);
197
+ // Truncated, not renumbered: positions stay stable under the cap.
198
+ expect(solids.map((solid) => solid.name)).toEqual(["Body1", "Body2"]);
199
+ });
200
+ });
@@ -1,5 +1,5 @@
1
1
  import { IfcApiProxyAP214 } from "./ifc_api_proxy_ap214.js";
2
- import { IncludeProperties, Node } from "./properties_passthrough.js";
2
+ import { IncludeProperties, Node, SpatialStructureOptions } from "./properties_passthrough.js";
3
3
  /**
4
4
  * web-ifc-compatible property/spatial surface over an AP214/AP242 step model.
5
5
  *
@@ -15,6 +15,7 @@ import { IncludeProperties, Node } from "./properties_passthrough.js";
15
15
  export declare class AP214Properties {
16
16
  private api;
17
17
  private structureRoots_?;
18
+ private structureRootsWithSolids_?;
18
19
  private propertyMap_?;
19
20
  private ownerByExpressID_?;
20
21
  private nodeNameByExpressID_?;
@@ -85,10 +86,13 @@ export declare class AP214Properties {
85
86
  *
86
87
  * @param includeProperties When true, merge each node's item properties onto
87
88
  * the node (mirrors the IFC surface's `includeProperties`).
89
+ * @param options Optional shaping: `{ includeSolids: true }` adds the
90
+ * ephemeral solid layer beneath multibody products (`type: 'solid'`,
91
+ * `ephemeral: true` nodes) — see `design/new/step-nonproduct-semantics.md`.
88
92
  * @return {Promise<Node>} The root node. A single-root file returns its root
89
93
  * directly; a multi-root file is wrapped in a synthetic container root.
90
94
  */
91
- getSpatialStructure(includeProperties?: IncludeProperties): Promise<Node>;
95
+ getSpatialStructure(includeProperties?: IncludeProperties, options?: SpatialStructureOptions): Promise<Node>;
92
96
  /**
93
97
  * Get every element of a STEP entity type, backed by the model type index.
94
98
  *
@@ -146,6 +150,16 @@ export declare class AP214Properties {
146
150
  * @return {ProductStructureNode[]} The cached assembly roots.
147
151
  */
148
152
  private productStructure;
153
+ /**
154
+ * Return (and cache) the product-structure forest with the ephemeral solid
155
+ * layer included. Built separately from the plain forest so default callers
156
+ * never pay for the solid walk; the solid nodes are also indexed so
157
+ * `getItemProperties` resolves a solid id to its identity row and
158
+ * `getPropertySets` maps it to the owning product's rows.
159
+ *
160
+ * @return {ProductStructureNode[]} The cached solid-including roots.
161
+ */
162
+ private productStructureWithSolids;
149
163
  /**
150
164
  * Lazily build and cache the extracted property map.
151
165
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ap214_properties.d.ts","sourceRoot":"","sources":["../../../../src/compat/web-ifc/ap214_properties.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AAuGlE;;;;;;;;;;;GAWG;AACH,qBAAa,eAAe;IAWb,OAAO,CAAC,GAAG;IATxB,OAAO,CAAC,eAAe,CAAC,CAAwB;IAChD,OAAO,CAAC,YAAY,CAAC,CAAsB;IAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAqB;IAC/C,OAAO,CAAC,oBAAoB,CAAC,CAAqB;IAClD,OAAO,CAAC,iBAAiB,CAAC,CAAgC;IAE1D;;OAEG;gBACkB,GAAG,EAAE,gBAAgB;IAG1C;;;;;;OAMG;IACH,UAAU,CAAE,IAAI,EAAE,MAAM,GAAI,MAAM;IAIlC;;;;;;;;;;;;;;;;;;OAkBG;IACG,iBAAiB,CAAE,EAAE,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAI,OAAO,CAAC,MAAM,CAAC;IAoBzE;;;;;;;;;;;OAWG;IACG,eAAe,CAAE,SAAS,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAkD7E;;;;;;OAMG;IACG,iBAAiB,CAAE,SAAS,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAI/E;;;;;;OAMG;IACG,sBAAsB,CAAE,SAAS,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAIpF;;;;;;;OAOG;IACG,mBAAmB,CAAE,iBAAiB,CAAC,EAAE,iBAAiB,GAAI,OAAO,CAAC,IAAI,CAAC;IAuBjF;;;;;;OAMG;IACG,iBAAiB,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAmBzE;;;;;;;OAOG;YACW,aAAa;IA4B3B;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;;;;;;;OASG;IACH;;;;;;OAMG;IACI,YAAY,IAAI,IAAI;IAI3B,OAAO,CAAC,YAAY;IA6BpB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAUlB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;;;OAIG;IACH,OAAO,CAAC,UAAU;CAQnB"}
1
+ {"version":3,"file":"ap214_properties.d.ts","sourceRoot":"","sources":["../../../../src/compat/web-ifc/ap214_properties.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EACL,iBAAiB,EACjB,IAAI,EACJ,uBAAuB,EACxB,MAAM,0BAA0B,CAAA;AAiHjC;;;;;;;;;;;GAWG;AACH,qBAAa,eAAe;IAYb,OAAO,CAAC,GAAG;IAVxB,OAAO,CAAC,eAAe,CAAC,CAAwB;IAChD,OAAO,CAAC,yBAAyB,CAAC,CAAwB;IAC1D,OAAO,CAAC,YAAY,CAAC,CAAsB;IAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAqB;IAC/C,OAAO,CAAC,oBAAoB,CAAC,CAAqB;IAClD,OAAO,CAAC,iBAAiB,CAAC,CAAgC;IAE1D;;OAEG;gBACkB,GAAG,EAAE,gBAAgB;IAG1C;;;;;;OAMG;IACH,UAAU,CAAE,IAAI,EAAE,MAAM,GAAI,MAAM;IAIlC;;;;;;;;;;;;;;;;;;OAkBG;IACG,iBAAiB,CAAE,EAAE,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAI,OAAO,CAAC,MAAM,CAAC;IAoBzE;;;;;;;;;;;OAWG;IACG,eAAe,CAAE,SAAS,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAkD7E;;;;;;OAMG;IACG,iBAAiB,CAAE,SAAS,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAI/E;;;;;;OAMG;IACG,sBAAsB,CAAE,SAAS,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAIpF;;;;;;;;;;OAUG;IACG,mBAAmB,CACrB,iBAAiB,CAAC,EAAE,iBAAiB,EACrC,OAAO,CAAC,EAAE,uBAAuB,GAAI,OAAO,CAAC,IAAI,CAAC;IAwBtD;;;;;;OAMG;IACG,iBAAiB,CAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAmBzE;;;;;;;OAOG;YACW,aAAa;IAoC3B;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;;;;;;;OASG;IACH;;;;;;OAMG;IACI,YAAY,IAAI,IAAI;IAI3B,OAAO,CAAC,YAAY;IA6BpB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAUlB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;;;;;;;OAQG;IACH,OAAO,CAAC,0BAA0B;IAkBlC;;;;OAIG;IACH,OAAO,CAAC,UAAU;CAQnB"}