@ifc-lite/create 1.14.5 → 1.15.1

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 (87) hide show
  1. package/dist/ifc-creator-math.d.ts +23 -0
  2. package/dist/ifc-creator-math.d.ts.map +1 -0
  3. package/dist/ifc-creator-math.js +50 -0
  4. package/dist/ifc-creator-math.js.map +1 -0
  5. package/dist/ifc-creator.d.ts +63 -1
  6. package/dist/ifc-creator.d.ts.map +1 -1
  7. package/dist/ifc-creator.js +222 -41
  8. package/dist/ifc-creator.js.map +1 -1
  9. package/dist/in-store/_emit-helpers.d.ts +52 -0
  10. package/dist/in-store/_emit-helpers.d.ts.map +1 -0
  11. package/dist/in-store/_emit-helpers.js +147 -0
  12. package/dist/in-store/_emit-helpers.js.map +1 -0
  13. package/dist/in-store/anchor.d.ts +29 -0
  14. package/dist/in-store/anchor.d.ts.map +1 -0
  15. package/dist/in-store/anchor.js +5 -0
  16. package/dist/in-store/anchor.js.map +1 -0
  17. package/dist/in-store/auto-space-detect.d.ts +68 -0
  18. package/dist/in-store/auto-space-detect.d.ts.map +1 -0
  19. package/dist/in-store/auto-space-detect.js +393 -0
  20. package/dist/in-store/auto-space-detect.js.map +1 -0
  21. package/dist/in-store/beam.d.ts +25 -0
  22. package/dist/in-store/beam.d.ts.map +1 -0
  23. package/dist/in-store/beam.js +119 -0
  24. package/dist/in-store/beam.js.map +1 -0
  25. package/dist/in-store/column.d.ts +42 -0
  26. package/dist/in-store/column.d.ts.map +1 -0
  27. package/dist/in-store/column.js +108 -0
  28. package/dist/in-store/column.js.map +1 -0
  29. package/dist/in-store/door.d.ts +44 -0
  30. package/dist/in-store/door.d.ts.map +1 -0
  31. package/dist/in-store/door.js +68 -0
  32. package/dist/in-store/door.js.map +1 -0
  33. package/dist/in-store/duplicate.d.ts +100 -0
  34. package/dist/in-store/duplicate.d.ts.map +1 -0
  35. package/dist/in-store/duplicate.js +122 -0
  36. package/dist/in-store/duplicate.js.map +1 -0
  37. package/dist/in-store/extract-walls.d.ts +80 -0
  38. package/dist/in-store/extract-walls.d.ts.map +1 -0
  39. package/dist/in-store/extract-walls.js +522 -0
  40. package/dist/in-store/extract-walls.js.map +1 -0
  41. package/dist/in-store/generate-spaces.d.ts +71 -0
  42. package/dist/in-store/generate-spaces.d.ts.map +1 -0
  43. package/dist/in-store/generate-spaces.js +76 -0
  44. package/dist/in-store/generate-spaces.js.map +1 -0
  45. package/dist/in-store/member.d.ts +32 -0
  46. package/dist/in-store/member.d.ts.map +1 -0
  47. package/dist/in-store/member.js +35 -0
  48. package/dist/in-store/member.js.map +1 -0
  49. package/dist/in-store/plate.d.ts +43 -0
  50. package/dist/in-store/plate.d.ts.map +1 -0
  51. package/dist/in-store/plate.js +33 -0
  52. package/dist/in-store/plate.js.map +1 -0
  53. package/dist/in-store/resolve-anchor.d.ts +12 -0
  54. package/dist/in-store/resolve-anchor.d.ts.map +1 -0
  55. package/dist/in-store/resolve-anchor.js +125 -0
  56. package/dist/in-store/resolve-anchor.js.map +1 -0
  57. package/dist/in-store/resolve-source.d.ts +19 -0
  58. package/dist/in-store/resolve-source.d.ts.map +1 -0
  59. package/dist/in-store/resolve-source.js +203 -0
  60. package/dist/in-store/resolve-source.js.map +1 -0
  61. package/dist/in-store/roof.d.ts +43 -0
  62. package/dist/in-store/roof.d.ts.map +1 -0
  63. package/dist/in-store/roof.js +33 -0
  64. package/dist/in-store/roof.js.map +1 -0
  65. package/dist/in-store/slab.d.ts +44 -0
  66. package/dist/in-store/slab.d.ts.map +1 -0
  67. package/dist/in-store/slab.js +142 -0
  68. package/dist/in-store/slab.js.map +1 -0
  69. package/dist/in-store/space.d.ts +43 -0
  70. package/dist/in-store/space.d.ts.map +1 -0
  71. package/dist/in-store/space.js +71 -0
  72. package/dist/in-store/space.js.map +1 -0
  73. package/dist/in-store/wall.d.ts +27 -0
  74. package/dist/in-store/wall.d.ts.map +1 -0
  75. package/dist/in-store/wall.js +121 -0
  76. package/dist/in-store/wall.js.map +1 -0
  77. package/dist/in-store/window.d.ts +36 -0
  78. package/dist/in-store/window.d.ts.map +1 -0
  79. package/dist/in-store/window.js +57 -0
  80. package/dist/in-store/window.js.map +1 -0
  81. package/dist/index.d.ts +18 -1
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +18 -0
  84. package/dist/index.js.map +1 -1
  85. package/dist/types.d.ts +96 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/package.json +10 -6
