@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/assets/{Arrow.dom-CwcRxist.js → Arrow.dom-Bw5JMdDs.js} +1 -1
  3. package/dist/assets/browser-DdRf3aWl.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/ifc-lite_bg-C1-gLAHo.wasm +0 -0
  13. package/dist/assets/index-1ff6P0kc.js +100011 -0
  14. package/dist/assets/index-Bz7vHRxl.js +216 -0
  15. package/dist/assets/index-mvbV6NHd.css +1 -0
  16. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  17. package/dist/assets/{native-bridge-5LbrYh3R.js → native-bridge-C5hD5vae.js} +1 -1
  18. package/dist/assets/{wasm-bridge-CgpLtj1h.js → wasm-bridge-CaNKXFGM.js} +1 -1
  19. package/dist/index.html +12 -3
  20. package/index.html +10 -1
  21. package/package.json +30 -21
  22. package/src/App.tsx +6 -1
  23. package/src/components/ui/dialog.tsx +8 -6
  24. package/src/components/viewer/CodeEditor.tsx +309 -0
  25. package/src/components/viewer/CommandPalette.tsx +597 -0
  26. package/src/components/viewer/MainToolbar.tsx +31 -3
  27. package/src/components/viewer/ScriptPanel.tsx +416 -0
  28. package/src/components/viewer/ViewerLayout.tsx +63 -11
  29. package/src/components/viewer/Viewport.tsx +58 -2
  30. package/src/components/viewer/hierarchy/treeDataBuilder.ts +3 -1
  31. package/src/components/viewer/useAnimationLoop.ts +4 -1
  32. package/src/components/viewer/useGeometryStreaming.ts +13 -1
  33. package/src/components/viewer/useRenderUpdates.ts +6 -1
  34. package/src/hooks/useKeyboardShortcuts.ts +1 -0
  35. package/src/hooks/useLens.ts +2 -1
  36. package/src/hooks/useSandbox.ts +113 -0
  37. package/src/hooks/useViewerSelectors.ts +22 -0
  38. package/src/index.css +6 -0
  39. package/src/lib/recent-files.ts +122 -0
  40. package/src/lib/scripts/persistence.ts +132 -0
  41. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  42. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  43. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  44. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  45. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  46. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  47. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  48. package/src/lib/scripts/templates/reset-view.ts +6 -0
  49. package/src/lib/scripts/templates/space-validation.ts +189 -0
  50. package/src/lib/scripts/templates/tsconfig.json +13 -0
  51. package/src/lib/scripts/templates.ts +86 -0
  52. package/src/sdk/BimProvider.tsx +50 -0
  53. package/src/sdk/adapters/export-adapter.ts +283 -0
  54. package/src/sdk/adapters/lens-adapter.ts +44 -0
  55. package/src/sdk/adapters/model-adapter.ts +32 -0
  56. package/src/sdk/adapters/model-compat.ts +80 -0
  57. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  58. package/src/sdk/adapters/query-adapter.ts +241 -0
  59. package/src/sdk/adapters/selection-adapter.ts +29 -0
  60. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  61. package/src/sdk/adapters/types.ts +11 -0
  62. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  63. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  64. package/src/sdk/local-backend.ts +144 -0
  65. package/src/sdk/useBimHost.ts +69 -0
  66. package/src/store/constants.ts +30 -2
  67. package/src/store/index.ts +24 -1
  68. package/src/store/slices/pinboardSlice.ts +37 -41
  69. package/src/store/slices/scriptSlice.ts +218 -0
  70. package/src/store/slices/uiSlice.ts +43 -0
  71. package/tsconfig.json +5 -2
  72. package/vite.config.ts +8 -0
  73. package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
  74. package/dist/assets/index-7WoQ-qVC.css +0 -1
  75. package/dist/assets/index-BSANf7-H.js +0 -78795
