@ifc-lite/viewer 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
- package/dist/assets/browser-BXNIkE8a.js +694 -0
- package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
- package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
- package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
- package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
- package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
- package/dist/assets/esbuild-COv63sf-.js +1 -0
- package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
- package/dist/assets/ffi-DlhRHxHv.js +1 -0
- package/dist/assets/index-6Mr3byM-.js +216 -0
- package/dist/assets/index-CGbokkQ9.css +1 -0
- package/dist/assets/index-huvR-kGC.js +98305 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
- package/dist/index.html +12 -3
- package/index.html +10 -1
- package/package.json +30 -21
- package/src/App.tsx +6 -1
- package/src/components/ui/dialog.tsx +8 -6
- package/src/components/viewer/CodeEditor.tsx +309 -0
- package/src/components/viewer/CommandPalette.tsx +597 -0
- package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +145 -84
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +25 -5
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +114 -14
- package/src/hooks/useLens.ts +40 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/hooks/useSandbox.ts +113 -0
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -0
- package/src/lib/recent-files.ts +122 -0
- package/src/lib/scripts/persistence.ts +132 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
- package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
- package/src/lib/scripts/templates/envelope-check.ts +164 -0
- package/src/lib/scripts/templates/federation-compare.ts +189 -0
- package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
- package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
- package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
- package/src/lib/scripts/templates/reset-view.ts +6 -0
- package/src/lib/scripts/templates/space-validation.ts +189 -0
- package/src/lib/scripts/templates/tsconfig.json +13 -0
- package/src/lib/scripts/templates.ts +86 -0
- package/src/sdk/BimProvider.tsx +50 -0
- package/src/sdk/adapters/export-adapter.ts +283 -0
- package/src/sdk/adapters/lens-adapter.ts +44 -0
- package/src/sdk/adapters/model-adapter.ts +32 -0
- package/src/sdk/adapters/model-compat.ts +80 -0
- package/src/sdk/adapters/mutate-adapter.ts +45 -0
- package/src/sdk/adapters/query-adapter.ts +241 -0
- package/src/sdk/adapters/selection-adapter.ts +29 -0
- package/src/sdk/adapters/spatial-adapter.ts +37 -0
- package/src/sdk/adapters/types.ts +11 -0
- package/src/sdk/adapters/viewer-adapter.ts +103 -0
- package/src/sdk/adapters/visibility-adapter.ts +61 -0
- package/src/sdk/local-backend.ts +144 -0
- package/src/sdk/useBimHost.ts +69 -0
- package/src/store/constants.ts +10 -2
- package/src/store/index.ts +28 -2
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -42
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +2 -0
- package/src/store.ts +3 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/index-dgdgiQ9p.js +0 -75456
- package/dist/assets/index-yTqs8kgX.css +0 -1
|
@@ -0,0 +1,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')
|