@ifc-lite/viewer 1.7.0 → 1.9.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 +88 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
- package/dist/assets/browser-BXNIkE8a.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/index-6Mr3byM-.js +216 -0
- package/dist/assets/index-CGbokkQ9.css +1 -0
- package/dist/assets/index-huvR-kGC.js +98305 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.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/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +145 -84
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +25 -5
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +114 -14
- package/src/hooks/useLens.ts +40 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/hooks/useSandbox.ts +113 -0
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -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 +10 -2
- package/src/store/index.ts +28 -2
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -42
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +2 -0
- package/src/store.ts +3 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/index-dgdgiQ9p.js +0 -75456
- package/dist/assets/index-yTqs8kgX.css +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
export {} // module boundary (stripped by transpiler)
|
|
2
|
+
|
|
3
|
+
// ── MEP Equipment Schedule ──────────────────────────────────────────────
|
|
4
|
+
// Stakeholder: HVAC / MEP Engineer
|
|
5
|
+
//
|
|
6
|
+
// Discovers all MEP/distribution elements in the model, extracts their
|
|
7
|
+
// properties (manufacturer, model, capacity, pressure, flow rate), and
|
|
8
|
+
// generates a structured equipment register. The UI's property panel
|
|
9
|
+
// shows one element at a time — this script scans hundreds of elements
|
|
10
|
+
// and builds the schedule that would otherwise require a spreadsheet.
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
// ── 1. Query all MEP-related IFC types ──────────────────────────────────
|
|
14
|
+
const MEP_TYPES = [
|
|
15
|
+
// HVAC
|
|
16
|
+
'IfcAirTerminal', 'IfcAirTerminalBox', 'IfcFan', 'IfcCoil', 'IfcCompressor',
|
|
17
|
+
'IfcCondenser', 'IfcCooledBeam', 'IfcCoolingTower', 'IfcEvaporativeCooler',
|
|
18
|
+
'IfcEvaporator', 'IfcHeatExchanger', 'IfcHumidifier', 'IfcUnitaryEquipment',
|
|
19
|
+
'IfcBoiler', 'IfcChiller', 'IfcAirToAirHeatRecovery',
|
|
20
|
+
// Plumbing
|
|
21
|
+
'IfcSanitaryTerminal', 'IfcWasteTerminal', 'IfcFireSuppressionTerminal',
|
|
22
|
+
'IfcTank', 'IfcPump', 'IfcValve',
|
|
23
|
+
// Electrical
|
|
24
|
+
'IfcElectricDistributionBoard', 'IfcElectricGenerator', 'IfcElectricMotor',
|
|
25
|
+
'IfcTransformer', 'IfcSwitchingDevice', 'IfcOutlet', 'IfcLightFixture',
|
|
26
|
+
'IfcLamp', 'IfcJunctionBox',
|
|
27
|
+
// Generic distribution
|
|
28
|
+
'IfcDistributionElement', 'IfcDistributionControlElement',
|
|
29
|
+
'IfcDistributionFlowElement', 'IfcFlowTerminal', 'IfcFlowMovingDevice',
|
|
30
|
+
'IfcFlowController', 'IfcFlowStorageDevice', 'IfcFlowTreatmentDevice',
|
|
31
|
+
'IfcFlowFitting', 'IfcFlowSegment', 'IfcEnergyConversionDevice',
|
|
32
|
+
// Segments
|
|
33
|
+
'IfcDuctSegment', 'IfcPipeSegment', 'IfcCableCarrierSegment', 'IfcCableSegment',
|
|
34
|
+
'IfcDuctFitting', 'IfcPipeFitting', 'IfcCableCarrierFitting',
|
|
35
|
+
// Furnishing (often used for MEP equipment in some models)
|
|
36
|
+
'IfcFurnishingElement',
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const elements = bim.query.byType(...MEP_TYPES)
|
|
40
|
+
|
|
41
|
+
if (elements.length === 0) {
|
|
42
|
+
// Fall back: show what types ARE in the model so user can adapt
|
|
43
|
+
console.warn('No standard MEP element types found.')
|
|
44
|
+
console.log('')
|
|
45
|
+
console.log('Available types in this model:')
|
|
46
|
+
const all = bim.query.all()
|
|
47
|
+
const types: Record<string, number> = {}
|
|
48
|
+
for (const e of all) types[e.Type] = (types[e.Type] || 0) + 1
|
|
49
|
+
for (const [t, c] of Object.entries(types).sort((a, b) => b[1] - a[1])) {
|
|
50
|
+
console.log(' ' + t + ': ' + c)
|
|
51
|
+
}
|
|
52
|
+
console.log('')
|
|
53
|
+
console.log('Tip: edit MEP_TYPES at top of script to match your model\'s types.')
|
|
54
|
+
throw new Error('no MEP elements')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── 2. Group by IFC type ────────────────────────────────────────────────
|
|
58
|
+
const byType: Record<string, BimEntity[]> = {}
|
|
59
|
+
for (const e of elements) {
|
|
60
|
+
if (!byType[e.Type]) byType[e.Type] = []
|
|
61
|
+
byType[e.Type].push(e)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log('═══════════════════════════════════════')
|
|
65
|
+
console.log(' MEP EQUIPMENT SCHEDULE')
|
|
66
|
+
console.log('═══════════════════════════════════════')
|
|
67
|
+
console.log('')
|
|
68
|
+
console.log('Found ' + elements.length + ' MEP elements across ' + Object.keys(byType).length + ' types')
|
|
69
|
+
|
|
70
|
+
// ── 3. Extract properties for each type ─────────────────────────────────
|
|
71
|
+
// Properties of interest for MEP equipment
|
|
72
|
+
const MEP_PROPS = [
|
|
73
|
+
'manufacturer', 'model', 'reference', 'status',
|
|
74
|
+
'capacity', 'power', 'voltage', 'current', 'frequency',
|
|
75
|
+
'flowrate', 'pressure', 'temperature',
|
|
76
|
+
'nominalcapacity', 'nominalpower',
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
interface EquipmentEntry {
|
|
80
|
+
entity: BimEntity
|
|
81
|
+
props: Record<string, string | number | boolean | null>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const schedule: Record<string, EquipmentEntry[]> = {}
|
|
85
|
+
// Collect all unique property paths for CSV export columns
|
|
86
|
+
const propertyColumns = new Set<string>()
|
|
87
|
+
|
|
88
|
+
for (const [type, entities] of Object.entries(byType).sort((a, b) => b[1].length - a[1].length)) {
|
|
89
|
+
schedule[type] = []
|
|
90
|
+
|
|
91
|
+
// Cap property extraction at 200 per type
|
|
92
|
+
const sample = entities.length > 200 ? entities.slice(0, 200) : entities
|
|
93
|
+
|
|
94
|
+
for (const entity of sample) {
|
|
95
|
+
const entry: EquipmentEntry = { entity, props: {} }
|
|
96
|
+
const psets = bim.query.properties(entity)
|
|
97
|
+
for (const pset of psets) {
|
|
98
|
+
for (const p of pset.properties) {
|
|
99
|
+
const lower = p.name.toLowerCase()
|
|
100
|
+
if (MEP_PROPS.some(k => lower.includes(k)) && p.value !== null && p.value !== '') {
|
|
101
|
+
const path = pset.name + '.' + p.name
|
|
102
|
+
entry.props[path] = p.value
|
|
103
|
+
propertyColumns.add(path)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
schedule[type].push(entry)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 4. Print schedule ───────────────────────────────────────────────────
|
|
112
|
+
for (const [type, entries] of Object.entries(schedule).sort((a, b) => b[1].length - a[1].length)) {
|
|
113
|
+
console.log('')
|
|
114
|
+
console.log('── ' + type + ' (' + byType[type].length + ') ──')
|
|
115
|
+
|
|
116
|
+
// Collect all property keys found in this type
|
|
117
|
+
const allKeys = new Set<string>()
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
for (const key of Object.keys(entry.props)) allKeys.add(key)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (allKeys.size === 0) {
|
|
123
|
+
// Show name grouping even without properties
|
|
124
|
+
const names: Record<string, number> = {}
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const key = entry.entity.Name || entry.entity.ObjectType || '<unnamed>'
|
|
127
|
+
names[key] = (names[key] || 0) + 1
|
|
128
|
+
}
|
|
129
|
+
for (const [name, count] of Object.entries(names).sort((a, b) => b[1] - a[1])) {
|
|
130
|
+
console.log(' ' + name + (count > 1 ? ' (x' + count + ')' : ''))
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// Show first 10 entries with their properties
|
|
134
|
+
for (const entry of entries.slice(0, 10)) {
|
|
135
|
+
const label = entry.entity.Name || entry.entity.ObjectType || '<unnamed>'
|
|
136
|
+
console.log(' ' + label)
|
|
137
|
+
for (const [key, value] of Object.entries(entry.props)) {
|
|
138
|
+
console.log(' ' + key + ' = ' + value)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (entries.length > 10) console.log(' ... and ' + (entries.length - 10) + ' more')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── 5. Color-code by system type ────────────────────────────────────────
|
|
146
|
+
const palette = [
|
|
147
|
+
'#e74c3c', '#3498db', '#2ecc71', '#f39c12',
|
|
148
|
+
'#9b59b6', '#1abc9c', '#e67e22', '#34495e',
|
|
149
|
+
]
|
|
150
|
+
const batches: Array<{ entities: BimEntity[]; color: string }> = []
|
|
151
|
+
let colorIdx = 0
|
|
152
|
+
for (const [type, entities] of Object.entries(byType).sort((a, b) => b[1].length - a[1].length)) {
|
|
153
|
+
batches.push({ entities, color: palette[colorIdx % palette.length] })
|
|
154
|
+
colorIdx++
|
|
155
|
+
}
|
|
156
|
+
bim.viewer.colorizeAll(batches)
|
|
157
|
+
bim.viewer.isolate(elements)
|
|
158
|
+
|
|
159
|
+
// ── 6. Summary ──────────────────────────────────────────────────────────
|
|
160
|
+
console.log('')
|
|
161
|
+
console.log('── Type Summary ──')
|
|
162
|
+
for (const [type, entities] of Object.entries(byType).sort((a, b) => b[1].length - a[1].length)) {
|
|
163
|
+
const color = palette[(Object.keys(byType).sort((a, b) => byType[b].length - byType[a].length).indexOf(type)) % palette.length]
|
|
164
|
+
console.log(' ' + type + ': ' + entities.length + ' ● ' + color)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── 7. Export ────────────────────────────────────────────────────────────
|
|
168
|
+
const propCols = Array.from(propertyColumns).sort()
|
|
169
|
+
bim.export.csv(elements, {
|
|
170
|
+
columns: ['Name', 'Type', 'ObjectType', 'GlobalId', 'Description', ...propCols],
|
|
171
|
+
filename: 'mep-equipment-schedule.csv'
|
|
172
|
+
})
|
|
173
|
+
console.log('')
|
|
174
|
+
console.log('Exported ' + elements.length + ' MEP elements (' + (5 + propCols.length) + ' columns) to mep-equipment-schedule.csv')
|
|
175
|
+
console.log('Elements are isolated and color-coded by type in 3D view')
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
export {} // module boundary (stripped by transpiler)
|
|
2
|
+
|
|
3
|
+
// ── Quantity Takeoff ────────────────────────────────────────────────────
|
|
4
|
+
// Stakeholder: Cost Estimator / Project Manager
|
|
5
|
+
//
|
|
6
|
+
// Aggregates quantities (area, volume, length, width, height) across all
|
|
7
|
+
// structural and architectural element types. This combines what would
|
|
8
|
+
// require opening the properties panel for each element individually,
|
|
9
|
+
// manually recording numbers, and aggregating in a spreadsheet. The
|
|
10
|
+
// script does it in seconds and exports a ready-to-use CSV.
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const ELEMENT_TYPES = [
|
|
14
|
+
'IfcWall', 'IfcWallStandardCase',
|
|
15
|
+
'IfcSlab',
|
|
16
|
+
'IfcColumn',
|
|
17
|
+
'IfcBeam',
|
|
18
|
+
'IfcDoor', 'IfcDoorStandardCase',
|
|
19
|
+
'IfcWindow',
|
|
20
|
+
'IfcCovering',
|
|
21
|
+
'IfcCurtainWall',
|
|
22
|
+
'IfcRoof',
|
|
23
|
+
'IfcStair', 'IfcStairFlight',
|
|
24
|
+
'IfcRailing',
|
|
25
|
+
'IfcPlate',
|
|
26
|
+
'IfcMember',
|
|
27
|
+
'IfcFooting',
|
|
28
|
+
'IfcPile',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
// Quantities we care about (case-insensitive matching)
|
|
32
|
+
const QTY_KEYS = ['area', 'volume', 'length', 'width', 'height', 'netarea', 'netsidearea', 'netvolume', 'grossarea', 'grossvolume', 'perimeter']
|
|
33
|
+
|
|
34
|
+
interface TypeTakeoff {
|
|
35
|
+
type: string
|
|
36
|
+
count: number
|
|
37
|
+
quantities: Record<string, { sum: number; count: number; unit: string }>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log('═══════════════════════════════════════')
|
|
41
|
+
console.log(' QUANTITY TAKEOFF')
|
|
42
|
+
console.log('═══════════════════════════════════════')
|
|
43
|
+
console.log('')
|
|
44
|
+
|
|
45
|
+
const takeoffs: TypeTakeoff[] = []
|
|
46
|
+
let totalElements = 0
|
|
47
|
+
// Collect all unique Qto set+quantity paths for CSV export columns
|
|
48
|
+
const quantityColumns = new Set<string>()
|
|
49
|
+
|
|
50
|
+
for (const ifcType of ELEMENT_TYPES) {
|
|
51
|
+
const entities = bim.query.byType(ifcType)
|
|
52
|
+
if (entities.length === 0) continue
|
|
53
|
+
|
|
54
|
+
totalElements += entities.length
|
|
55
|
+
const takeoff: TypeTakeoff = { type: ifcType, count: entities.length, quantities: {} }
|
|
56
|
+
|
|
57
|
+
// Sample all entities for quantities (cap at 500 to avoid timeout)
|
|
58
|
+
const sample = entities.length > 500 ? entities.slice(0, 500) : entities
|
|
59
|
+
const isSampled = entities.length > 500
|
|
60
|
+
|
|
61
|
+
for (const entity of sample) {
|
|
62
|
+
const qsets = bim.query.quantities(entity)
|
|
63
|
+
for (const qset of qsets) {
|
|
64
|
+
for (const q of qset.quantities) {
|
|
65
|
+
if (q.value === null || q.value === 0) continue
|
|
66
|
+
const lower = q.name.toLowerCase()
|
|
67
|
+
// Only aggregate quantities we care about
|
|
68
|
+
const match = QTY_KEYS.find(k => lower.includes(k))
|
|
69
|
+
if (!match) continue
|
|
70
|
+
const key = q.name
|
|
71
|
+
if (!takeoff.quantities[key]) takeoff.quantities[key] = { sum: 0, count: 0, unit: '' }
|
|
72
|
+
takeoff.quantities[key].sum += q.value
|
|
73
|
+
takeoff.quantities[key].count++
|
|
74
|
+
// Track the full path for CSV export
|
|
75
|
+
quantityColumns.add(qset.name + '.' + q.name)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Scale up if sampled
|
|
81
|
+
if (isSampled) {
|
|
82
|
+
const factor = entities.length / sample.length
|
|
83
|
+
for (const q of Object.values(takeoff.quantities)) {
|
|
84
|
+
q.sum = q.sum * factor
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
takeoffs.push(takeoff)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (takeoffs.length === 0) {
|
|
92
|
+
console.error('No elements with quantities found')
|
|
93
|
+
throw new Error('no quantities')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Report ──────────────────────────────────────────────────────────────
|
|
97
|
+
console.log('Scanned ' + totalElements + ' elements across ' + takeoffs.length + ' types')
|
|
98
|
+
console.log('')
|
|
99
|
+
|
|
100
|
+
for (const t of takeoffs.sort((a, b) => b.count - a.count)) {
|
|
101
|
+
console.log('── ' + t.type + ' (' + t.count + ') ──')
|
|
102
|
+
const qEntries = Object.entries(t.quantities).sort((a, b) => b[1].sum - a[1].sum)
|
|
103
|
+
if (qEntries.length === 0) {
|
|
104
|
+
console.log(' (no quantities defined)')
|
|
105
|
+
} else {
|
|
106
|
+
for (const [name, q] of qEntries) {
|
|
107
|
+
const avg = q.sum / q.count
|
|
108
|
+
console.log(' ' + name + ': total=' + q.sum.toFixed(2) + ' avg=' + avg.toFixed(2) + ' (from ' + q.count + ' entities)')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log('')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Summary table ───────────────────────────────────────────────────────
|
|
115
|
+
console.log('── Summary ──')
|
|
116
|
+
console.log('Type | Count | Area | Volume')
|
|
117
|
+
console.log('---------------------------+-------+------------+-----------')
|
|
118
|
+
for (const t of takeoffs.sort((a, b) => b.count - a.count)) {
|
|
119
|
+
// Find area and volume totals
|
|
120
|
+
let area = 0
|
|
121
|
+
let volume = 0
|
|
122
|
+
for (const [name, q] of Object.entries(t.quantities)) {
|
|
123
|
+
const lower = name.toLowerCase()
|
|
124
|
+
if (lower.includes('area') && !lower.includes('net')) area += q.sum
|
|
125
|
+
if (lower.includes('volume') && !lower.includes('net')) volume += q.sum
|
|
126
|
+
}
|
|
127
|
+
const typeStr = (t.type + ' ').slice(0, 27)
|
|
128
|
+
const countStr = (' ' + t.count).slice(-5)
|
|
129
|
+
const areaStr = area > 0 ? (area.toFixed(1) + ' m²') : '-'
|
|
130
|
+
const volStr = volume > 0 ? (volume.toFixed(2) + ' m³') : '-'
|
|
131
|
+
console.log(typeStr + '| ' + countStr + ' | ' + (' ' + areaStr).slice(-10) + ' | ' + (' ' + volStr).slice(-9))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Export ───────────────────────────────────────────────────────────────
|
|
135
|
+
// Build a flat entity list with quantities for CSV export
|
|
136
|
+
const allElements = bim.query.byType(...ELEMENT_TYPES)
|
|
137
|
+
if (allElements.length > 0) {
|
|
138
|
+
const qtyCols = Array.from(quantityColumns).sort()
|
|
139
|
+
bim.export.csv(allElements, {
|
|
140
|
+
columns: ['Name', 'Type', 'ObjectType', 'GlobalId', ...qtyCols],
|
|
141
|
+
filename: 'quantity-takeoff.csv'
|
|
142
|
+
})
|
|
143
|
+
console.log('')
|
|
144
|
+
console.log('Exported ' + allElements.length + ' elements (' + (4 + qtyCols.length) + ' columns) to quantity-takeoff.csv')
|
|
145
|
+
}
|
|
@@ -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
|
+
}
|