@ifc-lite/viewer 1.8.0 → 1.10.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.
- package/CHANGELOG.md +77 -0
- package/dist/assets/{Arrow.dom-CwcRxist.js → Arrow.dom-Bw5JMdDs.js} +1 -1
- package/dist/assets/browser-DdRf3aWl.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/ifc-lite_bg-C1-gLAHo.wasm +0 -0
- package/dist/assets/index-1ff6P0kc.js +100011 -0
- package/dist/assets/index-Bz7vHRxl.js +216 -0
- package/dist/assets/index-mvbV6NHd.css +1 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-5LbrYh3R.js → native-bridge-C5hD5vae.js} +1 -1
- package/dist/assets/{wasm-bridge-CgpLtj1h.js → wasm-bridge-CaNKXFGM.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/MainToolbar.tsx +31 -3
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -2
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +3 -1
- package/src/components/viewer/useAnimationLoop.ts +4 -1
- package/src/components/viewer/useGeometryStreaming.ts +13 -1
- package/src/components/viewer/useRenderUpdates.ts +6 -1
- package/src/hooks/useKeyboardShortcuts.ts +1 -0
- package/src/hooks/useLens.ts +2 -1
- package/src/hooks/useSandbox.ts +113 -0
- package/src/hooks/useViewerSelectors.ts +22 -0
- package/src/index.css +6 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +30 -2
- package/src/store/index.ts +24 -1
- package/src/store/slices/pinboardSlice.ts +37 -41
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +43 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/index-7WoQ-qVC.css +0 -1
- package/dist/assets/index-BSANf7-H.js +0 -78795
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export {} // module boundary (stripped by transpiler)
|
|
2
|
+
|
|
3
|
+
// ── Space & Room Validation ─────────────────────────────────────────────
|
|
4
|
+
// Stakeholder: Facility Manager / Architect
|
|
5
|
+
//
|
|
6
|
+
// Validates IfcSpace entities for operations handover readiness. Checks
|
|
7
|
+
// that every space has a Name, LongName, area and volume quantities,
|
|
8
|
+
// and required properties. Generates a room schedule with measurements
|
|
9
|
+
// and flags incomplete spaces. This would take hours of clicking through
|
|
10
|
+
// individual spaces in the property panel.
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
bim.viewer.resetColors()
|
|
14
|
+
bim.viewer.resetVisibility()
|
|
15
|
+
|
|
16
|
+
const spaces = bim.query.byType('IfcSpace')
|
|
17
|
+
|
|
18
|
+
if (spaces.length === 0) {
|
|
19
|
+
console.warn('No IfcSpace entities found in this model.')
|
|
20
|
+
console.log('')
|
|
21
|
+
console.log('This script validates room/space data. Models without')
|
|
22
|
+
console.log('IfcSpace entities are missing spatial programming data.')
|
|
23
|
+
console.log('')
|
|
24
|
+
// Show what spatial types exist
|
|
25
|
+
const spatialTypes = bim.query.byType('IfcBuildingStorey', 'IfcBuilding', 'IfcSite')
|
|
26
|
+
if (spatialTypes.length > 0) {
|
|
27
|
+
console.log('Spatial structure found:')
|
|
28
|
+
for (const e of spatialTypes) {
|
|
29
|
+
console.log(' ' + e.Type + ': ' + (e.Name || '<unnamed>'))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw new Error('no spaces')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── 1. Extract space data ───────────────────────────────────────────────
|
|
36
|
+
interface SpaceData {
|
|
37
|
+
entity: BimEntity
|
|
38
|
+
area: number | null
|
|
39
|
+
volume: number | null
|
|
40
|
+
perimeter: number | null
|
|
41
|
+
height: number | null
|
|
42
|
+
longName: string | null
|
|
43
|
+
category: string | null
|
|
44
|
+
occupancy: string | null
|
|
45
|
+
issues: string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const spaceData: SpaceData[] = []
|
|
49
|
+
// Collect property/quantity paths for CSV export
|
|
50
|
+
const spacePropPaths = new Set<string>()
|
|
51
|
+
const spaceQtyPaths = new Set<string>()
|
|
52
|
+
|
|
53
|
+
for (const space of spaces) {
|
|
54
|
+
const data: SpaceData = {
|
|
55
|
+
entity: space, area: null, volume: null, perimeter: null, height: null,
|
|
56
|
+
longName: null, category: null, occupancy: null, issues: []
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract quantities
|
|
60
|
+
const qsets = bim.query.quantities(space)
|
|
61
|
+
for (const qset of qsets) {
|
|
62
|
+
for (const q of qset.quantities) {
|
|
63
|
+
const lower = q.name.toLowerCase()
|
|
64
|
+
if (lower.includes('area') && !lower.includes('wall') && data.area === null) data.area = q.value
|
|
65
|
+
if (lower.includes('volume') && data.volume === null) data.volume = q.value
|
|
66
|
+
if (lower.includes('perimeter') && data.perimeter === null) data.perimeter = q.value
|
|
67
|
+
if (lower.includes('height') && data.height === null) data.height = q.value
|
|
68
|
+
if (q.value !== null && q.value !== 0) spaceQtyPaths.add(qset.name + '.' + q.name)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract properties
|
|
73
|
+
const psets = bim.query.properties(space)
|
|
74
|
+
for (const pset of psets) {
|
|
75
|
+
for (const p of pset.properties) {
|
|
76
|
+
const lower = p.name.toLowerCase()
|
|
77
|
+
if (lower === 'longname' && p.value) data.longName = String(p.value)
|
|
78
|
+
if (lower === 'category' && p.value) data.category = String(p.value)
|
|
79
|
+
if (lower === 'occupancytype' && p.value) data.occupancy = String(p.value)
|
|
80
|
+
if (p.value !== null && p.value !== '') spacePropPaths.add(pset.name + '.' + p.name)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Also check entity attributes
|
|
85
|
+
if (!data.longName && space.Description) data.longName = space.Description
|
|
86
|
+
|
|
87
|
+
// Check for issues
|
|
88
|
+
if (!space.Name || space.Name === '') data.issues.push('Missing Name')
|
|
89
|
+
if (!data.longName) data.issues.push('Missing LongName/Description')
|
|
90
|
+
if (data.area === null) data.issues.push('Missing Area')
|
|
91
|
+
if (data.volume === null) data.issues.push('Missing Volume')
|
|
92
|
+
|
|
93
|
+
spaceData.push(data)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── 2. Classify spaces ─────────────────────────────────────────────────
|
|
97
|
+
const complete = spaceData.filter(s => s.issues.length === 0)
|
|
98
|
+
const incomplete = spaceData.filter(s => s.issues.length > 0)
|
|
99
|
+
|
|
100
|
+
// ── 3. Color-code ───────────────────────────────────────────────────────
|
|
101
|
+
const batches: Array<{ entities: BimEntity[]; color: string }> = []
|
|
102
|
+
if (complete.length > 0) batches.push({ entities: complete.map(s => s.entity), color: '#27ae60' })
|
|
103
|
+
const minor = incomplete.filter(s => s.issues.length <= 2)
|
|
104
|
+
const major = incomplete.filter(s => s.issues.length > 2)
|
|
105
|
+
if (minor.length > 0) batches.push({ entities: minor.map(s => s.entity), color: '#f39c12' })
|
|
106
|
+
if (major.length > 0) batches.push({ entities: major.map(s => s.entity), color: '#e74c3c' })
|
|
107
|
+
if (batches.length > 0) bim.viewer.colorizeAll(batches)
|
|
108
|
+
|
|
109
|
+
// ── 4. Report ───────────────────────────────────────────────────────────
|
|
110
|
+
console.log('═══════════════════════════════════════')
|
|
111
|
+
console.log(' SPACE & ROOM VALIDATION')
|
|
112
|
+
console.log('═══════════════════════════════════════')
|
|
113
|
+
console.log('')
|
|
114
|
+
console.log('Spaces found: ' + spaces.length)
|
|
115
|
+
console.log(' Complete: ' + complete.length + ' ● green')
|
|
116
|
+
console.log(' Minor issues: ' + minor.length + ' ● orange (1-2 issues)')
|
|
117
|
+
console.log(' Major issues: ' + major.length + ' ● red (3+ issues)')
|
|
118
|
+
|
|
119
|
+
// ── 5. Room schedule ────────────────────────────────────────────────────
|
|
120
|
+
console.log('')
|
|
121
|
+
console.log('── Room Schedule ──')
|
|
122
|
+
console.log('Name | Area m² | Volume m³ | Height m | Status')
|
|
123
|
+
console.log('------------------------+----------+-----------+----------+-------')
|
|
124
|
+
|
|
125
|
+
// Sort by name
|
|
126
|
+
const sorted = [...spaceData].sort((a, b) => {
|
|
127
|
+
const nameA = a.entity.Name || 'zzz'
|
|
128
|
+
const nameB = b.entity.Name || 'zzz'
|
|
129
|
+
return nameA.localeCompare(nameB)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
let totalArea = 0
|
|
133
|
+
let totalVolume = 0
|
|
134
|
+
|
|
135
|
+
for (const s of sorted) {
|
|
136
|
+
const name = ((s.entity.Name || '<unnamed>') + ' ').slice(0, 24)
|
|
137
|
+
const area = s.area !== null ? (s.area.toFixed(1) + ' ').slice(0, 8) : '- '
|
|
138
|
+
const vol = s.volume !== null ? (s.volume.toFixed(1) + ' ').slice(0, 9) : '- '
|
|
139
|
+
const height = s.height !== null ? (s.height.toFixed(2) + ' ').slice(0, 8) : '- '
|
|
140
|
+
const status = s.issues.length === 0 ? 'OK' : s.issues.length + ' issues'
|
|
141
|
+
console.log(name + '| ' + area + ' | ' + vol + ' | ' + height + ' | ' + status)
|
|
142
|
+
if (s.area !== null) totalArea += s.area
|
|
143
|
+
if (s.volume !== null) totalVolume += s.volume
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('------------------------+----------+-----------+----------+-------')
|
|
147
|
+
console.log('TOTALS | ' + (totalArea.toFixed(1) + ' ').slice(0, 8) + ' | ' + (totalVolume.toFixed(1) + ' ').slice(0, 9) + ' |')
|
|
148
|
+
|
|
149
|
+
// ── 6. Category breakdown ───────────────────────────────────────────────
|
|
150
|
+
const categories: Record<string, { count: number; area: number }> = {}
|
|
151
|
+
for (const s of spaceData) {
|
|
152
|
+
const cat = s.category || s.entity.ObjectType || 'Uncategorized'
|
|
153
|
+
if (!categories[cat]) categories[cat] = { count: 0, area: 0 }
|
|
154
|
+
categories[cat].count++
|
|
155
|
+
if (s.area !== null) categories[cat].area += s.area
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (Object.keys(categories).length > 1) {
|
|
159
|
+
console.log('')
|
|
160
|
+
console.log('── By Category ──')
|
|
161
|
+
for (const [cat, data] of Object.entries(categories).sort((a, b) => b[1].area - a[1].area)) {
|
|
162
|
+
console.log(' ' + cat + ': ' + data.count + ' spaces, ' + data.area.toFixed(1) + ' m²')
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── 7. Issue details ────────────────────────────────────────────────────
|
|
167
|
+
if (incomplete.length > 0) {
|
|
168
|
+
console.log('')
|
|
169
|
+
console.warn('── Incomplete Spaces ──')
|
|
170
|
+
const issueCount: Record<string, number> = {}
|
|
171
|
+
for (const s of incomplete) {
|
|
172
|
+
for (const issue of s.issues) {
|
|
173
|
+
issueCount[issue] = (issueCount[issue] || 0) + 1
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const [issue, count] of Object.entries(issueCount).sort((a, b) => b[1] - a[1])) {
|
|
177
|
+
console.warn(' ' + issue + ': ' + count + ' spaces')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 8. Export ────────────────────────────────────────────────────────────
|
|
182
|
+
const spPropCols = Array.from(spacePropPaths).sort().slice(0, 15)
|
|
183
|
+
const spQtyCols = Array.from(spaceQtyPaths).sort().slice(0, 15)
|
|
184
|
+
bim.export.csv(spaces, {
|
|
185
|
+
columns: ['Name', 'Type', 'GlobalId', 'Description', 'ObjectType', ...spPropCols, ...spQtyCols],
|
|
186
|
+
filename: 'room-schedule.csv'
|
|
187
|
+
})
|
|
188
|
+
console.log('')
|
|
189
|
+
console.log('Exported ' + spaces.length + ' spaces (' + (5 + spPropCols.length + spQtyCols.length) + ' columns) to room-schedule.csv')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"skipLibCheck": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["bim-globals.d.ts", "*.ts"],
|
|
12
|
+
"exclude": ["tsconfig.json"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Built-in script templates for the script editor.
|
|
7
|
+
*
|
|
8
|
+
* Templates are real .ts files in ./templates/ that are type-checked
|
|
9
|
+
* against the bim-globals.d.ts declaration. They are loaded as raw
|
|
10
|
+
* strings via Vite's ?raw import and served to the sandbox transpiler.
|
|
11
|
+
*
|
|
12
|
+
* Each template targets a specific stakeholder and combines multiple
|
|
13
|
+
* API calls into automated workflows that go beyond what the UI can
|
|
14
|
+
* do through manual clicking.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Raw source imports — Vite returns the file content as a string
|
|
18
|
+
import dataQualityAudit from './templates/data-quality-audit.ts?raw';
|
|
19
|
+
import fireSafetyCheck from './templates/fire-safety-check.ts?raw';
|
|
20
|
+
import quantityTakeoff from './templates/quantity-takeoff.ts?raw';
|
|
21
|
+
import envelopeCheck from './templates/envelope-check.ts?raw';
|
|
22
|
+
import mepEquipmentSchedule from './templates/mep-equipment-schedule.ts?raw';
|
|
23
|
+
import spaceValidation from './templates/space-validation.ts?raw';
|
|
24
|
+
import federationCompare from './templates/federation-compare.ts?raw';
|
|
25
|
+
import resetView from './templates/reset-view.ts?raw';
|
|
26
|
+
|
|
27
|
+
export interface ScriptTemplate {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
code: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Strip the `export {}` module boundary line that enables type checking */
|
|
34
|
+
function stripModuleLine(raw: string): string {
|
|
35
|
+
return raw.replace(/^export \{\}[^\n]*\n\n?/, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const SCRIPT_TEMPLATES: ScriptTemplate[] = [
|
|
39
|
+
{
|
|
40
|
+
name: 'Data quality audit',
|
|
41
|
+
description:
|
|
42
|
+
'BIM Manager — scan all entities for missing names, properties, and quantities; score model completeness; color-code by data quality',
|
|
43
|
+
code: stripModuleLine(dataQualityAudit),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Fire safety compliance',
|
|
47
|
+
description:
|
|
48
|
+
'Architect — check fire ratings across walls, doors, and slabs; flag load-bearing elements without ratings; export non-compliant list',
|
|
49
|
+
code: stripModuleLine(fireSafetyCheck),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Quantity takeoff',
|
|
53
|
+
description:
|
|
54
|
+
'Cost Estimator — aggregate area, volume, and length quantities across all element types; generate material takeoff table and CSV',
|
|
55
|
+
code: stripModuleLine(quantityTakeoff),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Envelope & thermal check',
|
|
59
|
+
description:
|
|
60
|
+
'Energy Consultant — identify external elements, check thermal transmittance values, isolate building envelope, flag missing data',
|
|
61
|
+
code: stripModuleLine(envelopeCheck),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'MEP equipment schedule',
|
|
65
|
+
description:
|
|
66
|
+
'HVAC Engineer — discover all distribution elements, extract equipment properties, generate schedule, isolate and color by system',
|
|
67
|
+
code: stripModuleLine(mepEquipmentSchedule),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Space & room validation',
|
|
71
|
+
description:
|
|
72
|
+
'Facility Manager — validate IfcSpace entities for area, volume, naming; generate room schedule with totals; flag incomplete spaces',
|
|
73
|
+
code: stripModuleLine(spaceValidation),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'Federation comparison',
|
|
77
|
+
description:
|
|
78
|
+
'Project Manager — compare multiple loaded models side by side: entity counts, type coverage, naming consistency, coordination issues',
|
|
79
|
+
code: stripModuleLine(federationCompare),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Reset view',
|
|
83
|
+
description: 'Utility — remove all color overrides and show all entities',
|
|
84
|
+
code: stripModuleLine(resetView),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* BimProvider — React context for the SDK's BimContext.
|
|
7
|
+
*
|
|
8
|
+
* Wraps useBimHost() and makes the `bim` object available to all children
|
|
9
|
+
* via useBim(). This enables gradual migration: components can progressively
|
|
10
|
+
* switch from direct Zustand store calls to SDK calls.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* <BimProvider>
|
|
14
|
+
* <App />
|
|
15
|
+
* </BimProvider>
|
|
16
|
+
*
|
|
17
|
+
* // In any component:
|
|
18
|
+
* const bim = useBim();
|
|
19
|
+
* const walls = bim.query().byType('IfcWall').toArray();
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createContext, useContext, type ReactNode } from 'react';
|
|
23
|
+
import type { BimContext } from '@ifc-lite/sdk';
|
|
24
|
+
import { useBimHost } from './useBimHost.js';
|
|
25
|
+
|
|
26
|
+
const BimReactContext = createContext<BimContext | null>(null);
|
|
27
|
+
|
|
28
|
+
/** Provider that initializes the SDK and makes it available via useBim() */
|
|
29
|
+
export function BimProvider({ children }: { children: ReactNode }) {
|
|
30
|
+
const bim = useBimHost();
|
|
31
|
+
return (
|
|
32
|
+
<BimReactContext.Provider value={bim}>
|
|
33
|
+
{children}
|
|
34
|
+
</BimReactContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Access the BimContext from any component.
|
|
40
|
+
* Must be rendered inside a <BimProvider>.
|
|
41
|
+
*
|
|
42
|
+
* @throws if used outside a BimProvider
|
|
43
|
+
*/
|
|
44
|
+
export function useBim(): BimContext {
|
|
45
|
+
const ctx = useContext(BimReactContext);
|
|
46
|
+
if (!ctx) {
|
|
47
|
+
throw new Error('useBim() must be used within a <BimProvider>');
|
|
48
|
+
}
|
|
49
|
+
return ctx;
|
|
50
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
import type { StoreApi } from './types.js';
|
|
6
|
+
import type { EntityRef, EntityData, PropertySetData, QuantitySetData, ExportBackendMethods } from '@ifc-lite/sdk';
|
|
7
|
+
import { EntityNode } from '@ifc-lite/query';
|
|
8
|
+
import { getModelForRef } from './model-compat.js';
|
|
9
|
+
|
|
10
|
+
/** Options for CSV export */
|
|
11
|
+
interface CsvOptions {
|
|
12
|
+
columns: string[];
|
|
13
|
+
separator?: string;
|
|
14
|
+
filename?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a value is a CsvOptions object.
|
|
19
|
+
*/
|
|
20
|
+
function isCsvOptions(v: unknown): v is CsvOptions {
|
|
21
|
+
if (v === null || typeof v !== 'object' || !('columns' in v)) return false;
|
|
22
|
+
const columns = (v as CsvOptions).columns;
|
|
23
|
+
if (!Array.isArray(columns)) return false;
|
|
24
|
+
// Validate all column entries are strings
|
|
25
|
+
return columns.every((c): c is string => typeof c === 'string');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate that a value is an array of EntityRef objects.
|
|
30
|
+
*/
|
|
31
|
+
function isEntityRefArray(v: unknown): v is EntityRef[] {
|
|
32
|
+
if (!Array.isArray(v)) return false;
|
|
33
|
+
if (v.length === 0) return true;
|
|
34
|
+
const first = v[0] as Record<string, unknown>;
|
|
35
|
+
// Accept both raw EntityRef and entity proxy objects with .ref
|
|
36
|
+
if ('modelId' in first && 'expressId' in first) {
|
|
37
|
+
return typeof first.modelId === 'string' && typeof first.expressId === 'number';
|
|
38
|
+
}
|
|
39
|
+
if ('ref' in first && first.ref !== null && typeof first.ref === 'object') {
|
|
40
|
+
const ref = first.ref as Record<string, unknown>;
|
|
41
|
+
return typeof ref.modelId === 'string' && typeof ref.expressId === 'number';
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize entity refs — entities from the sandbox may be EntityData
|
|
48
|
+
* objects with a .ref property, or raw EntityRef { modelId, expressId }.
|
|
49
|
+
*/
|
|
50
|
+
function normalizeRefs(raw: unknown[]): EntityRef[] {
|
|
51
|
+
return raw.map((item) => {
|
|
52
|
+
const r = item as Record<string, unknown>;
|
|
53
|
+
if (r.ref && typeof r.ref === 'object') {
|
|
54
|
+
return r.ref as EntityRef;
|
|
55
|
+
}
|
|
56
|
+
return { modelId: r.modelId as string, expressId: r.expressId as number };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Escape a CSV cell value — wrap in quotes if it contains the separator,
|
|
62
|
+
* double-quotes, or newlines.
|
|
63
|
+
*/
|
|
64
|
+
function escapeCsv(value: string, sep: string): string {
|
|
65
|
+
if (value.includes(sep) || value.includes('"') || value.includes('\n')) {
|
|
66
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Export adapter — implements CSV and JSON export directly.
|
|
73
|
+
*
|
|
74
|
+
* This adapter resolves entity data by dispatching to the query adapter
|
|
75
|
+
* on the same LocalBackend, providing full export support for both
|
|
76
|
+
* direct dispatch calls and SDK namespace usage.
|
|
77
|
+
*/
|
|
78
|
+
export function createExportAdapter(store: StoreApi): ExportBackendMethods {
|
|
79
|
+
/** Resolve entity data via the query subsystem */
|
|
80
|
+
function getEntityData(ref: EntityRef): EntityData | null {
|
|
81
|
+
const state = store.getState();
|
|
82
|
+
const model = getModelForRef(state, ref.modelId);
|
|
83
|
+
if (!model?.ifcDataStore) return null;
|
|
84
|
+
|
|
85
|
+
const node = new EntityNode(model.ifcDataStore, ref.expressId);
|
|
86
|
+
return {
|
|
87
|
+
ref,
|
|
88
|
+
globalId: node.globalId,
|
|
89
|
+
name: node.name,
|
|
90
|
+
type: node.type,
|
|
91
|
+
description: node.description,
|
|
92
|
+
objectType: node.objectType,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Resolve property sets for an entity */
|
|
97
|
+
function getProperties(ref: EntityRef): PropertySetData[] {
|
|
98
|
+
const state = store.getState();
|
|
99
|
+
const model = getModelForRef(state, ref.modelId);
|
|
100
|
+
if (!model?.ifcDataStore) return [];
|
|
101
|
+
|
|
102
|
+
const node = new EntityNode(model.ifcDataStore, ref.expressId);
|
|
103
|
+
return node.properties().map((pset: { name: string; globalId?: string; properties: Array<{ name: string; type: number; value: string | number | boolean | null }> }) => ({
|
|
104
|
+
name: pset.name,
|
|
105
|
+
globalId: pset.globalId,
|
|
106
|
+
properties: pset.properties.map((p: { name: string; type: number; value: string | number | boolean | null }) => ({
|
|
107
|
+
name: p.name,
|
|
108
|
+
type: p.type,
|
|
109
|
+
value: p.value,
|
|
110
|
+
})),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Resolve quantity sets for an entity */
|
|
115
|
+
function getQuantities(ref: EntityRef): QuantitySetData[] {
|
|
116
|
+
const state = store.getState();
|
|
117
|
+
const model = getModelForRef(state, ref.modelId);
|
|
118
|
+
if (!model?.ifcDataStore) return [];
|
|
119
|
+
|
|
120
|
+
const node = new EntityNode(model.ifcDataStore, ref.expressId);
|
|
121
|
+
return node.quantities().map((qset: { name: string; quantities: Array<{ name: string; type: number; value: number }> }) => ({
|
|
122
|
+
name: qset.name,
|
|
123
|
+
quantities: qset.quantities.map((q: { name: string; type: number; value: number }) => ({
|
|
124
|
+
name: q.name,
|
|
125
|
+
type: q.type,
|
|
126
|
+
value: q.value,
|
|
127
|
+
})),
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Resolve a single column value from entity data + properties + quantities.
|
|
132
|
+
* Accepts both IFC PascalCase (Name, GlobalId) and legacy camelCase (name, globalId).
|
|
133
|
+
* Dot-path columns (e.g. "Pset_WallCommon.FireRating" or "Qto_WallBaseQuantities.GrossVolume")
|
|
134
|
+
* resolve against property sets first, then quantity sets. */
|
|
135
|
+
function resolveColumnValue(
|
|
136
|
+
data: EntityData,
|
|
137
|
+
col: string,
|
|
138
|
+
getProps: () => PropertySetData[],
|
|
139
|
+
getQties: () => QuantitySetData[],
|
|
140
|
+
): string {
|
|
141
|
+
// IFC schema attribute names (PascalCase) + legacy camelCase
|
|
142
|
+
switch (col) {
|
|
143
|
+
case 'Name': case 'name': return data.name;
|
|
144
|
+
case 'Type': case 'type': return data.type;
|
|
145
|
+
case 'GlobalId': case 'globalId': return data.globalId;
|
|
146
|
+
case 'Description': case 'description': return data.description;
|
|
147
|
+
case 'ObjectType': case 'objectType': return data.objectType;
|
|
148
|
+
case 'modelId': return data.ref.modelId;
|
|
149
|
+
case 'expressId': return String(data.ref.expressId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Property/Quantity path: "SetName.ValueName"
|
|
153
|
+
const dotIdx = col.indexOf('.');
|
|
154
|
+
if (dotIdx > 0) {
|
|
155
|
+
const setName = col.slice(0, dotIdx);
|
|
156
|
+
const valueName = col.slice(dotIdx + 1);
|
|
157
|
+
|
|
158
|
+
// Try property sets first
|
|
159
|
+
const psets = getProps();
|
|
160
|
+
const pset = psets.find(p => p.name === setName);
|
|
161
|
+
if (pset) {
|
|
162
|
+
const prop = pset.properties.find(p => p.name === valueName);
|
|
163
|
+
if (prop?.value != null) return String(prop.value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fall back to quantity sets
|
|
167
|
+
const qsets = getQties();
|
|
168
|
+
const qset = qsets.find(q => q.name === setName);
|
|
169
|
+
if (qset) {
|
|
170
|
+
const qty = qset.quantities.find(q => q.name === valueName);
|
|
171
|
+
if (qty?.value != null) return String(qty.value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
csv(rawRefs: unknown, rawOptions: unknown) {
|
|
182
|
+
if (!isEntityRefArray(rawRefs)) {
|
|
183
|
+
throw new Error('export.csv: first argument must be an array of entity references');
|
|
184
|
+
}
|
|
185
|
+
if (!isCsvOptions(rawOptions)) {
|
|
186
|
+
throw new Error('export.csv: second argument must be { columns: string[], separator?: string }');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const refs = normalizeRefs(rawRefs);
|
|
190
|
+
const options = rawOptions;
|
|
191
|
+
const sep = options.separator ?? ',';
|
|
192
|
+
const rows: string[][] = [];
|
|
193
|
+
|
|
194
|
+
// Header row
|
|
195
|
+
rows.push(options.columns);
|
|
196
|
+
|
|
197
|
+
// Data rows
|
|
198
|
+
for (const ref of refs) {
|
|
199
|
+
const data = getEntityData(ref);
|
|
200
|
+
if (!data) continue;
|
|
201
|
+
|
|
202
|
+
// Lazy-load properties/quantities only if a column needs them
|
|
203
|
+
let cachedProps: PropertySetData[] | null = null;
|
|
204
|
+
const getProps = (): PropertySetData[] => {
|
|
205
|
+
if (!cachedProps) cachedProps = getProperties(ref);
|
|
206
|
+
return cachedProps;
|
|
207
|
+
};
|
|
208
|
+
let cachedQties: QuantitySetData[] | null = null;
|
|
209
|
+
const getQties = (): QuantitySetData[] => {
|
|
210
|
+
if (!cachedQties) cachedQties = getQuantities(ref);
|
|
211
|
+
return cachedQties;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const row = options.columns.map(col => resolveColumnValue(data, col, getProps, getQties));
|
|
215
|
+
rows.push(row);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const csvString = rows.map(r => r.map(cell => escapeCsv(cell, sep)).join(sep)).join('\n');
|
|
219
|
+
|
|
220
|
+
// If filename specified, trigger browser download
|
|
221
|
+
if (options.filename) {
|
|
222
|
+
triggerDownload(csvString, options.filename, 'text/csv;charset=utf-8;');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return csvString;
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
json(rawRefs: unknown, columns: unknown) {
|
|
229
|
+
if (!isEntityRefArray(rawRefs)) {
|
|
230
|
+
throw new Error('export.json: first argument must be an array of entity references');
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(columns)) {
|
|
233
|
+
throw new Error('export.json: second argument must be a string[] of column names');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const refs = normalizeRefs(rawRefs);
|
|
237
|
+
const result: Record<string, unknown>[] = [];
|
|
238
|
+
|
|
239
|
+
for (const ref of refs) {
|
|
240
|
+
const data = getEntityData(ref);
|
|
241
|
+
if (!data) continue;
|
|
242
|
+
|
|
243
|
+
let cachedProps: PropertySetData[] | null = null;
|
|
244
|
+
const getProps = (): PropertySetData[] => {
|
|
245
|
+
if (!cachedProps) cachedProps = getProperties(ref);
|
|
246
|
+
return cachedProps;
|
|
247
|
+
};
|
|
248
|
+
let cachedQties: QuantitySetData[] | null = null;
|
|
249
|
+
const getQties = (): QuantitySetData[] => {
|
|
250
|
+
if (!cachedQties) cachedQties = getQuantities(ref);
|
|
251
|
+
return cachedQties;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const row: Record<string, unknown> = {};
|
|
255
|
+
for (const col of columns as string[]) {
|
|
256
|
+
const value = resolveColumnValue(data, col, getProps, getQties);
|
|
257
|
+
// Try to parse numeric values
|
|
258
|
+
const numVal = Number(value);
|
|
259
|
+
row[col] = value === '' ? null : !isNaN(numVal) && value.trim() !== '' ? numVal : value;
|
|
260
|
+
}
|
|
261
|
+
result.push(row);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
download(content: string, filename: string, mimeType?: string) {
|
|
268
|
+
triggerDownload(content, filename, mimeType ?? 'text/plain');
|
|
269
|
+
return undefined;
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Trigger a browser file download */
|
|
275
|
+
function triggerDownload(content: string, filename: string, mimeType: string): void {
|
|
276
|
+
const blob = new Blob([content], { type: mimeType });
|
|
277
|
+
const url = URL.createObjectURL(blob);
|
|
278
|
+
const a = document.createElement('a');
|
|
279
|
+
a.href = url;
|
|
280
|
+
a.download = filename;
|
|
281
|
+
a.click();
|
|
282
|
+
URL.revokeObjectURL(url);
|
|
283
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
import type { LensBackendMethods } from '@ifc-lite/sdk';
|
|
6
|
+
import type { StoreApi } from './types.js';
|
|
7
|
+
import { BUILTIN_LENSES } from '@ifc-lite/lens';
|
|
8
|
+
|
|
9
|
+
/** Type guard for lens config object */
|
|
10
|
+
function isLensConfig(v: unknown): v is Record<string, unknown> {
|
|
11
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createLensAdapter(store: StoreApi): LensBackendMethods {
|
|
15
|
+
return {
|
|
16
|
+
presets() {
|
|
17
|
+
return BUILTIN_LENSES;
|
|
18
|
+
},
|
|
19
|
+
create(config: unknown) {
|
|
20
|
+
if (!isLensConfig(config)) {
|
|
21
|
+
throw new Error('lens.create: argument must be a lens configuration object');
|
|
22
|
+
}
|
|
23
|
+
const id = crypto.randomUUID();
|
|
24
|
+
return { ...config, id };
|
|
25
|
+
},
|
|
26
|
+
activate(lensId: unknown) {
|
|
27
|
+
if (typeof lensId !== 'string') {
|
|
28
|
+
throw new Error('lens.activate: argument must be a lens ID string');
|
|
29
|
+
}
|
|
30
|
+
const state = store.getState();
|
|
31
|
+
state.setActiveLens?.(lensId);
|
|
32
|
+
return undefined;
|
|
33
|
+
},
|
|
34
|
+
deactivate() {
|
|
35
|
+
const state = store.getState();
|
|
36
|
+
state.setActiveLens?.(null);
|
|
37
|
+
return undefined;
|
|
38
|
+
},
|
|
39
|
+
getActive() {
|
|
40
|
+
const state = store.getState();
|
|
41
|
+
return state.activeLensId ?? null;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|