@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,164 @@
1
+ export {} // module boundary (stripped by transpiler)
2
+
3
+ // ── Building Envelope & Thermal Check ───────────────────────────────────
4
+ // Stakeholder: Architect / Energy Consultant
5
+ //
6
+ // Identifies all external-facing elements (walls, slabs, roofs, windows,
7
+ // doors, curtain walls) by reading Pset_*Common.IsExternal and checks
8
+ // whether they carry thermal transmittance values. Missing thermal data
9
+ // on external elements is a common issue in energy models. The script
10
+ // isolates the envelope, color-codes by thermal status, and exports
11
+ // the findings — a workflow that requires cross-referencing two property
12
+ // values across hundreds of elements.
13
+ // ─────────────────────────────────────────────────────────────────────────
14
+
15
+ bim.viewer.resetColors()
16
+ bim.viewer.resetVisibility()
17
+
18
+ // ── 1. Gather envelope candidates ───────────────────────────────────────
19
+ const envelopeTypes = [
20
+ 'IfcWall', 'IfcWallStandardCase', 'IfcCurtainWall',
21
+ 'IfcSlab', 'IfcRoof',
22
+ 'IfcDoor', 'IfcDoorStandardCase',
23
+ 'IfcWindow',
24
+ 'IfcPlate',
25
+ ]
26
+ const candidates = bim.query.byType(...envelopeTypes)
27
+
28
+ if (candidates.length === 0) {
29
+ console.error('No envelope element types found')
30
+ throw new Error('no elements')
31
+ }
32
+
33
+ // ── 2. Classify each element ────────────────────────────────────────────
34
+ interface EnvelopeResult {
35
+ entity: BimEntity
36
+ isExternal: boolean | null
37
+ thermalTransmittance: number | null
38
+ thermalSource: string
39
+ }
40
+
41
+ const results: EnvelopeResult[] = []
42
+ // Collect property paths for CSV export
43
+ const envelopePropPaths = new Set<string>()
44
+
45
+ for (const entity of candidates) {
46
+ const result: EnvelopeResult = { entity, isExternal: null, thermalTransmittance: null, thermalSource: '' }
47
+ const psets = bim.query.properties(entity)
48
+ for (const pset of psets) {
49
+ for (const p of pset.properties) {
50
+ const lower = p.name.toLowerCase()
51
+ if (lower === 'isexternal' && p.value !== null) {
52
+ result.isExternal = p.value === true || p.value === 'TRUE' || p.value === '.T.'
53
+ envelopePropPaths.add(pset.name + '.IsExternal')
54
+ }
55
+ if (lower === 'thermaltransmittance' && p.value !== null && p.value !== '') {
56
+ result.thermalTransmittance = typeof p.value === 'number' ? p.value : parseFloat(String(p.value))
57
+ result.thermalSource = pset.name
58
+ envelopePropPaths.add(pset.name + '.ThermalTransmittance')
59
+ }
60
+ // Also capture other thermal-related properties
61
+ if ((lower === 'firerating' || lower === 'loadbearing' || lower === 'acousticrating') && p.value !== null && p.value !== '') {
62
+ envelopePropPaths.add(pset.name + '.' + p.name)
63
+ }
64
+ }
65
+ }
66
+ results.push(result)
67
+ }
68
+
69
+ // ── 3. Separate external vs internal ────────────────────────────────────
70
+ const external = results.filter(r => r.isExternal === true)
71
+ const internal = results.filter(r => r.isExternal === false)
72
+ const unknown = results.filter(r => r.isExternal === null)
73
+
74
+ const extWithThermal = external.filter(r => r.thermalTransmittance !== null)
75
+ const extNoThermal = external.filter(r => r.thermalTransmittance === null)
76
+
77
+ // ── 4. Color-code ───────────────────────────────────────────────────────
78
+ const batches: Array<{ entities: BimEntity[]; color: string }> = []
79
+ if (extWithThermal.length > 0) batches.push({ entities: extWithThermal.map(r => r.entity), color: '#27ae60' }) // green: external + thermal
80
+ if (extNoThermal.length > 0) batches.push({ entities: extNoThermal.map(r => r.entity), color: '#e74c3c' }) // red: external, missing thermal
81
+ if (internal.length > 0) batches.push({ entities: internal.map(r => r.entity), color: '#95a5a6' }) // grey: internal
82
+ if (unknown.length > 0) batches.push({ entities: unknown.map(r => r.entity), color: '#f39c12' }) // orange: unknown
83
+ if (batches.length > 0) bim.viewer.colorizeAll(batches)
84
+
85
+ // Isolate envelope if we found external elements
86
+ if (external.length > 0) {
87
+ bim.viewer.isolate(external.map(r => r.entity))
88
+ }
89
+
90
+ // ── 5. Report ───────────────────────────────────────────────────────────
91
+ console.log('═══════════════════════════════════════')
92
+ console.log(' BUILDING ENVELOPE & THERMAL CHECK')
93
+ console.log('═══════════════════════════════════════')
94
+ console.log('')
95
+ console.log('Scanned ' + candidates.length + ' envelope-type elements')
96
+ console.log('')
97
+ console.log(' External: ' + external.length + ' (isolated in view)')
98
+ console.log(' Internal: ' + internal.length + ' ● grey')
99
+ console.log(' Undefined: ' + unknown.length + ' ● orange — IsExternal not set')
100
+ console.log('')
101
+ console.log(' External with thermal data: ' + extWithThermal.length + ' ● green')
102
+ console.log(' External without thermal data: ' + extNoThermal.length + ' ● red — NEEDS ATTENTION')
103
+
104
+ // ── 6. Thermal value distribution ───────────────────────────────────────
105
+ if (extWithThermal.length > 0) {
106
+ console.log('')
107
+ console.log('── Thermal Transmittance (U-value) Distribution ──')
108
+
109
+ // Group by type
110
+ const byType: Record<string, number[]> = {}
111
+ for (const r of extWithThermal) {
112
+ if (!byType[r.entity.Type]) byType[r.entity.Type] = []
113
+ if (r.thermalTransmittance !== null) byType[r.entity.Type].push(r.thermalTransmittance)
114
+ }
115
+
116
+ for (const [type, values] of Object.entries(byType).sort((a, b) => b[1].length - a[1].length)) {
117
+ const min = Math.min(...values)
118
+ const max = Math.max(...values)
119
+ const avg = values.reduce((s, v) => s + v, 0) / values.length
120
+ console.log(' ' + type + ' (' + values.length + ')')
121
+ console.log(' min=' + min.toFixed(3) + ' avg=' + avg.toFixed(3) + ' max=' + max.toFixed(3) + ' W/(m²·K)')
122
+ }
123
+ }
124
+
125
+ // ── 7. Elements missing IsExternal property ─────────────────────────────
126
+ if (unknown.length > 0) {
127
+ console.log('')
128
+ console.warn('── Missing IsExternal Property ──')
129
+ const byType: Record<string, number> = {}
130
+ for (const r of unknown) {
131
+ byType[r.entity.Type] = (byType[r.entity.Type] || 0) + 1
132
+ }
133
+ for (const [type, count] of Object.entries(byType).sort((a, b) => b[1] - a[1])) {
134
+ console.warn(' ' + type + ': ' + count + ' elements')
135
+ }
136
+ }
137
+
138
+ // ── 8. External elements missing thermal data ───────────────────────────
139
+ if (extNoThermal.length > 0) {
140
+ console.log('')
141
+ console.warn('── External Elements Without Thermal Data ──')
142
+ for (const r of extNoThermal.slice(0, 15)) {
143
+ console.warn(' ' + (r.entity.Name || '<unnamed>') + ' [' + r.entity.Type + ']')
144
+ }
145
+ if (extNoThermal.length > 15) console.warn(' ... and ' + (extNoThermal.length - 15) + ' more')
146
+
147
+ const envPropCols = Array.from(envelopePropPaths).sort()
148
+ bim.export.csv(extNoThermal.map(r => r.entity), {
149
+ columns: ['Name', 'Type', 'GlobalId', 'ObjectType', ...envPropCols],
150
+ filename: 'envelope-missing-thermal.csv'
151
+ })
152
+ console.log('')
153
+ console.log('Exported ' + extNoThermal.length + ' elements to envelope-missing-thermal.csv')
154
+ }
155
+
156
+ // Export full envelope dataset with all discovered properties
157
+ if (external.length > 0) {
158
+ const envPropCols = Array.from(envelopePropPaths).sort()
159
+ bim.export.csv(external.map(r => r.entity), {
160
+ columns: ['Name', 'Type', 'GlobalId', 'ObjectType', ...envPropCols],
161
+ filename: 'envelope-analysis.csv'
162
+ })
163
+ console.log('Exported ' + external.length + ' external elements (' + (4 + envPropCols.length) + ' columns) to envelope-analysis.csv')
164
+ }
@@ -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')