@ifc-lite/create 2.2.1 → 2.4.0

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 (44) hide show
  1. package/dist/in-store/anchor.d.ts +16 -1
  2. package/dist/in-store/anchor.d.ts.map +1 -1
  3. package/dist/in-store/anchor.js +20 -0
  4. package/dist/in-store/anchor.js.map +1 -1
  5. package/dist/in-store/drawing-markup-geometry.d.ts +46 -0
  6. package/dist/in-store/drawing-markup-geometry.d.ts.map +1 -0
  7. package/dist/in-store/drawing-markup-geometry.js +80 -0
  8. package/dist/in-store/drawing-markup-geometry.js.map +1 -0
  9. package/dist/in-store/drawing-markup-tags.d.ts +29 -0
  10. package/dist/in-store/drawing-markup-tags.d.ts.map +1 -0
  11. package/dist/in-store/drawing-markup-tags.js +31 -0
  12. package/dist/in-store/drawing-markup-tags.js.map +1 -0
  13. package/dist/in-store/drawing-markup.d.ts +184 -0
  14. package/dist/in-store/drawing-markup.d.ts.map +1 -0
  15. package/dist/in-store/drawing-markup.js +196 -0
  16. package/dist/in-store/drawing-markup.js.map +1 -0
  17. package/dist/in-store/extract-walls.d.ts +16 -16
  18. package/dist/in-store/extract-walls.d.ts.map +1 -1
  19. package/dist/in-store/extract-walls.js +31 -289
  20. package/dist/in-store/extract-walls.js.map +1 -1
  21. package/dist/in-store/length-unit-scale.d.ts +34 -0
  22. package/dist/in-store/length-unit-scale.d.ts.map +1 -0
  23. package/dist/in-store/length-unit-scale.js +44 -0
  24. package/dist/in-store/length-unit-scale.js.map +1 -0
  25. package/dist/in-store/placement-frame.d.ts +132 -0
  26. package/dist/in-store/placement-frame.d.ts.map +1 -0
  27. package/dist/in-store/placement-frame.js +266 -0
  28. package/dist/in-store/placement-frame.js.map +1 -0
  29. package/dist/in-store/resolve-anchor.d.ts.map +1 -1
  30. package/dist/in-store/resolve-anchor.js +7 -14
  31. package/dist/in-store/resolve-anchor.js.map +1 -1
  32. package/dist/in-store/resolve-source.d.ts.map +1 -1
  33. package/dist/in-store/resolve-source.js +3 -10
  34. package/dist/in-store/resolve-source.js.map +1 -1
  35. package/dist/in-store/spatial-zone.js +1 -1
  36. package/dist/in-store/storey-plan-frame.d.ts +69 -0
  37. package/dist/in-store/storey-plan-frame.d.ts.map +1 -0
  38. package/dist/in-store/storey-plan-frame.js +157 -0
  39. package/dist/in-store/storey-plan-frame.js.map +1 -0
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -0
  43. package/dist/index.js.map +1 -1
  44. package/package.json +4 -4
