export {} // module boundary (stripped by transpiler) // ── Space & Room Validation ───────────────────────────────────────────── // Stakeholder: Facility Manager / Architect // // Validates IfcSpace entities for operations handover readiness. Checks // that every space has a Name, LongName, area and volume quantities, // and required properties. Generates a room schedule with measurements // and flags incomplete spaces. This would take hours of clicking through // individual spaces in the property panel. // ───────────────────────────────────────────────────────────────────────── bim.viewer.resetColors() bim.viewer.resetVisibility() const spaces = bim.query.byType('IfcSpace') if (spaces.length === 0) { console.warn('No IfcSpace entities found in this model.') console.log('') console.log('This script validates room/space data. Models without') console.log('IfcSpace entities are missing spatial programming data.') console.log('') // Show what spatial types exist const spatialTypes = bim.query.byType('IfcBuildingStorey', 'IfcBuilding', 'IfcSite') if (spatialTypes.length > 0) { console.log('Spatial structure found:') for (const e of spatialTypes) { console.log(' ' + e.Type + ': ' + (e.Name || '')) } } throw new Error('no spaces') } // ── 1. Extract space data ─────────────────────────────────────────────── interface SpaceData { entity: BimEntity area: number | null volume: number | null perimeter: number | null height: number | null longName: string | null category: string | null occupancy: string | null issues: string[] } const spaceData: SpaceData[] = [] // Collect property/quantity paths for CSV export const spacePropPaths = new Set() const spaceQtyPaths = new Set() for (const space of spaces) { const data: SpaceData = { entity: space, area: null, volume: null, perimeter: null, height: null, longName: null, category: null, occupancy: null, issues: [] } // Extract quantities const qsets = bim.query.quantities(space) for (const qset of qsets) { for (const q of qset.quantities) { const lower = q.name.toLowerCase() if (lower.includes('area') && !lower.includes('wall') && data.area === null) data.area = q.value if (lower.includes('volume') && data.volume === null) data.volume = q.value if (lower.includes('perimeter') && data.perimeter === null) data.perimeter = q.value if (lower.includes('height') && data.height === null) data.height = q.value if (q.value !== null && q.value !== 0) spaceQtyPaths.add(qset.name + '.' + q.name) } } // Extract properties const psets = bim.query.properties(space) for (const pset of psets) { for (const p of pset.properties) { const lower = p.name.toLowerCase() if (lower === 'longname' && p.value) data.longName = String(p.value) if (lower === 'category' && p.value) data.category = String(p.value) if (lower === 'occupancytype' && p.value) data.occupancy = String(p.value) if (p.value !== null && p.value !== '') spacePropPaths.add(pset.name + '.' + p.name) } } // Also check entity attributes if (!data.longName && space.Description) data.longName = space.Description // Check for issues if (!space.Name || space.Name === '') data.issues.push('Missing Name') if (!data.longName) data.issues.push('Missing LongName/Description') if (data.area === null) data.issues.push('Missing Area') if (data.volume === null) data.issues.push('Missing Volume') spaceData.push(data) } // ── 2. Classify spaces ───────────────────────────────────────────────── const complete = spaceData.filter(s => s.issues.length === 0) const incomplete = spaceData.filter(s => s.issues.length > 0) // ── 3. Color-code ─────────────────────────────────────────────────────── const batches: Array<{ entities: BimEntity[]; color: string }> = [] if (complete.length > 0) batches.push({ entities: complete.map(s => s.entity), color: '#27ae60' }) const minor = incomplete.filter(s => s.issues.length <= 2) const major = incomplete.filter(s => s.issues.length > 2) if (minor.length > 0) batches.push({ entities: minor.map(s => s.entity), color: '#f39c12' }) if (major.length > 0) batches.push({ entities: major.map(s => s.entity), color: '#e74c3c' }) if (batches.length > 0) bim.viewer.colorizeAll(batches) // ── 4. Report ─────────────────────────────────────────────────────────── console.log('═══════════════════════════════════════') console.log(' SPACE & ROOM VALIDATION') console.log('═══════════════════════════════════════') console.log('') console.log('Spaces found: ' + spaces.length) console.log(' Complete: ' + complete.length + ' ● green') console.log(' Minor issues: ' + minor.length + ' ● orange (1-2 issues)') console.log(' Major issues: ' + major.length + ' ● red (3+ issues)') // ── 5. Room schedule ──────────────────────────────────────────────────── console.log('') console.log('── Room Schedule ──') console.log('Name | Area m² | Volume m³ | Height m | Status') console.log('------------------------+----------+-----------+----------+-------') // Sort by name const sorted = [...spaceData].sort((a, b) => { const nameA = a.entity.Name || 'zzz' const nameB = b.entity.Name || 'zzz' return nameA.localeCompare(nameB) }) let totalArea = 0 let totalVolume = 0 for (const s of sorted) { const name = ((s.entity.Name || '') + ' ').slice(0, 24) const area = s.area !== null ? (s.area.toFixed(1) + ' ').slice(0, 8) : '- ' const vol = s.volume !== null ? (s.volume.toFixed(1) + ' ').slice(0, 9) : '- ' const height = s.height !== null ? (s.height.toFixed(2) + ' ').slice(0, 8) : '- ' const status = s.issues.length === 0 ? 'OK' : s.issues.length + ' issues' console.log(name + '| ' + area + ' | ' + vol + ' | ' + height + ' | ' + status) if (s.area !== null) totalArea += s.area if (s.volume !== null) totalVolume += s.volume } console.log('------------------------+----------+-----------+----------+-------') console.log('TOTALS | ' + (totalArea.toFixed(1) + ' ').slice(0, 8) + ' | ' + (totalVolume.toFixed(1) + ' ').slice(0, 9) + ' |') // ── 6. Category breakdown ─────────────────────────────────────────────── const categories: Record = {} for (const s of spaceData) { const cat = s.category || s.entity.ObjectType || 'Uncategorized' if (!categories[cat]) categories[cat] = { count: 0, area: 0 } categories[cat].count++ if (s.area !== null) categories[cat].area += s.area } if (Object.keys(categories).length > 1) { console.log('') console.log('── By Category ──') for (const [cat, data] of Object.entries(categories).sort((a, b) => b[1].area - a[1].area)) { console.log(' ' + cat + ': ' + data.count + ' spaces, ' + data.area.toFixed(1) + ' m²') } } // ── 7. Issue details ──────────────────────────────────────────────────── if (incomplete.length > 0) { console.log('') console.warn('── Incomplete Spaces ──') const issueCount: Record = {} for (const s of incomplete) { for (const issue of s.issues) { issueCount[issue] = (issueCount[issue] || 0) + 1 } } for (const [issue, count] of Object.entries(issueCount).sort((a, b) => b[1] - a[1])) { console.warn(' ' + issue + ': ' + count + ' spaces') } } // ── 8. Export ──────────────────────────────────────────────────────────── const spPropCols = Array.from(spacePropPaths).sort().slice(0, 15) const spQtyCols = Array.from(spaceQtyPaths).sort().slice(0, 15) bim.export.csv(spaces, { columns: ['Name', 'Type', 'GlobalId', 'Description', 'ObjectType', ...spPropCols, ...spQtyCols], filename: 'room-schedule.csv' }) console.log('') console.log('Exported ' + spaces.length + ' spaces (' + (5 + spPropCols.length + spQtyCols.length) + ' columns) to room-schedule.csv')