@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.
- package/dist/ifc-creator-math.d.ts +23 -0
- package/dist/ifc-creator-math.d.ts.map +1 -0
- package/dist/ifc-creator-math.js +50 -0
- package/dist/ifc-creator-math.js.map +1 -0
- package/dist/ifc-creator.d.ts +63 -1
- package/dist/ifc-creator.d.ts.map +1 -1
- package/dist/ifc-creator.js +222 -41
- package/dist/ifc-creator.js.map +1 -1
- package/dist/in-store/_emit-helpers.d.ts +52 -0
- package/dist/in-store/_emit-helpers.d.ts.map +1 -0
- package/dist/in-store/_emit-helpers.js +147 -0
- package/dist/in-store/_emit-helpers.js.map +1 -0
- package/dist/in-store/anchor.d.ts +29 -0
- package/dist/in-store/anchor.d.ts.map +1 -0
- package/dist/in-store/anchor.js +5 -0
- package/dist/in-store/anchor.js.map +1 -0
- package/dist/in-store/auto-space-detect.d.ts +68 -0
- package/dist/in-store/auto-space-detect.d.ts.map +1 -0
- package/dist/in-store/auto-space-detect.js +393 -0
- package/dist/in-store/auto-space-detect.js.map +1 -0
- package/dist/in-store/beam.d.ts +25 -0
- package/dist/in-store/beam.d.ts.map +1 -0
- package/dist/in-store/beam.js +119 -0
- package/dist/in-store/beam.js.map +1 -0
- package/dist/in-store/column.d.ts +42 -0
- package/dist/in-store/column.d.ts.map +1 -0
- package/dist/in-store/column.js +108 -0
- package/dist/in-store/column.js.map +1 -0
- package/dist/in-store/door.d.ts +44 -0
- package/dist/in-store/door.d.ts.map +1 -0
- package/dist/in-store/door.js +68 -0
- package/dist/in-store/door.js.map +1 -0
- package/dist/in-store/duplicate.d.ts +100 -0
- package/dist/in-store/duplicate.d.ts.map +1 -0
- package/dist/in-store/duplicate.js +122 -0
- package/dist/in-store/duplicate.js.map +1 -0
- package/dist/in-store/extract-walls.d.ts +80 -0
- package/dist/in-store/extract-walls.d.ts.map +1 -0
- package/dist/in-store/extract-walls.js +522 -0
- package/dist/in-store/extract-walls.js.map +1 -0
- package/dist/in-store/generate-spaces.d.ts +71 -0
- package/dist/in-store/generate-spaces.d.ts.map +1 -0
- package/dist/in-store/generate-spaces.js +76 -0
- package/dist/in-store/generate-spaces.js.map +1 -0
- package/dist/in-store/member.d.ts +32 -0
- package/dist/in-store/member.d.ts.map +1 -0
- package/dist/in-store/member.js +35 -0
- package/dist/in-store/member.js.map +1 -0
- package/dist/in-store/plate.d.ts +43 -0
- package/dist/in-store/plate.d.ts.map +1 -0
- package/dist/in-store/plate.js +33 -0
- package/dist/in-store/plate.js.map +1 -0
- package/dist/in-store/resolve-anchor.d.ts +12 -0
- package/dist/in-store/resolve-anchor.d.ts.map +1 -0
- package/dist/in-store/resolve-anchor.js +125 -0
- package/dist/in-store/resolve-anchor.js.map +1 -0
- package/dist/in-store/resolve-source.d.ts +19 -0
- package/dist/in-store/resolve-source.d.ts.map +1 -0
- package/dist/in-store/resolve-source.js +203 -0
- package/dist/in-store/resolve-source.js.map +1 -0
- package/dist/in-store/roof.d.ts +43 -0
- package/dist/in-store/roof.d.ts.map +1 -0
- package/dist/in-store/roof.js +33 -0
- package/dist/in-store/roof.js.map +1 -0
- package/dist/in-store/slab.d.ts +44 -0
- package/dist/in-store/slab.d.ts.map +1 -0
- package/dist/in-store/slab.js +142 -0
- package/dist/in-store/slab.js.map +1 -0
- package/dist/in-store/space.d.ts +43 -0
- package/dist/in-store/space.d.ts.map +1 -0
- package/dist/in-store/space.js +71 -0
- package/dist/in-store/space.js.map +1 -0
- package/dist/in-store/wall.d.ts +27 -0
- package/dist/in-store/wall.d.ts.map +1 -0
- package/dist/in-store/wall.js +121 -0
- package/dist/in-store/wall.js.map +1 -0
- package/dist/in-store/window.d.ts +36 -0
- package/dist/in-store/window.d.ts.map +1 -0
- package/dist/in-store/window.js +57 -0
- package/dist/in-store/window.js.map +1 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -1
- 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 @@
|
|
|
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
|