@@ -0,0 +1,196 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Translate 2D drawing markup (issue #4153's "save into the model" option)
6
+ * into IFC entities via the additive `StoreEditor` overlay — modelled on
7
+ * `space.ts`'s `addSpaceToStore`.
8
+ *
9
+ * WRITE SIDE ONLY. This module has no UI wiring and does not import or edit
10
+ * `apps/viewer/src/store/slices/drawing2DSlice.ts` — a "save into model"
11
+ * action, and the read-side translator that recognises
12
+ * `DRAWING_MARKUP_OBJECTTYPE` and rehydrates `Drawing2DState`, are both
13
+ * future follow-ups. The param shapes below (`MarkupPoint2D`,
14
+ * `MeasureMarkupParams`, …) intentionally mirror `Drawing2DState`'s arrays
15
+ * structurally without importing `apps/viewer` — a published package
16
+ * (`@ifc-lite/create`) cannot depend on the app.
17
+ *
18
+ * Geometry frame: every point is treated as already resolved into the
19
+ * target `IfcGeometricRepresentationSubContext`'s 2D plane, in METRES —
20
+ * the same "drawing units (typically meters)" `Drawing2DState`'s own
21
+ * comments describe. Reprojecting a plan/section drawing's local 2D
22
+ * coordinates into that frame (accounting for the `SectionConfig` cut
23
+ * plane) is a UI/wiring concern for the future "save into model" action,
24
+ * not this translation layer.
25
+ *
26
+ * Additive by construction, same guarantee `StoreEditor.addEntity` documents:
27
+ * every call only APPENDS overlay entities (new `IfcAnnotation` +
28
+ * geometry + property/quantity sets); nothing existing is read, mutated,
29
+ * or renumbered.
30
+ */
31
+ import { generateIfcGuid } from '@ifc-lite/encoding';
32
+ import { toNativeLength } from './anchor.js';
33
+ import { emitLocalPlacement, ownerHistoryRef } from './_emit-helpers.js';
34
+ import { emitMarkupPolyline, emitMarkupPoint2DPlacement, emitMarkupSubContext, emitMarkupRepresentation, } from './drawing-markup-geometry.js';
35
+ import { DRAWING_MARKUP_OBJECTTYPE, DRAWING_MARKUP_PSET_NAME, DRAWING_MARKUP_QSET_NAME } from './drawing-markup-tags.js';
36
+ export { DRAWING_MARKUP_OBJECTTYPE, DRAWING_MARKUP_PSET_NAME, DRAWING_MARKUP_QSET_NAME, } from './drawing-markup-tags.js';
37
+ /**
38
+ * A drawing-space coordinate may legitimately be negative or zero — the
39
+ * caller's own drawing origin, not this module's to constrain — so this only
40
+ * rejects NaN/Infinity, never a sign check. Same split `spatial-zone.ts`'s
41
+ * `validateZone` uses for its Footprint points, the closest analog: without
42
+ * it, a NaN/Infinity coordinate reaches `IfcCartesianPoint` as the literal
43
+ * `$` inside a mandatory-REAL attribute list — schema-invalid STEP written
44
+ * with no error.
45
+ */
46
+ function assertFinitePoint(functionName, label, p) {
47
+ if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) {
48
+ throw new Error(`${functionName}: ${label} needs a finite point`);
49
+ }
50
+ }
51
+ /**
52
+ * Distance/area/perimeter/extent are derived measurements: never negative,
53
+ * but legitimately zero (a degenerate measurement, e.g. a zero-length
54
+ * measure). So this rejects NaN/Infinity and negative values, not `<= 0`
55
+ * like `_emit-helpers.ts`'s `assertPositiveFinite` (which guards true
56
+ * dimensions — Width, Height — that can never be zero). Without this, a NaN
57
+ * silently becomes a zeroed quantity value on write (`toNativeLength(NaN)`
58
+ * multiplies through to `NaN`, which the STEP writer would still need to
59
+ * catch) rather than failing loudly here.
60
+ */
61
+ function assertFiniteNonNegative(functionName, label, value) {
62
+ if (!Number.isFinite(value) || value < 0) {
63
+ throw new Error(`${functionName}: ${label} must be finite and non-negative`);
64
+ }
65
+ }
66
+ /** IfcAnnotation attribute order: GlobalId, OwnerHistory, Name, Description,
67
+ * ObjectType, ObjectPlacement, Representation — same 7-slot shape across
68
+ * IFC2X3/IFC4/IFC4X3 (schema-registry.ts `IfcAnnotation.allAttributes`). */
69
+ function emitAnnotation(editor, anchor, name, objectType, placementId, productShapeId) {
70
+ return editor.addEntity('IfcAnnotation', [
71
+ generateIfcGuid(anchor.guidRandom),
72
+ ownerHistoryRef(anchor.ownerHistoryId),
73
+ name,
74
+ null,
75
+ objectType,
76
+ `#${placementId}`,
77
+ `#${productShapeId}`,
78
+ ]).expressId;
79
+ }
80
+ /** A polyline annotation for one `Measure2DResult`. Distance is preserved as
81
+ * a LENGTH quantity (native length unit, matching every other in-store
82
+ * builder's LENGTH convention — see `space.ts`'s `GrossPerimeter`) rather
83
+ * than recomputed from the two endpoints on read-back. */
84
+ export function addMeasureMarkupToStore(editor, anchor, contextId, params) {
85
+ assertFinitePoint('addMeasureMarkupToStore', 'start', params.start);
86
+ assertFinitePoint('addMeasureMarkupToStore', 'end', params.end);
87
+ assertFiniteNonNegative('addMeasureMarkupToStore', 'distance', params.distance);
88
+ const n = (metres) => toNativeLength(anchor, metres);
89
+ const placementId = emitLocalPlacement(editor, anchor.storeyPlacementId, [0, 0, 0]);
90
+ const polylineId = emitMarkupPolyline(editor, [[n(params.start.x), n(params.start.y)], [n(params.end.x), n(params.end.y)]], false);
91
+ const { shapeRepId, productShapeId } = emitMarkupRepresentation(editor, contextId, 'Curve2D', polylineId);
92
+ const annotationId = emitAnnotation(editor, anchor, params.Name ?? 'Measurement', DRAWING_MARKUP_OBJECTTYPE.MEASURE, placementId, productShapeId);
93
+ editor.addQuantitySet(annotationId, DRAWING_MARKUP_QSET_NAME, [
94
+ { name: 'Distance', value: n(params.distance), quantityType: 'LENGTH' },
95
+ ]);
96
+ return { annotationId, placementId, polylineId, shapeRepId, productShapeId };
97
+ }
98
+ /** A closed polyline annotation for one `PolygonArea2DResult`. Area/perimeter
99
+ * preserved so a reader doesn't recompute a shoelace sum from re-projected
100
+ * points (area is SI m², not unit-scaled — same split `space.ts` uses for
101
+ * AREA vs LENGTH quantities). */
102
+ export function addPolygonAreaMarkupToStore(editor, anchor, contextId, params) {
103
+ if (params.points.length < 3) {
104
+ throw new Error('addPolygonAreaMarkupToStore: needs at least 3 points');
105
+ }
106
+ for (const [i, point] of params.points.entries()) {
107
+ assertFinitePoint('addPolygonAreaMarkupToStore', `points[${i}]`, point);
108
+ }
109
+ assertFiniteNonNegative('addPolygonAreaMarkupToStore', 'area', params.area);
110
+ assertFiniteNonNegative('addPolygonAreaMarkupToStore', 'perimeter', params.perimeter);
111
+ const n = (metres) => toNativeLength(anchor, metres);
112
+ const placementId = emitLocalPlacement(editor, anchor.storeyPlacementId, [0, 0, 0]);
113
+ const polylineId = emitMarkupPolyline(editor, params.points.map((p) => [n(p.x), n(p.y)]), true);
114
+ const { shapeRepId, productShapeId } = emitMarkupRepresentation(editor, contextId, 'Curve2D', polylineId);
115
+ const annotationId = emitAnnotation(editor, anchor, params.Name ?? 'Area', DRAWING_MARKUP_OBJECTTYPE.POLYGON_AREA, placementId, productShapeId);
116
+ editor.addQuantitySet(annotationId, DRAWING_MARKUP_QSET_NAME, [
117
+ { name: 'Area', value: params.area, quantityType: 'AREA' },
118
+ { name: 'Perimeter', value: n(params.perimeter), quantityType: 'LENGTH' },
119
+ ]);
120
+ return { annotationId, placementId, polylineId, shapeRepId, productShapeId };
121
+ }
122
+ /** An `IfcTextLiteralWithExtent` annotation for one `TextAnnotation2D`. The
123
+ * literal string is the derived value here — it lives in `Literal` itself,
124
+ * the same field the read-side symbolic parser already consumes, so no
125
+ * separate property-set copy is needed (unlike distance/area/perimeter,
126
+ * which have nowhere else to live). `fontSize`/colors are display-only
127
+ * presentation state, not a derived value this issue asks to preserve, and
128
+ * are left for a future presentation-style follow-up. */
129
+ export function addTextMarkupToStore(editor, anchor, contextId, params) {
130
+ assertFinitePoint('addTextMarkupToStore', 'position', params.position);
131
+ const extent = params.extent ?? { sizeX: 1, sizeY: 0.25 };
132
+ assertFiniteNonNegative('addTextMarkupToStore', 'extent.sizeX', extent.sizeX);
133
+ assertFiniteNonNegative('addTextMarkupToStore', 'extent.sizeY', extent.sizeY);
134
+ const n = (metres) => toNativeLength(anchor, metres);
135
+ const placementId = emitLocalPlacement(editor, anchor.storeyPlacementId, [0, 0, 0]);
136
+ const textPlacementId = emitMarkupPoint2DPlacement(editor, [n(params.position.x), n(params.position.y)]);
137
+ const extentId = editor.addEntity('IfcPlanarExtent', [n(extent.sizeX), n(extent.sizeY)]).expressId;
138
+ const textLiteralId = editor.addEntity('IfcTextLiteralWithExtent', [
139
+ params.text,
140
+ `#${textPlacementId}`,
141
+ '.RIGHT.',
142
+ `#${extentId}`,
143
+ params.boxAlignment ?? 'top-left',
144
+ ]).expressId;
145
+ const { shapeRepId, productShapeId } = emitMarkupRepresentation(editor, contextId, 'Annotation2D', textLiteralId);
146
+ const annotationId = emitAnnotation(editor, anchor, params.Name ?? 'Note', DRAWING_MARKUP_OBJECTTYPE.TEXT, placementId, productShapeId);
147
+ return { annotationId, placementId, textLiteralId, shapeRepId, productShapeId };
148
+ }
149
+ /**
150
+ * An `IfcAnnotationFillArea` annotation for one `CloudAnnotation2D`.
151
+ *
152
+ * Chose fill-area over a bare polyline because a revision cloud is
153
+ * semantically a bounded REGION (its two stored points are a rectangle's
154
+ * corners, not an open path), and `IfcAnnotationFillArea` is what the read
155
+ * side's own symbolic parser already recognises as a fill (`AnnotationFill2D`
156
+ * in `apps/viewer/src/lib/overlay-parse/symbolic-shapes.ts`) — a bare
157
+ * polyline would render as an outline only, losing the "this is a cloud
158
+ * region" distinction a reader needs to tell it apart from a measurement.
159
+ */
160
+ export function addCloudMarkupToStore(editor, anchor, contextId, params) {
161
+ const [topLeft, bottomRight] = params.points;
162
+ assertFinitePoint('addCloudMarkupToStore', 'points[0]', topLeft);
163
+ assertFinitePoint('addCloudMarkupToStore', 'points[1]', bottomRight);
164
+ const n = (metres) => toNativeLength(anchor, metres);
165
+ const corners = [
166
+ [n(topLeft.x), n(topLeft.y)],
167
+ [n(bottomRight.x), n(topLeft.y)],
168
+ [n(bottomRight.x), n(bottomRight.y)],
169
+ [n(topLeft.x), n(bottomRight.y)],
170
+ ];
171
+ const placementId = emitLocalPlacement(editor, anchor.storeyPlacementId, [0, 0, 0]);
172
+ const polylineId = emitMarkupPolyline(editor, corners, true);
173
+ const fillAreaId = editor.addEntity('IfcAnnotationFillArea', [`#${polylineId}`, null]).expressId;
174
+ const { shapeRepId, productShapeId } = emitMarkupRepresentation(editor, contextId, 'Annotation2D', fillAreaId);
175
+ const annotationId = emitAnnotation(editor, anchor, params.Name ?? 'Cloud', DRAWING_MARKUP_OBJECTTYPE.CLOUD, placementId, productShapeId);
176
+ editor.addPropertySet(annotationId, DRAWING_MARKUP_PSET_NAME, [
177
+ { name: 'Label', value: params.label, type: 'TEXT' },
178
+ ]);
179
+ return { annotationId, placementId, polylineId, fillAreaId, shapeRepId, productShapeId };
180
+ }
181
+ /**
182
+ * Translate a full `Drawing2DState` markup snapshot into the store overlay:
183
+ * one shared `Annotation` `IfcGeometricRepresentationSubContext` (created
184
+ * fresh under `rootContextId`, the model's root 3D
185
+ * `IfcGeometricRepresentationContext`), then one `IfcAnnotation` per markup
186
+ * item, tagged per {@link DRAWING_MARKUP_OBJECTTYPE}.
187
+ */
188
+ export function addDrawingMarkupToStore(editor, anchor, rootContextId, input, targetView = 'PLAN_VIEW') {
189
+ const subContextId = emitMarkupSubContext(editor, rootContextId, targetView);
190
+ const measures = (input.measure2DResults ?? []).map((m) => addMeasureMarkupToStore(editor, anchor, subContextId, m));
191
+ const polygons = (input.polygonArea2DResults ?? []).map((p) => addPolygonAreaMarkupToStore(editor, anchor, subContextId, p));
192
+ const texts = (input.textAnnotations2D ?? []).map((t) => addTextMarkupToStore(editor, anchor, subContextId, t));
193
+ const clouds = (input.cloudAnnotations2D ?? []).map((c) => addCloudMarkupToStore(editor, anchor, subContextId, c));
194
+ return { subContextId, measures, polygons, texts, clouds };
195
+ }
196
+ //# sourceMappingURL=drawing-markup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawing-markup.js","sourceRoot":"","sources":["../../src/in-store/drawing-markup.ts"],"names":[],"mappings":"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"}
@@ -25,23 +25,10 @@
25
25
  * callers (and the viewer's Auto Spaces UI) can surface why a wall
26
26
  * didn't contribute to the planar graph.
27
27
  */
28
- import { type IfcDataStore, type IfcAttributeValue } from '@ifc-lite/parser';
28
+ import { type IfcDataStore } from '@ifc-lite/parser';
29
29
  import type { Segment, Vec2 } from './auto-space-detect.js';
30
- /**
31
- * Optional overlay reader. If supplied, overlay walls (entities
32
- * created via `editor.addEntity('IfcWall', ...)` since the model was
33
- * parsed) are included alongside the source walls.
34
- */
35
- export interface OverlayWallReader {
36
- /** Iterate every overlay-created entity. */
37
- getNewEntities(): Iterable<{
38
- expressId: number;
39
- type: string;
40
- attributes: IfcAttributeValue[];
41
- }>;
42
- /** Resolve a positional attribute (with mutations applied). */
43
- getAttribute?(expressId: number, index: number): IfcAttributeValue | undefined;
44
- }
30
+ import { type OverlayWallReader } from './placement-frame.js';
31
+ export type { OverlayWallReader };
45
32
  export type WallSkipReason = 'no-source-bytes' | 'wall-not-parsed' | 'no-placement' | 'no-representation' | 'placement-not-resolvable' | 'no-axis-or-rect-profile' | 'zero-length-axis' | 'sloped-axis';
46
33
  export interface WallSkip {
47
34
  wallId: number;
@@ -90,6 +77,19 @@ export declare function extractWallSegmentsForStorey(store: IfcDataStore, storey
90
77
  * the new rooms that overlap an already-present space (per-space dedup), while
91
78
  * still adding rooms an empty part of the floor lacks. Keyed by storey
92
79
  * expressId; storeys with no resolvable space footprints are omitted.
80
+ *
81
+ * If the model's length-unit scale itself can't be trusted, this refuses
82
+ * for the WHOLE store (empty map) rather than dividing every footprint by a
83
+ * wrong factor: `storeyPlanFrame` — the write side of the same round-trip
84
+ * (`useSpaceBake.ts` folds a new room through it, then reads existing
85
+ * footprints back through this function to dedup against) — refuses the
86
+ * same way per storey. A silent `?? 1` here on a genuinely degenerate scale
87
+ * would have scaled every existing footprint to a value the write side had
88
+ * already refused to trust, so the dedup compare would run on stale
89
+ * coordinates with no warning that anything was wrong. An empty map means
90
+ * dedup is skipped, matching the existing `--force` behaviour of this same
91
+ * call site (`generateSpaces` in `generate-spaces-all.ts`) rather than
92
+ * emitting mis-scaled polygons.
93
93
  */
94
94
  export declare function existingSpaceFootprintsByStorey(store: IfcDataStore): Map<number, Vec2[][]>;
95
95
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"extract-walls.d.ts","sourceRoot":"","sources":["../../src/in-store/extract-walls.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,4CAA4C;IAC5C,cAAc,IAAI,QAAQ,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,iBAAiB,EAAE,CAAA;KAAE,CAAC,CAAC;IACjG,+DAA+D;IAC/D,YAAY,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAAC;CAChF;AAED,MAAM,MAAM,cAAc,GACtB,iBAAiB,GACjB,iBAAiB,GACjB,cAAc,GACd,mBAAmB,GACnB,0BAA0B,GAC1B,yBAAyB,GACzB,kBAAkB,GAClB,aAAa,CAAC;AAElB,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,wDAAwD;IACxD,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,eAAe,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IACxC,uDAAuD;IACvD,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAuBD,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,YAAY,EACnB,eAAe,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE,iBAAiB,EAC3B,OAAO,GAAE,0BAA+B,GACvC,oBAAoB,CA6GtB;AAgGD;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAiC1F;AA6sBD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EACzB,QAAQ,EAAE,MAAM,GACf,MAAM,CAIR"}
1
+ {"version":3,"file":"extract-walls.d.ts","sourceRoot":"","sources":["../../src/in-store/extract-walls.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAE5D,OAAO,EAQL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAElC,MAAM,MAAM,cAAc,GACtB,iBAAiB,GACjB,iBAAiB,GACjB,cAAc,GACd,mBAAmB,GACnB,0BAA0B,GAC1B,yBAAyB,GACzB,kBAAkB,GAClB,aAAa,CAAC;AAElB,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,wDAAwD;IACxD,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,eAAe,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IACxC,uDAAuD;IACvD,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAqBD,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,YAAY,EACnB,eAAe,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE,iBAAiB,EAC3B,OAAO,GAAE,0BAA+B,GACvC,oBAAoB,CA+FtB;AAgGD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAkC1F;AA0dD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EACzB,QAAQ,EAAE,MAAM,GACf,MAAM,CAIR"}
@@ -28,7 +28,9 @@
28
28
  * callers (and the viewer's Auto Spaces UI) can surface why a wall
29
29
  * didn't contribute to the planar graph.
30
30
  */
31
- import { EntityExtractor, extractLengthUnitScale, extractMaterialsOnDemand, } from '@ifc-lite/parser';
31
+ import { EntityExtractor, extractMaterialsOnDemand, } from '@ifc-lite/parser';
32
+ import { safeLengthUnitScale } from './length-unit-scale.js';
33
+ import { AXIS_EPS, applyFrame, frameInStoreyFrame, numericAttr, readEntity, readVec3, storeyPlacementChain, } from './placement-frame.js';
32
34
  /**
33
35
  * Element types treated as "wall-like" dividers by default. Extends
34
36
  * the obvious walls with curtain walls (often the only divider on a
@@ -47,7 +49,6 @@ const DEFAULT_DIVIDER_TYPES = new Set([
47
49
  'ifcmember',
48
50
  'ifcrailing',
49
51
  ]);
50
- const AXIS_EPS = 1e-6;
51
52
  export function extractWallSegmentsForStorey(store, storeyExpressId, overlay, options = {}) {
52
53
  const segments = [];
53
54
  const contributing = [];
@@ -59,31 +60,18 @@ export function extractWallSegmentsForStorey(store, storeyExpressId, overlay, op
59
60
  // detector are always in METRES — without this a millimetre model
60
61
  // would produce coords like (31614, 23345) and the panel's
61
62
  // metre-based snap tolerance would be effectively zero.
62
- let lengthUnitScale = 1.0;
63
- if (store.source) {
64
- try {
65
- lengthUnitScale = extractLengthUnitScale(store.source, store.entityIndex);
66
- if (!Number.isFinite(lengthUnitScale) || lengthUnitScale <= 0)
67
- lengthUnitScale = 1.0;
68
- }
69
- catch (error) {
70
- // Keep the metre fallback, but don't hide a THROWN failure — a wrong
71
- // scale silently mis-scales every extracted segment (a millimetre model
72
- // read as metres yields coords like 31614, collapsing the snap
73
- // tolerance). Mirrors resolve-anchor.ts / resolve-source.ts.
74
- //
75
- // Deliberately NOT claiming the case is covered: extractLengthUnitScale
76
- // returns 1.0 in band rather than throwing, so a missing IFCPROJECT or
77
- // UnitsInContext, or a malformed unit declaration, never reaches this
78
- // catch — it sees only a THROWN failure. Those paths are no longer silent
79
- // (#2104: `warnUnknownUnit` in parser/src/unit-extractor.ts warns once
80
- // per model). Still NOT supported: telling "unknown" from "genuinely
81
- // metres" here — both arrive as 1.0, and a warning is not branchable.
82
- // That needs null from a function with 7+ callers; not done, anywhere.
83
- console.warn('extractWallSegmentsForStorey: failed to extract length unit scale; defaulting to metres', error);
84
- lengthUnitScale = 1.0;
85
- }
86
- }
63
+ // Deliberately NOT claiming the case is covered: extractLengthUnitScale
64
+ // returns 1.0 in band rather than throwing, so a missing IFCPROJECT or
65
+ // UnitsInContext, or a malformed unit declaration, never reaches
66
+ // `safeLengthUnitScale`'s catch — it sees only a THROWN failure or a
67
+ // non-finite/non-positive return. Those paths are no longer silent
68
+ // (#2104: `warnUnknownUnit` in parser/src/unit-extractor.ts warns once
69
+ // per model). Still NOT supported: telling "unknown" from "genuinely
70
+ // metres" here — both arrive as 1.0, and a warning is not branchable.
71
+ // That needs null from a function with 7+ callers; not done, anywhere.
72
+ const lengthUnitScale = store.source
73
+ ? safeLengthUnitScale(store.source, store.entityIndex, 'extractWallSegmentsForStorey') ?? 1.0
74
+ : 1.0;
87
75
  log(`length unit scale = ${lengthUnitScale} (raw → metres)`);
88
76
  if (!store.source) {
89
77
  log('no source bytes on data store — extraction cannot run');
@@ -245,13 +233,28 @@ function collectDividerIdsOnStorey(store, extractor, storeyId, dividerTypes, log
245
233
  * the new rooms that overlap an already-present space (per-space dedup), while
246
234
  * still adding rooms an empty part of the floor lacks. Keyed by storey
247
235
  * expressId; storeys with no resolvable space footprints are omitted.
236
+ *
237
+ * If the model's length-unit scale itself can't be trusted, this refuses
238
+ * for the WHOLE store (empty map) rather than dividing every footprint by a
239
+ * wrong factor: `storeyPlanFrame` — the write side of the same round-trip
240
+ * (`useSpaceBake.ts` folds a new room through it, then reads existing
241
+ * footprints back through this function to dedup against) — refuses the
242
+ * same way per storey. A silent `?? 1` here on a genuinely degenerate scale
243
+ * would have scaled every existing footprint to a value the write side had
244
+ * already refused to trust, so the dedup compare would run on stale
245
+ * coordinates with no warning that anything was wrong. An empty map means
246
+ * dedup is skipped, matching the existing `--force` behaviour of this same
247
+ * call site (`generateSpaces` in `generate-spaces-all.ts`) rather than
248
+ * emitting mis-scaled polygons.
248
249
  */
249
250
  export function existingSpaceFootprintsByStorey(store) {
250
251
  const out = new Map();
251
252
  if (!store.source)
252
253
  return out;
253
254
  const extractor = new EntityExtractor(store.source);
254
- const scale = extractLengthUnitScale(store.source, store.entityIndex) ?? 1;
255
+ const scale = safeLengthUnitScale(store.source, store.entityIndex, 'existingSpaceFootprintsByStorey');
256
+ if (scale === null)
257
+ return out;
255
258
  const aggregated = buildRelatingChildrenIndex(store, extractor, 'IFCRELAGGREGATES', 4, 5);
256
259
  const contained = buildRelatingChildrenIndex(store, extractor, 'IFCRELCONTAINEDINSPATIALSTRUCTURE', 5, 4);
257
260
  for (const st of store.getEntitiesByType('IfcBuildingStorey')) {
@@ -660,229 +663,6 @@ function finaliseSegment(start, end, wallId, log, source) {
660
663
  log(`wall #${wallId}: axis (${start[0].toFixed(3)},${start[1].toFixed(3)})→(${end[0].toFixed(3)},${end[1].toFixed(3)}) len=${len.toFixed(3)} source=${source}`);
661
664
  return { segment: { a: start, b: end } };
662
665
  }
663
- /**
664
- * Walk IfcLocalPlacement → IfcAxis2Placement3D → CartesianPoint and read the
665
- * ground-plane origin + RefDirection *of this one placement*, i.e. expressed
666
- * in its own `PlacementRelTo` parent's frame. Returns null when any link is
667
- * missing.
668
- */
669
- function readOwnPlacementFrame(store, extractor, overlay, placementId) {
670
- const placement = readEntity(store, extractor, overlay, placementId);
671
- if (!placement)
672
- return null;
673
- const axisPlacementId = numericAttr(placement.attributes[1]);
674
- if (axisPlacementId === null)
675
- return null;
676
- const axisPlacement = readEntity(store, extractor, overlay, axisPlacementId);
677
- if (!axisPlacement)
678
- return null;
679
- const locationId = numericAttr(axisPlacement.attributes[0]);
680
- const refDirId = numericAttr(axisPlacement.attributes[2]);
681
- if (locationId === null)
682
- return null;
683
- const locationEnt = readEntity(store, extractor, overlay, locationId);
684
- if (!locationEnt)
685
- return null;
686
- const origin = readVec3(locationEnt.attributes[0]);
687
- if (!origin)
688
- return null;
689
- let axisX = [1, 0];
690
- if (refDirId !== null) {
691
- const refDir = readEntity(store, extractor, overlay, refDirId);
692
- if (refDir) {
693
- const dir = readVec3(refDir.attributes[0]);
694
- if (dir) {
695
- const len = Math.hypot(dir[0], dir[1]);
696
- if (len > AXIS_EPS)
697
- axisX = [dir[0] / len, dir[1] / len];
698
- }
699
- }
700
- }
701
- return { origin: [origin[0], origin[1]], axisX };
702
- }
703
- /**
704
- * The storey's OWN placement chain: its `ObjectPlacement` and every ancestor
705
- * reachable from it via `IfcLocalPlacement.PlacementRelTo`, mapped to the
706
- * number of hops from the storey's own placement (`0` for that placement
707
- * itself). Composition stops when it reaches one of these, and the hop count
708
- * says how much of the storey's own chain still separates the stopping point
709
- * from the storey frame — see `frameInStoreyFrame`.
710
- *
711
- * Returns `null` — NOT an empty map — when the storey has no resolvable
712
- * placement, which `IfcProduct.ObjectPlacement` being OPTIONAL makes a
713
- * well-formed possibility. An empty map is not "no storey frame", it is
714
- * "a storey frame the walk can never reach": the walk would find nothing to
715
- * stop on and compose every hop up to the world root, labelling world
716
- * coordinates storey-local. `frameInStoreyFrame` treats `null` as "compose
717
- * nothing" instead — see there.
718
- */
719
- function storeyPlacementChain(store, extractor, overlay, storeyId) {
720
- const chain = new Map();
721
- const storey = readEntity(store, extractor, overlay, storeyId);
722
- if (!storey)
723
- return null;
724
- let id = numericAttr(storey.attributes[5]); // ObjectPlacement
725
- if (id === null)
726
- return null;
727
- while (id !== null && !chain.has(id)) {
728
- chain.set(id, chain.size);
729
- const placement = readEntity(store, extractor, overlay, id);
730
- if (!placement)
731
- break;
732
- id = numericAttr(placement.attributes[0]); // PlacementRelTo
733
- }
734
- return chain;
735
- }
736
- /**
737
- * The storey's frame expressed in the frame of the chain entry `hops` steps
738
- * above its own placement: compose the storey's first `hops` placements, the
739
- * innermost one first. `hops === 0` is the storey's own placement, whose frame
740
- * relative to itself is the identity.
741
- *
742
- * Returns `null` when any of those placements has no readable frame — the
743
- * caller must then leave the element where it is rather than move it by a
744
- * partial chain.
745
- */
746
- function storeyFrameAboveBy(store, extractor, overlay, storeyChain, hops) {
747
- let frame = { origin: [0, 0], axisX: [1, 0] };
748
- let composed = 0;
749
- for (const id of storeyChain.keys()) {
750
- if (composed >= hops)
751
- break;
752
- const own = readOwnPlacementFrame(store, extractor, overlay, id);
753
- if (!own)
754
- return null;
755
- frame = composeFrames(own, frame);
756
- composed += 1;
757
- }
758
- return composed === hops ? frame : null;
759
- }
760
- /**
761
- * The inverse of a planar rigid frame: `invertFrame(f)` maps a point expressed
762
- * in `f`'s parent frame back into `f`'s own frame, so
763
- * `composeFrames(invertFrame(f), g)` re-expresses `g` — a sibling of `f` in
764
- * that parent frame — relative to `f`.
765
- *
766
- * With `axisX = (c, s)` the frame applies `p ↦ R·p + origin`, `R = [[c,−s],
767
- * [s,c]]`. The inverse applies `p ↦ Rᵀ·(p − origin)`, and `Rᵀ` is the rotation
768
- * whose `axisX` is `(c, −s)`, which is why only the Y component flips.
769
- */
770
- function invertFrame(frame) {
771
- const c = frame.axisX[0];
772
- const s = frame.axisX[1];
773
- const [ox, oy] = frame.origin;
774
- return { origin: [-(c * ox + s * oy), -(-s * ox + c * oy)], axisX: [c, -s] };
775
- }
776
- /**
777
- * Compose `inner` (expressed in `outer`'s frame) with `outer`, giving the
778
- * frame of `inner` expressed in whatever frame `outer` is expressed in.
779
- */
780
- function composeFrames(outer, inner) {
781
- const ax = outer.axisX[0];
782
- const ay = outer.axisX[1];
783
- // Perpendicular = rotate axisX 90° CCW around +Z — same convention as
784
- // `applyFrame`, which this must agree with exactly.
785
- const px = -ay;
786
- const py = ax;
787
- return {
788
- origin: applyFrame(outer, inner.origin),
789
- // Directions rotate but do not translate.
790
- axisX: [ax * inner.axisX[0] + px * inner.axisX[1], ay * inner.axisX[0] + py * inner.axisX[1]],
791
- };
792
- }
793
- /**
794
- * Frame of `placementId` expressed in the STOREY's frame: this placement's
795
- * own frame composed with every intermediate `IfcLocalPlacement.
796
- * PlacementRelTo` hop, stopping *before* the storey's own placement.
797
- *
798
- * An element's `ObjectPlacement` is not always one hop from its storey. The
799
- * IFC-standard grouping pattern — an `IfcElementAssembly` for a curtain
800
- * wall, precast panel run or railing system, all of them in
801
- * `DEFAULT_DIVIDER_TYPES` — inserts an intermediate `IfcLocalPlacement`
802
- * between the member and the storey. Reading only the member's own
803
- * `RelativePlacement` silently drops that hop's translation and rotation, so
804
- * the member is extracted at the wrong position relative to walls placed
805
- * directly under the storey: corners that should meet come out disconnected
806
- * and the enclosed room is never detected.
807
- *
808
- * Composition deliberately STOPS at the storey rather than continuing to the
809
- * root. Storey-local is the frame the write side uses: `generateSpacesFromWalls`
810
- * hands these segments to `addSpaceToStore`, which authors the new `IfcSpace`
811
- * with `anchor.storeyPlacementId` as its `PlacementRelTo` (see `space.ts` and
812
- * `resolve-anchor.ts`). Composing to the root would bake the storey's own
813
- * offset into the coordinates and the storey placement would then apply it a
814
- * second time, putting the space a whole site-offset away from its room.
815
- *
816
- * A `null` `storeyChain` (storey with no `ObjectPlacement`) means there is no
817
- * storey frame to compose towards, so no hop is composed at all.
818
- */
819
- function frameInStoreyFrame(store, extractor, overlay, placementId, storeyChain) {
820
- let frame = readOwnPlacementFrame(store, extractor, overlay, placementId);
821
- if (!frame)
822
- return null;
823
- // No storey placement at all (`ObjectPlacement` is OPTIONAL): there is no
824
- // storey frame to compose towards and nothing for the walk to stop on, so
825
- // walking parents would run to the world root and return world coordinates
826
- // where every caller expects storey-local ones. Compose nothing — the
827
- // element's own frame, which is what this returned before the chain walk
828
- // existed — and leave the element where its own placement puts it rather
829
- // than silently moving it by the site and building offsets.
830
- if (!storeyChain)
831
- return frame;
832
- // Visited-set termination: exact for cyclic/self-referential
833
- // `PlacementRelTo` in malformed IFC, and needs no arbitrary depth bound.
834
- const visited = new Set([placementId]);
835
- let currentId = placementId;
836
- for (;;) {
837
- const placement = readEntity(store, extractor, overlay, currentId);
838
- const relToId = placement ? numericAttr(placement.attributes[0]) : null;
839
- // The walk joins the storey's own chain (or runs out of parents at the
840
- // root, where the storey's chain also ends). `frame` is now expressed in
841
- // that shared ancestor's frame, and the storey sits `hops` placements
842
- // below it — `hops === 0` when the ancestor IS the storey's placement,
843
- // the common shape, where the frame is already storey-local.
844
- if (relToId === null || storeyChain.has(relToId)) {
845
- const hops = relToId === null ? storeyChain.size : storeyChain.get(relToId);
846
- if (hops === 0)
847
- return frame;
848
- // #3003: the wall joined the chain ABOVE the storey, so the storey's own
849
- // hops are in `frame` and every caller reads the result as storey-local.
850
- // Divide them out — the INVERSE of the storey's frame in the shared
851
- // ancestor, applied to the wall's frame in that same ancestor. Applying
852
- // the storey frame itself instead would double the offset rather than
853
- // remove it, so the two directions land on different coordinates and the
854
- // #3003 fixture pins which one this is.
855
- const storeyFrame = storeyFrameAboveBy(store, extractor, overlay, storeyChain, hops);
856
- return storeyFrame ? composeFrames(invertFrame(storeyFrame), frame) : frame;
857
- }
858
- // A cycle in a malformed `PlacementRelTo`: nothing above is trustworthy,
859
- // so stop with what has been composed so far.
860
- if (visited.has(relToId))
861
- return frame;
862
- const parent = readOwnPlacementFrame(store, extractor, overlay, relToId);
863
- if (!parent)
864
- return frame;
865
- frame = composeFrames(parent, frame);
866
- visited.add(relToId);
867
- currentId = relToId;
868
- }
869
- }
870
- /**
871
- * Apply a placement frame to a storey-local 2D point. The point's X is
872
- * along the wall's local axis; Y is perpendicular (perpendicular to
873
- * the wall direction in the ground plane).
874
- */
875
- function applyFrame(frame, local) {
876
- const ax = frame.axisX[0];
877
- const ay = frame.axisX[1];
878
- // Perpendicular = rotate axisX 90° CCW around +Z.
879
- const px = -ay;
880
- const py = ax;
881
- return [
882
- frame.origin[0] + ax * local[0] + px * local[1],
883
- frame.origin[1] + ay * local[0] + py * local[1],
884
- ];
885
- }
886
666
  /**
887
667
  * Walk the wall's representations, looking for the standard `Axis`
888
668
  * representation. Returns the first two vertices of the first
@@ -1003,34 +783,6 @@ export function resolveEntityTypeName(store, entity, entityId) {
1003
783
  const name = (fromTable && fromTable !== 'Unknown' ? fromTable : entity.type) ?? '';
1004
784
  return name.toLowerCase();
1005
785
  }
1006
- function readEntity(store, extractor, overlay, expressId) {
1007
- const ref = store.entityIndex.byId.get(expressId);
1008
- if (ref && ref.byteLength > 0 && ref.byteOffset >= 0) {
1009
- return extractor.extractEntity(ref);
1010
- }
1011
- // Overlay-only entity: fall back to the overlay reader.
1012
- if (overlay) {
1013
- for (const ent of overlay.getNewEntities()) {
1014
- if (ent.expressId === expressId) {
1015
- return { type: ent.type, attributes: ent.attributes };
1016
- }
1017
- }
1018
- }
1019
- return null;
1020
- }
1021
- function numericAttr(v) {
1022
- if (typeof v === 'number')
1023
- return v;
1024
- if (typeof v === 'string') {
1025
- if (v.startsWith('#')) {
1026
- const n = Number(v.slice(1));
1027
- return Number.isFinite(n) ? n : null;
1028
- }
1029
- const n = Number(v);
1030
- return Number.isFinite(n) ? n : null;
1031
- }
1032
- return null;
1033
- }
1034
786
  function stringAttr(v) {
1035
787
  if (typeof v === 'string') {
1036
788
  // Strip STEP single quotes if present (parser sometimes returns them, sometimes not).
@@ -1040,14 +792,4 @@ function stringAttr(v) {
1040
792
  }
1041
793
  return null;
1042
794
  }
1043
- function readVec3(v) {
1044
- if (!Array.isArray(v) || v.length < 2)
1045
- return null;
1046
- const x = numericAttr(v[0]);
1047
- const y = numericAttr(v[1]);
1048
- const z = v.length >= 3 ? numericAttr(v[2]) : 0;
1049
- if (x === null || y === null || z === null)
1050
- return null;
1051
- return [x, y, z];
1052
- }
1053
795
  //# sourceMappingURL=extract-walls.js.map