@@ -0,0 +1,189 @@
1
+ export {} // module boundary (stripped by transpiler)
2
+
3
+ // ── Multi-Model Federation Comparison ───────────────────────────────────
4
+ // Stakeholder: Project Manager / BIM Coordinator
5
+ //
6
+ // When multiple IFC models are loaded (architectural + structural + MEP),
7
+ // this script compares them side by side: entity counts, type coverage,
8
+ // naming consistency, and property completeness per model. The UI shows
9
+ // one tree at a time — this script cross-references everything and
10
+ // highlights discrepancies that indicate coordination issues.
11
+ // ─────────────────────────────────────────────────────────────────────────
12
+
13
+ const models = bim.model.list()
14
+
15
+ if (models.length === 0) {
16
+ console.error('No models loaded')
17
+ throw new Error('no models')
18
+ }
19
+
20
+ console.log('═══════════════════════════════════════')
21
+ console.log(' FEDERATION COMPARISON')
22
+ console.log('═══════════════════════════════════════')
23
+ console.log('')
24
+
25
+ // ── 1. Model overview ───────────────────────────────────────────────────
26
+ console.log('── Loaded Models (' + models.length + ') ──')
27
+ let totalEntities = 0
28
+ for (const m of models) {
29
+ const sizeMB = (m.fileSize / 1024 / 1024).toFixed(1)
30
+ console.log(' [' + m.id.slice(0, 8) + '] ' + m.name)
31
+ console.log(' Schema: ' + m.schemaVersion + ' | Entities: ' + m.entityCount + ' | Size: ' + sizeMB + ' MB')
32
+ totalEntities += m.entityCount
33
+ }
34
+ console.log('')
35
+ console.log('Total: ' + totalEntities + ' entities across ' + models.length + ' models')
36
+
37
+ // ── 2. Type distribution per model ──────────────────────────────────────
38
+ const all = bim.query.all()
39
+
40
+ // Build type→model distribution
41
+ const typeByModel: Record<string, Record<string, number>> = {}
42
+ const modelEntityMap: Record<string, BimEntity[]> = {}
43
+ for (const e of all) {
44
+ const mid = e.ref.modelId
45
+ if (!modelEntityMap[mid]) modelEntityMap[mid] = []
46
+ modelEntityMap[mid].push(e)
47
+
48
+ if (!typeByModel[e.Type]) typeByModel[e.Type] = {}
49
+ typeByModel[e.Type][mid] = (typeByModel[e.Type][mid] || 0) + 1
50
+ }
51
+
52
+ // Find types that exist in multiple models (potential overlaps/conflicts)
53
+ const sharedTypes: string[] = []
54
+ const exclusiveTypes: Record<string, string[]> = {} // modelId → types only in that model
55
+
56
+ for (const [type, distribution] of Object.entries(typeByModel)) {
57
+ const modelIds = Object.keys(distribution)
58
+ if (modelIds.length > 1) {
59
+ sharedTypes.push(type)
60
+ } else {
61
+ const mid = modelIds[0]
62
+ if (!exclusiveTypes[mid]) exclusiveTypes[mid] = []
63
+ exclusiveTypes[mid].push(type)
64
+ }
65
+ }
66
+
67
+ console.log('')
68
+ console.log('── Type Distribution ──')
69
+ console.log('')
70
+
71
+ // Types present in multiple models
72
+ if (sharedTypes.length > 0) {
73
+ console.log('Types shared across models (' + sharedTypes.length + '):')
74
+ const header = ' Type ' + models.map(m => ('| ' + m.name.slice(0, 10) + ' ').slice(0, 13)).join('')
75
+ console.log(header)
76
+ console.log(' ' + '-'.repeat(header.length - 2))
77
+
78
+ for (const type of sharedTypes.sort((a, b) => {
79
+ const totalA = Object.values(typeByModel[a]).reduce((s, v) => s + v, 0)
80
+ const totalB = Object.values(typeByModel[b]).reduce((s, v) => s + v, 0)
81
+ return totalB - totalA
82
+ }).slice(0, 20)) {
83
+ const typeStr = (type + ' ').slice(0, 30)
84
+ const counts = models.map(m => {
85
+ const c = typeByModel[type][m.id] || 0
86
+ return ('| ' + (c > 0 ? String(c) : '-') + ' ').slice(0, 13)
87
+ }).join('')
88
+ console.log(' ' + typeStr + counts)
89
+ }
90
+ }
91
+
92
+ // Types exclusive to one model
93
+ console.log('')
94
+ console.log('Types exclusive to one model:')
95
+ for (const m of models) {
96
+ const exclusive = exclusiveTypes[m.id] || []
97
+ if (exclusive.length > 0) {
98
+ console.log(' ' + m.name + ': ' + exclusive.slice(0, 8).join(', ') + (exclusive.length > 8 ? ' (+' + (exclusive.length - 8) + ' more)' : ''))
99
+ }
100
+ }
101
+
102
+ // ── 3. Naming consistency check ─────────────────────────────────────────
103
+ console.log('')
104
+ console.log('── Naming Consistency ──')
105
+ for (const m of models) {
106
+ const entities = modelEntityMap[m.id] || []
107
+ const named = entities.filter(e => e.Name && e.Name !== '')
108
+ const described = entities.filter(e => e.Description && e.Description !== '')
109
+ const typed = entities.filter(e => e.ObjectType && e.ObjectType !== '')
110
+ const namePct = entities.length > 0 ? (named.length / entities.length * 100).toFixed(1) : '0'
111
+ const descPct = entities.length > 0 ? (described.length / entities.length * 100).toFixed(1) : '0'
112
+ const typePct = entities.length > 0 ? (typed.length / entities.length * 100).toFixed(1) : '0'
113
+ console.log(' ' + m.name + ':')
114
+ console.log(' Named: ' + namePct + '% | Described: ' + descPct + '% | ObjectType: ' + typePct + '%')
115
+ }
116
+
117
+ // ── 4. Property coverage comparison ─────────────────────────────────────
118
+ console.log('')
119
+ console.log('── Property Coverage (sample of 100 per model) ──')
120
+ for (const m of models) {
121
+ const entities = modelEntityMap[m.id] || []
122
+ const sample = entities.slice(0, 100)
123
+ let withPsets = 0
124
+ let withQsets = 0
125
+ let totalPsets = 0
126
+ for (const e of sample) {
127
+ const psets = bim.query.properties(e)
128
+ const qsets = bim.query.quantities(e)
129
+ if (psets.length > 0) withPsets++
130
+ if (qsets.length > 0) withQsets++
131
+ totalPsets += psets.length
132
+ }
133
+ const psetPct = sample.length > 0 ? (withPsets / sample.length * 100).toFixed(0) : '0'
134
+ const qsetPct = sample.length > 0 ? (withQsets / sample.length * 100).toFixed(0) : '0'
135
+ const avgPsets = sample.length > 0 ? (totalPsets / sample.length).toFixed(1) : '0'
136
+ console.log(' ' + m.name + ':')
137
+ console.log(' With properties: ' + psetPct + '% | With quantities: ' + qsetPct + '% | Avg psets/entity: ' + avgPsets)
138
+ }
139
+
140
+ // ── 5. Potential coordination issues ────────────────────────────────────
141
+ if (models.length > 1) {
142
+ console.log('')
143
+ console.log('── Coordination Notes ──')
144
+
145
+ // Check for spatial types in non-architectural models
146
+ const spatialTypes = ['IfcBuilding', 'IfcBuildingStorey', 'IfcSite', 'IfcSpace']
147
+ for (const m of models) {
148
+ const hasSpatial: string[] = []
149
+ for (const st of spatialTypes) {
150
+ if (typeByModel[st] && typeByModel[st][m.id]) {
151
+ hasSpatial.push(st + '(' + typeByModel[st][m.id] + ')')
152
+ }
153
+ }
154
+ if (hasSpatial.length > 0) {
155
+ console.log(' ' + m.name + ' defines: ' + hasSpatial.join(', '))
156
+ }
157
+ }
158
+
159
+ // Check entity count ratios (large disparities may indicate issues)
160
+ const counts = models.map(m => modelEntityMap[m.id]?.length || 0)
161
+ const maxCount = Math.max(...counts)
162
+ const minCount = Math.min(...counts)
163
+ if (maxCount > 0 && minCount > 0 && maxCount / minCount > 10) {
164
+ console.warn('')
165
+ console.warn(' Large entity count disparity (' + maxCount + ' vs ' + minCount + ')')
166
+ console.warn(' This may indicate models at different levels of detail')
167
+ }
168
+ }
169
+
170
+ // ── 6. Color-code by model origin ───────────────────────────────────────
171
+ const modelColors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c']
172
+ const colorBatches: Array<{ entities: BimEntity[]; color: string }> = []
173
+ let idx = 0
174
+ for (const m of models) {
175
+ const entities = modelEntityMap[m.id] || []
176
+ if (entities.length > 0) {
177
+ colorBatches.push({ entities, color: modelColors[idx % modelColors.length] })
178
+ idx++
179
+ }
180
+ }
181
+ if (colorBatches.length > 0) bim.viewer.colorizeAll(colorBatches)
182
+
183
+ console.log('')
184
+ console.log('── Model Colors ──')
185
+ idx = 0
186
+ for (const m of models) {
187
+ console.log(' ' + m.name + ' ● ' + modelColors[idx % modelColors.length])
188
+ idx++
189
+ }
@@ -0,0 +1,161 @@
1
+ export {} // module boundary (stripped by transpiler)
2
+
3
+ // ── Fire Safety Compliance Check ────────────────────────────────────────
4
+ // Stakeholder: Architect / Fire Safety Engineer
5
+ //
6
+ // Cross-references fire rating properties across walls and doors — a task
7
+ // that requires clicking through hundreds of entities in the properties
8
+ // panel. The script checks Pset_WallCommon.FireRating and
9
+ // Pset_DoorCommon.FireRating, flags missing or non-compliant values,
10
+ // color-codes the model by compliance status, and generates a report.
11
+ // ─────────────────────────────────────────────────────────────────────────
12
+
13
+ bim.viewer.resetColors()
14
+
15
+ // ── 1. Gather fire-rated element types ──────────────────────────────────
16
+ const walls = bim.query.byType('IfcWall', 'IfcWallStandardCase')
17
+ const doors = bim.query.byType('IfcDoor', 'IfcDoorStandardCase')
18
+ const slabs = bim.query.byType('IfcSlab')
19
+ const total = walls.length + doors.length + slabs.length
20
+
21
+ if (total === 0) {
22
+ console.error('No walls, doors, or slabs found in model')
23
+ throw new Error('no elements')
24
+ }
25
+
26
+ // ── 2. Extract fire rating from properties ──────────────────────────────
27
+ interface FireResult {
28
+ entity: BimEntity
29
+ rating: string | null
30
+ isLoadBearing: boolean | null
31
+ isExternal: boolean | null
32
+ }
33
+
34
+ // Collect fire-related property paths for CSV export
35
+ const firePropPaths = new Set<string>()
36
+
37
+ function extractFireData(entity: BimEntity): FireResult {
38
+ const result: FireResult = { entity, rating: null, isLoadBearing: null, isExternal: null }
39
+ const psets = bim.query.properties(entity)
40
+ for (const pset of psets) {
41
+ for (const p of pset.properties) {
42
+ const lower = p.name.toLowerCase()
43
+ if (lower === 'firerating' && p.value !== null && p.value !== '') {
44
+ result.rating = String(p.value)
45
+ firePropPaths.add(pset.name + '.FireRating')
46
+ }
47
+ if (lower === 'loadbearing' && p.value !== null) {
48
+ result.isLoadBearing = p.value === true || p.value === 'TRUE' || p.value === '.T.'
49
+ firePropPaths.add(pset.name + '.LoadBearing')
50
+ }
51
+ if (lower === 'isexternal' && p.value !== null) {
52
+ result.isExternal = p.value === true || p.value === 'TRUE' || p.value === '.T.'
53
+ firePropPaths.add(pset.name + '.IsExternal')
54
+ }
55
+ }
56
+ }
57
+ return result
58
+ }
59
+
60
+ // ── 3. Analyze each element category ────────────────────────────────────
61
+ const wallResults = walls.map(extractFireData)
62
+ const doorResults = doors.map(extractFireData)
63
+ const slabResults = slabs.map(extractFireData)
64
+
65
+ // Classify
66
+ const rated: BimEntity[] = []
67
+ const unrated: BimEntity[] = []
68
+ const loadBearingUnrated: BimEntity[] = []
69
+ const externalUnrated: BimEntity[] = []
70
+
71
+ for (const r of [...wallResults, ...doorResults, ...slabResults]) {
72
+ if (r.rating) {
73
+ rated.push(r.entity)
74
+ } else {
75
+ unrated.push(r.entity)
76
+ if (r.isLoadBearing) loadBearingUnrated.push(r.entity)
77
+ if (r.isExternal) externalUnrated.push(r.entity)
78
+ }
79
+ }
80
+
81
+ // ── 4. Group by fire rating value ───────────────────────────────────────
82
+ const ratingGroups: Record<string, BimEntity[]> = {}
83
+ for (const r of [...wallResults, ...doorResults, ...slabResults]) {
84
+ if (r.rating) {
85
+ if (!ratingGroups[r.rating]) ratingGroups[r.rating] = []
86
+ ratingGroups[r.rating].push(r.entity)
87
+ }
88
+ }
89
+
90
+ // ── 5. Color-code by compliance ─────────────────────────────────────────
91
+ const batches: Array<{ entities: BimEntity[]; color: string }> = []
92
+ if (rated.length > 0) batches.push({ entities: rated, color: '#27ae60' }) // green = rated
93
+ if (loadBearingUnrated.length > 0) batches.push({ entities: loadBearingUnrated, color: '#e74c3c' }) // red = critical
94
+ if (externalUnrated.length > 0) batches.push({ entities: externalUnrated, color: '#e67e22' }) // orange = warning
95
+ // Remaining unrated that are not load-bearing or external
96
+ const otherUnrated = unrated.filter(e => {
97
+ const isLB = loadBearingUnrated.some(lb => lb.GlobalId === e.GlobalId)
98
+ const isExt = externalUnrated.some(ext => ext.GlobalId === e.GlobalId)
99
+ return !isLB && !isExt
100
+ })
101
+ if (otherUnrated.length > 0) batches.push({ entities: otherUnrated, color: '#f1c40f' }) // yellow = missing
102
+ if (batches.length > 0) bim.viewer.colorizeAll(batches)
103
+
104
+ // ── 6. Report ───────────────────────────────────────────────────────────
105
+ const compliancePct = (rated.length / total * 100).toFixed(1)
106
+ console.log('═══════════════════════════════════════')
107
+ console.log(' FIRE SAFETY COMPLIANCE CHECK')
108
+ console.log('═══════════════════════════════════════')
109
+ console.log('')
110
+ console.log('Scanned: ' + walls.length + ' walls, ' + doors.length + ' doors, ' + slabs.length + ' slabs')
111
+ console.log('Compliance: ' + compliancePct + '% (' + rated.length + '/' + total + ' rated)')
112
+ console.log('')
113
+ console.log(' ● Rated: ' + rated.length + ' green')
114
+ console.log(' ● Missing (load-bearing): ' + loadBearingUnrated.length + ' red — CRITICAL')
115
+ console.log(' ● Missing (external): ' + externalUnrated.length + ' orange — WARNING')
116
+ console.log(' ● Missing (other): ' + otherUnrated.length + ' yellow')
117
+
118
+ // ── 7. Rating distribution ──────────────────────────────────────────────
119
+ if (Object.keys(ratingGroups).length > 0) {
120
+ console.log('')
121
+ console.log('── Fire Rating Distribution ──')
122
+ const ratingsSorted = Object.entries(ratingGroups).sort((a, b) => b[1].length - a[1].length)
123
+ for (const [rating, entities] of ratingsSorted) {
124
+ console.log(' ' + rating + ': ' + entities.length + ' elements')
125
+ }
126
+ }
127
+
128
+ // ── 8. Per-type breakdown ───────────────────────────────────────────────
129
+ console.log('')
130
+ console.log('── Breakdown by Element Type ──')
131
+ const typeGroups: Record<string, { rated: number; total: number }> = {}
132
+ for (const r of [...wallResults, ...doorResults, ...slabResults]) {
133
+ const t = r.entity.Type
134
+ if (!typeGroups[t]) typeGroups[t] = { rated: 0, total: 0 }
135
+ typeGroups[t].total++
136
+ if (r.rating) typeGroups[t].rated++
137
+ }
138
+ for (const [type, g] of Object.entries(typeGroups).sort((a, b) => b[1].total - a[1].total)) {
139
+ const pct = (g.rated / g.total * 100).toFixed(0)
140
+ console.log(' ' + type + ': ' + g.rated + '/' + g.total + ' rated (' + pct + '%)')
141
+ }
142
+
143
+ // ── 9. Critical issues list ─────────────────────────────────────────────
144
+ if (loadBearingUnrated.length > 0) {
145
+ console.log('')
146
+ console.warn('── CRITICAL: Load-Bearing Without Fire Rating ──')
147
+ for (const e of loadBearingUnrated.slice(0, 15)) {
148
+ console.warn(' ' + (e.Name || '<unnamed>') + ' [' + e.Type + '] GlobalId=' + e.GlobalId)
149
+ }
150
+ if (loadBearingUnrated.length > 15) console.warn(' ... and ' + (loadBearingUnrated.length - 15) + ' more')
151
+ }
152
+
153
+ // ── 10. Export all scanned elements with fire properties ────────────────
154
+ const allFireElements = [...walls, ...doors, ...slabs]
155
+ const fireCols = Array.from(firePropPaths).sort()
156
+ bim.export.csv(allFireElements, {
157
+ columns: ['Name', 'Type', 'GlobalId', 'ObjectType', ...fireCols],
158
+ filename: 'fire-safety-report.csv'
159
+ })
160
+ console.log('')
161
+ console.log('Exported ' + allFireElements.length + ' elements (' + (4 + fireCols.length) + ' columns) to fire-safety-report.csv')
@@ -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
+ }