@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
  3. package/dist/assets/browser-BXNIkE8a.js +694 -0
  4. package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
  5. package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
  6. package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
  7. package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
  8. package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
  9. package/dist/assets/esbuild-COv63sf-.js +1 -0
  10. package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
  11. package/dist/assets/ffi-DlhRHxHv.js +1 -0
  12. package/dist/assets/index-6Mr3byM-.js +216 -0
  13. package/dist/assets/index-CGbokkQ9.css +1 -0
  14. package/dist/assets/index-huvR-kGC.js +98305 -0
  15. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  16. package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
  17. package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
  18. package/dist/index.html +12 -3
  19. package/index.html +10 -1
  20. package/package.json +30 -21
  21. package/src/App.tsx +6 -1
  22. package/src/components/ui/dialog.tsx +8 -6
  23. package/src/components/viewer/CodeEditor.tsx +309 -0
  24. package/src/components/viewer/CommandPalette.tsx +597 -0
  25. package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
  26. package/src/components/viewer/EntityContextMenu.tsx +47 -20
  27. package/src/components/viewer/ExportDialog.tsx +166 -17
  28. package/src/components/viewer/HierarchyPanel.tsx +3 -1
  29. package/src/components/viewer/LensPanel.tsx +848 -85
  30. package/src/components/viewer/MainToolbar.tsx +145 -84
  31. package/src/components/viewer/ScriptPanel.tsx +416 -0
  32. package/src/components/viewer/Section2DPanel.tsx +269 -29
  33. package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
  34. package/src/components/viewer/ViewerLayout.tsx +63 -11
  35. package/src/components/viewer/Viewport.tsx +58 -23
  36. package/src/components/viewer/ViewportContainer.tsx +2 -0
  37. package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
  38. package/src/components/viewer/hierarchy/types.ts +1 -1
  39. package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
  40. package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
  41. package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
  42. package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
  43. package/src/components/viewer/tools/computePolygonArea.ts +72 -0
  44. package/src/components/viewer/useGeometryStreaming.ts +25 -5
  45. package/src/hooks/ids/idsExportService.ts +1 -1
  46. package/src/hooks/useAnnotation2D.ts +551 -0
  47. package/src/hooks/useDrawingExport.ts +83 -1
  48. package/src/hooks/useKeyboardShortcuts.ts +114 -14
  49. package/src/hooks/useLens.ts +40 -55
  50. package/src/hooks/useLensDiscovery.ts +46 -0
  51. package/src/hooks/useModelSelection.ts +5 -22
  52. package/src/hooks/useSandbox.ts +113 -0
  53. package/src/index.css +7 -1
  54. package/src/lib/lens/adapter.ts +127 -1
  55. package/src/lib/lists/columnToAutoColor.ts +33 -0
  56. package/src/lib/recent-files.ts +122 -0
  57. package/src/lib/scripts/persistence.ts +132 -0
  58. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  59. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  60. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  61. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  62. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  63. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  64. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  65. package/src/lib/scripts/templates/reset-view.ts +6 -0
  66. package/src/lib/scripts/templates/space-validation.ts +189 -0
  67. package/src/lib/scripts/templates/tsconfig.json +13 -0
  68. package/src/lib/scripts/templates.ts +86 -0
  69. package/src/sdk/BimProvider.tsx +50 -0
  70. package/src/sdk/adapters/export-adapter.ts +283 -0
  71. package/src/sdk/adapters/lens-adapter.ts +44 -0
  72. package/src/sdk/adapters/model-adapter.ts +32 -0
  73. package/src/sdk/adapters/model-compat.ts +80 -0
  74. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  75. package/src/sdk/adapters/query-adapter.ts +241 -0
  76. package/src/sdk/adapters/selection-adapter.ts +29 -0
  77. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  78. package/src/sdk/adapters/types.ts +11 -0
  79. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  80. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  81. package/src/sdk/local-backend.ts +144 -0
  82. package/src/sdk/useBimHost.ts +69 -0
  83. package/src/store/constants.ts +10 -2
  84. package/src/store/index.ts +28 -2
  85. package/src/store/resolveEntityRef.ts +44 -0
  86. package/src/store/slices/drawing2DSlice.ts +321 -0
  87. package/src/store/slices/lensSlice.ts +46 -4
  88. package/src/store/slices/pinboardSlice.ts +171 -42
  89. package/src/store/slices/scriptSlice.ts +218 -0
  90. package/src/store/slices/uiSlice.ts +2 -0
  91. package/src/store.ts +3 -0
  92. package/tsconfig.json +5 -2
  93. package/vite.config.ts +8 -0
  94. package/dist/assets/index-dgdgiQ9p.js +0 -75456
  95. 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,6 @@
1
+ export {} // module boundary (stripped by transpiler)
2
+
3
+ // Reset colors and visibility
4
+ bim.viewer.resetColors()
5
+ bim.viewer.resetVisibility()
6
+ console.log('View reset — all colors and visibility restored')
@@ -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
+ }