@@ -0,0 +1,147 @@
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
+ * Shared sub-graph emitters for the in-store element builders.
6
+ *
7
+ * Every IFC element that lands on a storey shares the same prologue
8
+ * (IfcCartesianPoint → IfcAxis2Placement3D → IfcLocalPlacement) and
9
+ * the same epilogue (IfcShapeRepresentation → IfcProductDefinitionShape
10
+ * → IfcRelContainedInSpatialStructure). Extracting those into pure
11
+ * helpers keeps each builder focused on the one part that's actually
12
+ * unique — the profile + element-line attribute order.
13
+ *
14
+ * All helpers operate purely through the StoreEditor; no parser
15
+ * access, no I/O.
16
+ */
17
+ import { generateIfcGuid } from '@ifc-lite/encoding';
18
+ const POINT_EPSILON = 1e-6;
19
+ /**
20
+ * Emit an IfcLocalPlacement chained to a parent. Wraps the cartesian
21
+ * point + axis-placement bookkeeping. Pass `Axis` and/or `RefDirection`
22
+ * as `[x, y, z]` to override defaults (otherwise IFC fills them with
23
+ * world up / world X).
24
+ */
25
+ export function emitLocalPlacement(editor, parentPlacementId, location, axis, refDirection) {
26
+ const originPt = editor.addEntity('IfcCartesianPoint', [location]).expressId;
27
+ const axisRef = axis !== undefined
28
+ ? `#${editor.addEntity('IfcDirection', [axis]).expressId}`
29
+ : null;
30
+ const refDirRef = refDirection !== undefined
31
+ ? `#${editor.addEntity('IfcDirection', [refDirection]).expressId}`
32
+ : null;
33
+ const axisPlacement = editor.addEntity('IfcAxis2Placement3D', [
34
+ `#${originPt}`,
35
+ axisRef,
36
+ refDirRef,
37
+ ]).expressId;
38
+ return editor.addEntity('IfcLocalPlacement', [
39
+ `#${parentPlacementId}`,
40
+ `#${axisPlacement}`,
41
+ ]).expressId;
42
+ }
43
+ /**
44
+ * Emit a centred rectangle profile. `centerX`/`centerY` shift the
45
+ * profile's local origin — useful for slab-style "spans 0..W × 0..D"
46
+ * placements where the centre sits at (W/2, D/2).
47
+ */
48
+ export function emitRectangleProfile(editor, width, depth, centerX = 0, centerY = 0) {
49
+ const originPt = editor.addEntity('IfcCartesianPoint', [[centerX, centerY]]).expressId;
50
+ const pos = editor.addEntity('IfcAxis2Placement2D', [`#${originPt}`, null]).expressId;
51
+ return editor.addEntity('IfcRectangleProfileDef', [
52
+ '.AREA.',
53
+ null,
54
+ `#${pos}`,
55
+ width,
56
+ depth,
57
+ ]).expressId;
58
+ }
59
+ /**
60
+ * Emit an arbitrary closed profile from a 2D polyline. Auto-closes if
61
+ * the input doesn't already terminate at the start point.
62
+ */
63
+ export function emitPolygonProfile(editor, curve) {
64
+ if (curve.length < 3) {
65
+ throw new Error('emitPolygonProfile: outline needs at least 3 points');
66
+ }
67
+ const first = curve[0];
68
+ const last = curve[curve.length - 1];
69
+ const closed = Math.abs(first[0] - last[0]) < POINT_EPSILON &&
70
+ Math.abs(first[1] - last[1]) < POINT_EPSILON;
71
+ const sequence = closed ? curve : [...curve, first];
72
+ const pointIds = sequence.map((pt) => editor.addEntity('IfcCartesianPoint', [[pt[0], pt[1]]]).expressId);
73
+ const polylineId = editor.addEntity('IfcPolyline', [pointIds.map((id) => `#${id}`)]).expressId;
74
+ return editor.addEntity('IfcArbitraryClosedProfileDef', [
75
+ '.AREA.',
76
+ null,
77
+ `#${polylineId}`,
78
+ ]).expressId;
79
+ }
80
+ /**
81
+ * Emit an IfcExtrudedAreaSolid extruding `profileId` along local +Z
82
+ * for `depth` metres. Standard prologue for any swept-solid element.
83
+ */
84
+ export function emitExtrudedSolid(editor, profileId, depth) {
85
+ const originPt = editor.addEntity('IfcCartesianPoint', [[0, 0, 0]]).expressId;
86
+ const axis = editor.addEntity('IfcAxis2Placement3D', [`#${originPt}`, null, null]).expressId;
87
+ const direction = editor.addEntity('IfcDirection', [[0, 0, 1]]).expressId;
88
+ return editor.addEntity('IfcExtrudedAreaSolid', [
89
+ `#${profileId}`,
90
+ `#${axis}`,
91
+ `#${direction}`,
92
+ depth,
93
+ ]).expressId;
94
+ }
95
+ /**
96
+ * Emit a "Body" IfcShapeRepresentation + IfcProductDefinitionShape
97
+ * pair from a single solid. Returns both ids so callers can record
98
+ * them in their build result for downstream tooling.
99
+ */
100
+ export function emitBodyRepresentation(editor, bodyContextId, solidId) {
101
+ const shapeRepId = editor.addEntity('IfcShapeRepresentation', [
102
+ `#${bodyContextId}`,
103
+ 'Body',
104
+ 'SweptSolid',
105
+ [`#${solidId}`],
106
+ ]).expressId;
107
+ const productShapeId = editor.addEntity('IfcProductDefinitionShape', [
108
+ null,
109
+ null,
110
+ [`#${shapeRepId}`],
111
+ ]).expressId;
112
+ return { shapeRepId, productShapeId };
113
+ }
114
+ /**
115
+ * Emit a fresh IfcRelContainedInSpatialStructure that anchors a single
116
+ * element to its storey. Easier than mutating an existing rel — STEP
117
+ * importers fold parallel rels back into one container at parse time.
118
+ */
119
+ export function emitRelContainedInSpatialStructure(editor, ownerHistoryId, elementId, storeyId) {
120
+ return editor.addEntity('IfcRelContainedInSpatialStructure', [
121
+ generateIfcGuid(),
122
+ `#${ownerHistoryId}`,
123
+ null,
124
+ null,
125
+ [`#${elementId}`],
126
+ `#${storeyId}`,
127
+ ]).expressId;
128
+ }
129
+ /**
130
+ * Build the leading attributes shared by every IfcElement subclass
131
+ * (GlobalId → OwnerHistory → Name → Description → ObjectType →
132
+ * ObjectPlacement → Representation → Tag). Callers append their
133
+ * type-specific tail (PredefinedType, OperationType, etc.).
134
+ */
135
+ export function ifcElementHeader(ownerHistoryId, placementId, productShapeId, params, defaultName) {
136
+ return [
137
+ generateIfcGuid(),
138
+ `#${ownerHistoryId}`,
139
+ params.Name ?? defaultName,
140
+ params.Description ?? null,
141
+ params.ObjectType ?? null,
142
+ `#${placementId}`,
143
+ `#${productShapeId}`,
144
+ params.Tag ?? null,
145
+ ];
146
+ }
147
+ //# sourceMappingURL=_emit-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_emit-helpers.js","sourceRoot":"","sources":["../../src/in-store/_emit-helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Spatial anchor for in-store builders — the set of references that any
3
+ * element being added to an existing parsed model needs in order to slot
4
+ * into the existing IFC graph correctly.
5
+ *
6
+ * Resolution from a parsed `IfcDataStore` lives in the backend layer
7
+ * (where `@ifc-lite/parser` is already a dependency); the builder
8
+ * functions in this module operate purely on these resolved ids.
9
+ */
10
+ export type SpatialAnchorSchema = 'IFC2X3' | 'IFC4' | 'IFC4X3' | 'IFC5';
11
+ export interface SpatialAnchor {
12
+ /** IfcOwnerHistory expressId — referenced by every IfcRoot. */
13
+ ownerHistoryId: number;
14
+ /** IfcGeometricRepresentationSubContext for 'Body' (or its IfcGeometricRepresentationContext fallback). */
15
+ bodyContextId: number;
16
+ /** IfcGeometricRepresentationSubContext for 'Axis' (or its IfcGeometricRepresentationContext fallback). */
17
+ axisContextId: number;
18
+ /** The target IfcBuildingStorey expressId. */
19
+ storeyId: number;
20
+ /** The IfcLocalPlacement that the storey itself sits on. New element placements are chained from this. */
21
+ storeyPlacementId: number;
22
+ /**
23
+ * Target schema. Builders use this to decide which optional STEP arguments
24
+ * to emit — e.g. `IfcColumn.PredefinedType` only exists from IFC4 onward.
25
+ * Defaults to `'IFC4'` when unset for backward compatibility.
26
+ */
27
+ schema?: SpatialAnchorSchema;
28
+ }
29
+ //# sourceMappingURL=anchor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../../src/in-store/anchor.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;IACvB,2GAA2G;IAC3G,aAAa,EAAE,MAAM,CAAC;IACtB,2GAA2G;IAC3G,aAAa,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,0GAA0G;IAC1G,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B"}
@@ -0,0 +1,5 @@
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
+ export {};
5
+ //# sourceMappingURL=anchor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchor.js","sourceRoot":"","sources":["../../src/in-store/anchor.ts"],"names":[],"mappings":"AAAA;;+DAE+D"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Detect enclosed regions from a set of 2D wall axis segments.
3
+ *
4
+ * Pipeline:
5
+ * 1. Snap close vertices within `snapTolerance` (collapses tiny gaps
6
+ * between wall ends that should meet at a corner).
7
+ * 2. Resolve pairwise segment intersections — each crossing splits
8
+ * both segments into shorter pieces meeting at the new vertex.
9
+ * 3. Build a half-edge graph (DCEL): every undirected segment
10
+ * becomes two opposing directed half-edges; per vertex, the
11
+ * half-edges leaving it are ordered by polar angle so we can
12
+ * find the next CCW-around-a-face neighbour in O(1).
13
+ * 4. Walk minimum cycles by always taking the leftmost turn. Each
14
+ * half-edge belongs to exactly one face cycle.
15
+ * 5. Drop the outer (unbounded) face — the one with the most-
16
+ * negative signed area.
17
+ * 6. Filter the remaining faces by `minArea`.
18
+ *
19
+ * Pure: no IFC dependencies. Output is a list of CCW polygons
20
+ * (`outline`) plus the signed area of each. Callers feed these into
21
+ * the per-storey IfcSpace builder.
22
+ */
23
+ export type Vec2 = [number, number];
24
+ export interface Segment {
25
+ a: Vec2;
26
+ b: Vec2;
27
+ }
28
+ export interface DetectedSpace {
29
+ /** CCW outline (no implicit closing edge — first vertex isn't repeated). */
30
+ outline: Vec2[];
31
+ /** Absolute polygon area, m². */
32
+ area: number;
33
+ }
34
+ export interface DetectOptions {
35
+ /** Distance below which two endpoints are merged. Default 0.05 m. */
36
+ snapTolerance?: number;
37
+ /** Faces below this area are dropped. Default 0.5 m². */
38
+ minArea?: number;
39
+ /**
40
+ * When true, the detector emits `console.debug` messages tracing the
41
+ * pipeline (vertex/edge counts, face areas, drop reasons). Surfaces
42
+ * the data needed to diagnose "no enclosed regions detected" without
43
+ * touching the algorithm.
44
+ */
45
+ debug?: boolean;
46
+ }
47
+ export interface DetectStats {
48
+ inputSegments: number;
49
+ vertices: number;
50
+ segmentsAfterSplit: number;
51
+ edges: number;
52
+ faces: number;
53
+ outerFacesDropped: number;
54
+ belowMinAreaDropped: number;
55
+ /** Largest detected interior face area (m²). 0 when no face passed. */
56
+ largestArea: number;
57
+ }
58
+ export declare function detectEnclosedAreas(segments: Segment[], options?: DetectOptions): DetectedSpace[];
59
+ /**
60
+ * Same pipeline as `detectEnclosedAreas`, but returns the per-stage
61
+ * counts alongside the spaces so callers can surface diagnostic
62
+ * information (used by the orchestrator + viewer Auto Spaces panel).
63
+ */
64
+ export declare function detectEnclosedAreasWithStats(segments: Segment[], options?: DetectOptions): {
65
+ spaces: DetectedSpace[];
66
+ stats: DetectStats;
67
+ };
68
+ //# sourceMappingURL=auto-space-detect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-space-detect.d.ts","sourceRoot":"","sources":["../../src/in-store/auto-space-detect.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEpC,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,IAAI,CAAC;IACR,CAAC,EAAE,IAAI,CAAC;CACT;AAED,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,OAAO,EAAE,IAAI,EAAE,CAAC;IAChB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;CACrB;AA8BD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,GAAE,aAAkB,GAC1B,aAAa,EAAE,CAEjB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,GAAE,aAAkB,GAC1B;IAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAyUjD"}
@@ -0,0 +1,393 @@
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
+ const DEFAULT_SNAP = 0.05;
5
+ const DEFAULT_MIN_AREA = 0.5;
6
+ const EPS = 1e-9;
7
+ export function detectEnclosedAreas(segments, options = {}) {
8
+ return detectEnclosedAreasWithStats(segments, options).spaces;
9
+ }
10
+ /**
11
+ * Same pipeline as `detectEnclosedAreas`, but returns the per-stage
12
+ * counts alongside the spaces so callers can surface diagnostic
13
+ * information (used by the orchestrator + viewer Auto Spaces panel).
14
+ */
15
+ export function detectEnclosedAreasWithStats(segments, options = {}) {
16
+ const snap = options.snapTolerance ?? DEFAULT_SNAP;
17
+ const minArea = options.minArea ?? DEFAULT_MIN_AREA;
18
+ const debug = !!options.debug;
19
+ const log = debug ? (...args) => console.debug('[auto-space-detect]', ...args) : () => { };
20
+ const stats = {
21
+ inputSegments: segments.length,
22
+ vertices: 0,
23
+ segmentsAfterSplit: 0,
24
+ edges: 0,
25
+ faces: 0,
26
+ outerFacesDropped: 0,
27
+ belowMinAreaDropped: 0,
28
+ largestArea: 0,
29
+ };
30
+ log(`input: ${segments.length} segments, snapTolerance=${snap}, minArea=${minArea}`);
31
+ if (segments.length < 3) {
32
+ log('input below 3 segments — no faces possible');
33
+ return { spaces: [], stats };
34
+ }
35
+ // ── 1. Snap endpoints ──
36
+ // Spatial hash keyed on a snap-sized grid so endpoint resolution stays
37
+ // O(1) average instead of O(N) linear scans. We probe the cell + its 8
38
+ // neighbours so a query point near a cell boundary still finds matches
39
+ // on the other side.
40
+ const vertices = [];
41
+ const cellSize = Math.max(snap, EPS);
42
+ const grid = new Map();
43
+ const cellKey = (cx, cy) => `${cx},${cy}`;
44
+ const lookup = (pt) => {
45
+ const snapSq2 = snap * snap;
46
+ const cx = Math.floor(pt[0] / cellSize);
47
+ const cy = Math.floor(pt[1] / cellSize);
48
+ for (let dx = -1; dx <= 1; dx++) {
49
+ for (let dy = -1; dy <= 1; dy++) {
50
+ const bucket = grid.get(cellKey(cx + dx, cy + dy));
51
+ if (!bucket)
52
+ continue;
53
+ for (const id of bucket) {
54
+ const ddx = vertices[id].pt[0] - pt[0];
55
+ const ddy = vertices[id].pt[1] - pt[1];
56
+ if (ddx * ddx + ddy * ddy <= snapSq2)
57
+ return id;
58
+ }
59
+ }
60
+ }
61
+ const id = vertices.length;
62
+ vertices.push({ id, pt: [pt[0], pt[1]] });
63
+ const key = cellKey(cx, cy);
64
+ const bucket = grid.get(key);
65
+ if (bucket)
66
+ bucket.push(id);
67
+ else
68
+ grid.set(key, [id]);
69
+ return id;
70
+ };
71
+ // Initial vertex set: every endpoint, snapped.
72
+ const indexedSegs = [];
73
+ for (const seg of segments) {
74
+ const ai = lookup(seg.a);
75
+ const bi = lookup(seg.b);
76
+ if (ai === bi)
77
+ continue; // zero-length, post-snap
78
+ indexedSegs.push([ai, bi]);
79
+ }
80
+ log(`after snap: ${vertices.length} vertices, ${indexedSegs.length} segments`);
81
+ // ── 1b. Snap dangling endpoints onto nearby edge interiors ──
82
+ // Walls extracted from real IFC files often DON'T share corner
83
+ // vertices: each wall's axis runs centreline-to-centreline, but
84
+ // adjacent perpendicular walls have axes ending at the inside
85
+ // face of the partner wall — so the endpoints land on each
86
+ // other's interior, not at the same point. A pure endpoint snap
87
+ // misses this; we project each unique endpoint onto every nearby
88
+ // segment and, when within snap tolerance, mark the projection
89
+ // as the canonical vertex (and queue the host segment to be
90
+ // split there).
91
+ const splitSegs = [];
92
+ for (let i = 0; i < indexedSegs.length; i++) {
93
+ splitSegs.push([...indexedSegs[i]]);
94
+ }
95
+ const snapSq = snap * snap;
96
+ let tjunctionPasses = 0;
97
+ let tjunctionsApplied = false;
98
+ do {
99
+ tjunctionsApplied = false;
100
+ tjunctionPasses++;
101
+ // Snapshot endpoints we need to test — segs grow during the loop,
102
+ // but the new pieces share endpoints with the originals so we
103
+ // don't have to re-scan them.
104
+ const endpointIds = new Set();
105
+ for (const [a, b] of splitSegs) {
106
+ endpointIds.add(a);
107
+ endpointIds.add(b);
108
+ }
109
+ for (const vid of endpointIds) {
110
+ const p = vertices[vid].pt;
111
+ for (let s = 0; s < splitSegs.length; s++) {
112
+ const [a, b] = splitSegs[s];
113
+ if (a === vid || b === vid)
114
+ continue;
115
+ const proj = closestPointOnSegment(p, vertices[a].pt, vertices[b].pt);
116
+ if (!proj)
117
+ continue;
118
+ const dx = proj.point[0] - p[0];
119
+ const dy = proj.point[1] - p[1];
120
+ if (dx * dx + dy * dy > snapSq)
121
+ continue;
122
+ // Strictly interior — skip projections that land on the
123
+ // segment endpoints (those are handled by the regular vertex
124
+ // snap and would degenerate the split).
125
+ if (proj.t < 1e-6 || proj.t > 1 - 1e-6)
126
+ continue;
127
+ // Insert the dangling endpoint as the split vertex (its
128
+ // coords are already in `vertices[vid]`); split the host edge.
129
+ splitSegs[s] = [a, vid];
130
+ splitSegs.push([vid, b]);
131
+ tjunctionsApplied = true;
132
+ break;
133
+ }
134
+ if (tjunctionsApplied)
135
+ break;
136
+ }
137
+ } while (tjunctionsApplied && tjunctionPasses < Math.max(50, indexedSegs.length * 5));
138
+ log(`T-junction snap: ${tjunctionPasses} pass(es)`);
139
+ // Collect every interior crossing in a single O(N²) pass, recording
140
+ // the parametric position along each host segment, then split each
141
+ // segment at all of its collected crossings in one go. Splits don't
142
+ // alter geometry — they only subdivide — so further passes are
143
+ // unnecessary. Replaces the previous "split one pair, restart the
144
+ // whole scan" loop, which was O(N³) on dense wall sets.
145
+ //
146
+ // For each original segment we keep a sorted list of (t, vertexId).
147
+ // Endpoints (t=0 and t=1) are the existing endpoint vertex ids.
148
+ const seedSegs = splitSegs.slice();
149
+ const segSplits = seedSegs.map(([a, b]) => [
150
+ { t: 0, v: a },
151
+ { t: 1, v: b },
152
+ ]);
153
+ // Optional bbox-based pruning: skip pair checks whose AABBs miss.
154
+ // Keep simple — cost is dominated by segmentIntersection which already
155
+ // returns null for non-crossings; the bbox pre-check is just to avoid
156
+ // the math in the easy 90% case.
157
+ const segBBoxes = seedSegs.map(([a, b]) => {
158
+ const ax = vertices[a].pt[0], ay = vertices[a].pt[1];
159
+ const bx = vertices[b].pt[0], by = vertices[b].pt[1];
160
+ return {
161
+ minX: Math.min(ax, bx),
162
+ maxX: Math.max(ax, bx),
163
+ minY: Math.min(ay, by),
164
+ maxY: Math.max(ay, by),
165
+ };
166
+ });
167
+ for (let i = 0; i < seedSegs.length; i++) {
168
+ const [ai, bi] = seedSegs[i];
169
+ const bi_box = segBBoxes[i];
170
+ for (let j = i + 1; j < seedSegs.length; j++) {
171
+ const [aj, bj] = seedSegs[j];
172
+ if (ai === aj || ai === bj || bi === aj || bi === bj)
173
+ continue;
174
+ const bj_box = segBBoxes[j];
175
+ if (bi_box.maxX < bj_box.minX || bj_box.maxX < bi_box.minX ||
176
+ bi_box.maxY < bj_box.minY || bj_box.maxY < bi_box.minY)
177
+ continue;
178
+ const ip = segmentIntersectionParam(vertices[ai].pt, vertices[bi].pt, vertices[aj].pt, vertices[bj].pt);
179
+ if (!ip)
180
+ continue;
181
+ const newIdx = lookup(ip.point);
182
+ const isI_endpoint = newIdx === ai || newIdx === bi;
183
+ const isJ_endpoint = newIdx === aj || newIdx === bj;
184
+ if (!isI_endpoint)
185
+ segSplits[i].push({ t: ip.t, v: newIdx });
186
+ if (!isJ_endpoint)
187
+ segSplits[j].push({ t: ip.u, v: newIdx });
188
+ }
189
+ }
190
+ splitSegs.length = 0;
191
+ for (const splits of segSplits) {
192
+ if (splits.length <= 2) {
193
+ // No interior crossings — keep the segment as-is.
194
+ splitSegs.push([splits[0].v, splits[1].v]);
195
+ continue;
196
+ }
197
+ splits.sort((a, b) => a.t - b.t);
198
+ for (let k = 0; k < splits.length - 1; k++) {
199
+ const a = splits[k].v;
200
+ const b = splits[k + 1].v;
201
+ if (a !== b)
202
+ splitSegs.push([a, b]);
203
+ }
204
+ }
205
+ // Deduplicate (a, b) and (b, a) pairs.
206
+ const undirected = new Set();
207
+ const finalSegs = [];
208
+ for (const [a, b] of splitSegs) {
209
+ if (a === b)
210
+ continue;
211
+ const key = a < b ? `${a}-${b}` : `${b}-${a}`;
212
+ if (undirected.has(key))
213
+ continue;
214
+ undirected.add(key);
215
+ finalSegs.push([a, b]);
216
+ }
217
+ stats.vertices = vertices.length;
218
+ stats.segmentsAfterSplit = finalSegs.length;
219
+ log(`after intersect-split: ${finalSegs.length} unique edges`);
220
+ if (finalSegs.length < 3) {
221
+ log('after split: fewer than 3 edges — no faces possible');
222
+ return { spaces: [], stats };
223
+ }
224
+ // ── 3. Build half-edge graph ──
225
+ const edges = [];
226
+ const vertexEdges = vertices.map(() => []);
227
+ for (const [a, b] of finalSegs) {
228
+ const dxA = vertices[b].pt[0] - vertices[a].pt[0];
229
+ const dyA = vertices[b].pt[1] - vertices[a].pt[1];
230
+ const fwd = edges.length;
231
+ const bwd = edges.length + 1;
232
+ edges.push({
233
+ id: fwd,
234
+ origin: a,
235
+ dest: b,
236
+ twin: bwd,
237
+ angle: Math.atan2(dyA, dxA),
238
+ face: -1,
239
+ next: -1,
240
+ dx: dxA,
241
+ dy: dyA,
242
+ });
243
+ edges.push({
244
+ id: bwd,
245
+ origin: b,
246
+ dest: a,
247
+ twin: fwd,
248
+ angle: Math.atan2(-dyA, -dxA),
249
+ face: -1,
250
+ next: -1,
251
+ dx: -dxA,
252
+ dy: -dyA,
253
+ });
254
+ vertexEdges[a].push(fwd);
255
+ vertexEdges[b].push(bwd);
256
+ }
257
+ // Sort each vertex's outgoing edges by angle so we can compute
258
+ // "next around face" via the leftmost-turn rule in O(1).
259
+ for (const list of vertexEdges) {
260
+ list.sort((p, q) => edges[p].angle - edges[q].angle);
261
+ }
262
+ // ── 4. Walk faces ──
263
+ // Around a face (CCW interior), the next half-edge after entering
264
+ // a vertex along edge `e` is the half-edge whose origin is the
265
+ // entered vertex AND whose direction is the *clockwise* neighbour
266
+ // of e.twin's direction in the cyclic angle ordering.
267
+ //
268
+ // prev = e
269
+ // v = e.dest
270
+ // fanIdx = position of e.twin in vertexEdges[v]
271
+ // next = vertexEdges[v][(fanIdx - 1 + len) % len]
272
+ for (const e of edges) {
273
+ if (e.next !== -1)
274
+ continue;
275
+ const v = e.dest;
276
+ const fan = vertexEdges[v];
277
+ const idx = fan.indexOf(e.twin);
278
+ if (idx < 0)
279
+ continue; // structurally impossible, but defensive
280
+ const nextIdx = (idx - 1 + fan.length) % fan.length;
281
+ e.next = fan[nextIdx];
282
+ }
283
+ let faceCount = 0;
284
+ const faceCycles = [];
285
+ for (const e of edges) {
286
+ if (e.face !== -1)
287
+ continue;
288
+ const cycle = [];
289
+ let cur = e.id;
290
+ let safety = 0;
291
+ while (cur !== -1 && edges[cur].face === -1 && safety++ < edges.length + 4) {
292
+ edges[cur].face = faceCount;
293
+ cycle.push(cur);
294
+ cur = edges[cur].next;
295
+ if (cur === e.id)
296
+ break;
297
+ }
298
+ faceCycles.push(cycle);
299
+ faceCount++;
300
+ }
301
+ const faceAreas = faceCycles.map((cycle, idx) => {
302
+ let signed = 0;
303
+ for (const eid of cycle) {
304
+ const eg = edges[eid];
305
+ const p = vertices[eg.origin].pt;
306
+ const q = vertices[eg.dest].pt;
307
+ signed += p[0] * q[1] - q[0] * p[1];
308
+ }
309
+ signed *= 0.5;
310
+ return { idx, signed, area: Math.abs(signed) };
311
+ });
312
+ stats.edges = edges.length;
313
+ stats.faces = faceCycles.length;
314
+ log(`half-edge graph: ${edges.length} half-edges, ${faceCycles.length} faces total`);
315
+ // ── 6. Drop outer faces + filter by min area + emit CCW outlines ──
316
+ // With the leftmost-turn walk every interior (enclosed) face winds
317
+ // CCW (signed area > 0); the unbounded face surrounding each
318
+ // connected component winds CW (signed area < 0). Drop the
319
+ // negatives — that handles the multi-component case naturally,
320
+ // since each component contributes its own outer face.
321
+ const out = [];
322
+ for (const f of faceAreas) {
323
+ if (f.signed <= 0) {
324
+ stats.outerFacesDropped++;
325
+ continue;
326
+ }
327
+ if (f.area < minArea) {
328
+ stats.belowMinAreaDropped++;
329
+ log(`face #${f.idx}: dropped (area=${f.area.toFixed(3)} < minArea=${minArea})`);
330
+ continue;
331
+ }
332
+ const cycle = faceCycles[f.idx];
333
+ const outline = cycle.map((eid) => {
334
+ const v = vertices[edges[eid].origin].pt;
335
+ return [v[0], v[1]];
336
+ });
337
+ out.push({ outline, area: f.area });
338
+ if (f.area > stats.largestArea)
339
+ stats.largestArea = f.area;
340
+ }
341
+ // Stable sort: largest area first so the UI shows "main rooms" up top.
342
+ out.sort((a, b) => b.area - a.area);
343
+ log(`detected ${out.length} interior region(s); dropped ${stats.outerFacesDropped} outer + ${stats.belowMinAreaDropped} below min-area`);
344
+ return { spaces: out, stats };
345
+ }
346
+ /**
347
+ * Closest point on segment ab to a query point q, plus the
348
+ * parametric distance `t ∈ [0, 1]` along ab. Returns null for
349
+ * zero-length segments.
350
+ */
351
+ function closestPointOnSegment(q, a, b) {
352
+ const dx = b[0] - a[0];
353
+ const dy = b[1] - a[1];
354
+ const len2 = dx * dx + dy * dy;
355
+ if (len2 < 1e-12)
356
+ return null;
357
+ let t = ((q[0] - a[0]) * dx + (q[1] - a[1]) * dy) / len2;
358
+ if (t < 0)
359
+ t = 0;
360
+ else if (t > 1)
361
+ t = 1;
362
+ return { point: [a[0] + t * dx, a[1] + t * dy], t };
363
+ }
364
+ /**
365
+ * Proper-segment intersection test in 2D. Returns the crossing point
366
+ * plus the parametric positions on both segments when they cross
367
+ * inside both (excluding shared endpoints at parameter 0 or 1, which
368
+ * produce no new vertex). Uses a small parametric tolerance so two
369
+ * near-coincident endpoints don't register as a fresh interior crossing.
370
+ */
371
+ function segmentIntersectionParam(p1, p2, p3, p4) {
372
+ const x1 = p1[0], y1 = p1[1];
373
+ const x2 = p2[0], y2 = p2[1];
374
+ const x3 = p3[0], y3 = p3[1];
375
+ const x4 = p4[0], y4 = p4[1];
376
+ const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
377
+ if (Math.abs(denom) < EPS)
378
+ return null; // parallel / coincident
379
+ const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
380
+ const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
381
+ // Allow exact endpoints (t == 0 / 1) so a T-junction registers and
382
+ // splits the through-segment, but skip when both segments meet
383
+ // *only* at a shared endpoint (no new vertex needed).
384
+ const tol = 1e-7;
385
+ if (t < -tol || t > 1 + tol)
386
+ return null;
387
+ if (u < -tol || u > 1 + tol)
388
+ return null;
389
+ if ((t < tol || t > 1 - tol) && (u < tol || u > 1 - tol))
390
+ return null;
391
+ return { point: [x1 + t * (x2 - x1), y1 + t * (y2 - y1)], t, u };
392
+ }
393
+ //# sourceMappingURL=auto-space-detect.js.map