@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.
- package/CHANGELOG.md +77 -0
- package/dist/assets/{Arrow.dom-CwcRxist.js → Arrow.dom-Bw5JMdDs.js} +1 -1
- package/dist/assets/browser-DdRf3aWl.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/ifc-lite_bg-C1-gLAHo.wasm +0 -0
- package/dist/assets/index-1ff6P0kc.js +100011 -0
- package/dist/assets/index-Bz7vHRxl.js +216 -0
- package/dist/assets/index-mvbV6NHd.css +1 -0
- package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
- package/dist/assets/{native-bridge-5LbrYh3R.js → native-bridge-C5hD5vae.js} +1 -1
- package/dist/assets/{wasm-bridge-CgpLtj1h.js → wasm-bridge-CaNKXFGM.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/MainToolbar.tsx +31 -3
- package/src/components/viewer/ScriptPanel.tsx +416 -0
- package/src/components/viewer/ViewerLayout.tsx +63 -11
- package/src/components/viewer/Viewport.tsx +58 -2
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +3 -1
- package/src/components/viewer/useAnimationLoop.ts +4 -1
- package/src/components/viewer/useGeometryStreaming.ts +13 -1
- package/src/components/viewer/useRenderUpdates.ts +6 -1
- package/src/hooks/useKeyboardShortcuts.ts +1 -0
- package/src/hooks/useLens.ts +2 -1
- package/src/hooks/useSandbox.ts +113 -0
- package/src/hooks/useViewerSelectors.ts +22 -0
- package/src/index.css +6 -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 +30 -2
- package/src/store/index.ts +24 -1
- package/src/store/slices/pinboardSlice.ts +37 -41
- package/src/store/slices/scriptSlice.ts +218 -0
- package/src/store/slices/uiSlice.ts +43 -0
- package/tsconfig.json +5 -2
- package/vite.config.ts +8 -0
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/index-7WoQ-qVC.css +0 -1
- 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
|
+
}
|