@ifc-lite/viewer 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/assets/{Arrow.dom-CwcRxist.js → Arrow.dom-Bw5JMdDs.js} +1 -1
  3. package/dist/assets/browser-DdRf3aWl.js +694 -0
  4. package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
  5. package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
  6. package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
  7. package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
  8. package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
  9. package/dist/assets/esbuild-COv63sf-.js +1 -0
  10. package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
  11. package/dist/assets/ffi-DlhRHxHv.js +1 -0
  12. package/dist/assets/ifc-lite_bg-C1-gLAHo.wasm +0 -0
  13. package/dist/assets/index-1ff6P0kc.js +100011 -0
  14. package/dist/assets/index-Bz7vHRxl.js +216 -0
  15. package/dist/assets/index-mvbV6NHd.css +1 -0
  16. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  17. package/dist/assets/{native-bridge-5LbrYh3R.js → native-bridge-C5hD5vae.js} +1 -1
  18. package/dist/assets/{wasm-bridge-CgpLtj1h.js → wasm-bridge-CaNKXFGM.js} +1 -1
  19. package/dist/index.html +12 -3
  20. package/index.html +10 -1
  21. package/package.json +30 -21
  22. package/src/App.tsx +6 -1
  23. package/src/components/ui/dialog.tsx +8 -6
  24. package/src/components/viewer/CodeEditor.tsx +309 -0
  25. package/src/components/viewer/CommandPalette.tsx +597 -0
  26. package/src/components/viewer/MainToolbar.tsx +31 -3
  27. package/src/components/viewer/ScriptPanel.tsx +416 -0
  28. package/src/components/viewer/ViewerLayout.tsx +63 -11
  29. package/src/components/viewer/Viewport.tsx +58 -2
  30. package/src/components/viewer/hierarchy/treeDataBuilder.ts +3 -1
  31. package/src/components/viewer/useAnimationLoop.ts +4 -1
  32. package/src/components/viewer/useGeometryStreaming.ts +13 -1
  33. package/src/components/viewer/useRenderUpdates.ts +6 -1
  34. package/src/hooks/useKeyboardShortcuts.ts +1 -0
  35. package/src/hooks/useLens.ts +2 -1
  36. package/src/hooks/useSandbox.ts +113 -0
  37. package/src/hooks/useViewerSelectors.ts +22 -0
  38. package/src/index.css +6 -0
  39. package/src/lib/recent-files.ts +122 -0
  40. package/src/lib/scripts/persistence.ts +132 -0
  41. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  42. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  43. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  44. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  45. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  46. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  47. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  48. package/src/lib/scripts/templates/reset-view.ts +6 -0
  49. package/src/lib/scripts/templates/space-validation.ts +189 -0
  50. package/src/lib/scripts/templates/tsconfig.json +13 -0
  51. package/src/lib/scripts/templates.ts +86 -0
  52. package/src/sdk/BimProvider.tsx +50 -0
  53. package/src/sdk/adapters/export-adapter.ts +283 -0
  54. package/src/sdk/adapters/lens-adapter.ts +44 -0
  55. package/src/sdk/adapters/model-adapter.ts +32 -0
  56. package/src/sdk/adapters/model-compat.ts +80 -0
  57. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  58. package/src/sdk/adapters/query-adapter.ts +241 -0
  59. package/src/sdk/adapters/selection-adapter.ts +29 -0
  60. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  61. package/src/sdk/adapters/types.ts +11 -0
  62. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  63. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  64. package/src/sdk/local-backend.ts +144 -0
  65. package/src/sdk/useBimHost.ts +69 -0
  66. package/src/store/constants.ts +30 -2
  67. package/src/store/index.ts +24 -1
  68. package/src/store/slices/pinboardSlice.ts +37 -41
  69. package/src/store/slices/scriptSlice.ts +218 -0
  70. package/src/store/slices/uiSlice.ts +43 -0
  71. package/tsconfig.json +5 -2
  72. package/vite.config.ts +8 -0
  73. package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
  74. package/dist/assets/index-7WoQ-qVC.css +0 -1
  75. package/dist/assets/index-BSANf7-H.js +0 -78795
@@ -0,0 +1,132 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Persistence for user scripts via localStorage.
7
+ *
8
+ * Uses a versioned schema so future additions (tags, description, etc.)
9
+ * can be migrated without data loss.
10
+ */
11
+
12
+ /** Current schema version */
13
+ const SCHEMA_VERSION = 1;
14
+
15
+ export interface SavedScript {
16
+ id: string;
17
+ name: string;
18
+ code: string;
19
+ createdAt: number;
20
+ updatedAt: number;
21
+ version: number;
22
+ }
23
+
24
+ /** Stored wrapper with schema version for migration */
25
+ interface StoredScripts {
26
+ schemaVersion: number;
27
+ scripts: SavedScript[];
28
+ }
29
+
30
+ const STORAGE_KEY = 'ifc-lite-scripts';
31
+
32
+ /** Maximum scripts allowed (prevents storage exhaustion) */
33
+ const MAX_SCRIPTS = 500;
34
+
35
+ /** Maximum code size per script in characters (~100KB) */
36
+ const MAX_SCRIPT_SIZE = 100_000;
37
+
38
+ export function loadSavedScripts(): SavedScript[] {
39
+ try {
40
+ const raw = localStorage.getItem(STORAGE_KEY);
41
+ if (!raw) return [];
42
+
43
+ const parsed: unknown = JSON.parse(raw);
44
+
45
+ // Handle legacy format (bare array without schema version)
46
+ if (Array.isArray(parsed)) {
47
+ return migrateFromLegacy(parsed);
48
+ }
49
+
50
+ // Versioned format — validate structure
51
+ if (
52
+ typeof parsed === 'object' &&
53
+ parsed !== null &&
54
+ 'schemaVersion' in parsed &&
55
+ 'scripts' in parsed &&
56
+ Array.isArray((parsed as StoredScripts).scripts)
57
+ ) {
58
+ return (parsed as StoredScripts).scripts;
59
+ }
60
+
61
+ return [];
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+
67
+ /** Migrate from the original unversioned format — discards corrupted entries */
68
+ function migrateFromLegacy(scripts: unknown[]): SavedScript[] {
69
+ const migrated: SavedScript[] = [];
70
+ for (const s of scripts) {
71
+ if (s === null || typeof s !== 'object') continue;
72
+ const script = s as Record<string, unknown>;
73
+
74
+ // Validate essential fields — discard garbage values from String()/Number() coercion
75
+ const id = typeof script.id === 'string' && script.id.length > 0 ? script.id : crypto.randomUUID();
76
+ const name = typeof script.name === 'string' && script.name.length > 0 ? script.name : 'Untitled';
77
+ const code = typeof script.code === 'string' ? script.code : '';
78
+ const createdAt = typeof script.createdAt === 'number' && isFinite(script.createdAt) ? script.createdAt : Date.now();
79
+ const updatedAt = typeof script.updatedAt === 'number' && isFinite(script.updatedAt) ? script.updatedAt : Date.now();
80
+
81
+ migrated.push({ id, name, code, createdAt, updatedAt, version: SCHEMA_VERSION });
82
+ }
83
+ // Save in new format
84
+ saveScripts(migrated);
85
+ return migrated;
86
+ }
87
+
88
+ export type SaveResult =
89
+ | { ok: true }
90
+ | { ok: false; reason: 'quota_exceeded' | 'serialization_error' | 'unknown'; message: string };
91
+
92
+ export function saveScripts(scripts: SavedScript[]): SaveResult {
93
+ const stored: StoredScripts = {
94
+ schemaVersion: SCHEMA_VERSION,
95
+ scripts,
96
+ };
97
+
98
+ try {
99
+ const json = JSON.stringify(stored);
100
+ localStorage.setItem(STORAGE_KEY, json);
101
+ return { ok: true };
102
+ } catch (err: unknown) {
103
+ if (err instanceof DOMException && err.name === 'QuotaExceededError') {
104
+ console.warn('[Scripts] localStorage quota exceeded. Consider deleting unused scripts.');
105
+ return { ok: false, reason: 'quota_exceeded', message: 'Storage quota exceeded. Delete unused scripts to free space.' };
106
+ }
107
+ if (err instanceof TypeError) {
108
+ console.warn('[Scripts] Failed to serialize scripts:', err.message);
109
+ return { ok: false, reason: 'serialization_error', message: err.message };
110
+ }
111
+ console.warn('[Scripts] Failed to save scripts to localStorage');
112
+ return { ok: false, reason: 'unknown', message: String(err) };
113
+ }
114
+ }
115
+
116
+ /** Validate a script name — returns sanitized name or null if invalid */
117
+ export function validateScriptName(name: string): string | null {
118
+ const trimmed = name.trim();
119
+ if (trimmed.length === 0) return null;
120
+ if (trimmed.length > 100) return trimmed.slice(0, 100);
121
+ return trimmed;
122
+ }
123
+
124
+ /** Check if adding another script is within limits */
125
+ export function canCreateScript(currentCount: number): boolean {
126
+ return currentCount < MAX_SCRIPTS;
127
+ }
128
+
129
+ /** Check if script code is within size limits */
130
+ export function isScriptWithinSizeLimit(code: string): boolean {
131
+ return code.length <= MAX_SCRIPT_SIZE;
132
+ }
@@ -0,0 +1,111 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * AUTO-GENERATED — do not edit by hand.
7
+ * Run: npx tsx scripts/generate-bim-globals.ts
8
+ *
9
+ * Type declarations for the sandbox `bim` global.
10
+ * Generated from NAMESPACE_SCHEMAS in bridge-schema.ts.
11
+ */
12
+
13
+ // ── Entity types ────────────────────────────────────────────────────────
14
+
15
+ interface BimEntity {
16
+ ref: { modelId: string; expressId: number };
17
+ name: string; Name: string;
18
+ type: string; Type: string;
19
+ globalId: string; GlobalId: string;
20
+ description: string; Description: string;
21
+ objectType: string; ObjectType: string;
22
+ }
23
+
24
+ interface BimPropertySet {
25
+ name: string;
26
+ properties: Array<{ name: string; value: string | number | boolean | null }>;
27
+ }
28
+
29
+ interface BimQuantitySet {
30
+ name: string;
31
+ quantities: Array<{ name: string; value: number | null }>;
32
+ }
33
+
34
+ interface BimModelInfo {
35
+ id: string;
36
+ name: string;
37
+ schemaVersion: string;
38
+ entityCount: number;
39
+ fileSize: number;
40
+ }
41
+
42
+ // ── Namespace declarations ──────────────────────────────────────────────
43
+
44
+ declare const bim: {
45
+ /** Model operations */
46
+ model: {
47
+ /** List loaded models */
48
+ list(): BimModelInfo[];
49
+ /** Get active model */
50
+ active(): BimModelInfo | null;
51
+ /** Get active model ID */
52
+ activeId(): string | null;
53
+ };
54
+ /** Query entities */
55
+ query: {
56
+ /** Get all entities */
57
+ all(): BimEntity[];
58
+ /** Filter by IFC type e.g. 'IfcWall' */
59
+ byType(...types: string[]): BimEntity[];
60
+ /** Get entity by model ID and express ID */
61
+ entity(modelId: string, expressId: number): BimEntity | null;
62
+ /** Get all IfcPropertySet data for an entity */
63
+ properties(entity: BimEntity): BimPropertySet[];
64
+ /** Get all IfcElementQuantity data for an entity */
65
+ quantities(entity: BimEntity): BimQuantitySet[];
66
+ };
67
+ /** Viewer control */
68
+ viewer: {
69
+ /** Colorize entities e.g. '#ff0000' */
70
+ colorize(entities: BimEntity[], color: string): void;
71
+ /** Batch colorize with [{entities, color}] */
72
+ colorizeAll(batches: Array<{ entities: BimEntity[]; color: string }>): void;
73
+ /** Hide entities */
74
+ hide(entities: BimEntity[]): void;
75
+ /** Show entities */
76
+ show(entities: BimEntity[]): void;
77
+ /** Isolate entities */
78
+ isolate(entities: BimEntity[]): void;
79
+ /** Select entities */
80
+ select(entities: BimEntity[]): void;
81
+ /** Fly camera to entities */
82
+ flyTo(entities: BimEntity[]): void;
83
+ /** Reset all colors */
84
+ resetColors(): void;
85
+ /** Reset all visibility */
86
+ resetVisibility(): void;
87
+ };
88
+ /** Property editing */
89
+ mutate: {
90
+ /** Set a property value */
91
+ setProperty(entity: unknown, psetName: string, propName: string, value: unknown): void;
92
+ /** Delete a property */
93
+ deleteProperty(entity: unknown, psetName: string, propName: string): void;
94
+ /** Undo last mutation */
95
+ undo(modelId: string): void;
96
+ /** Redo undone mutation */
97
+ redo(modelId: string): void;
98
+ };
99
+ /** Lens visualization */
100
+ lens: {
101
+ /** Get built-in lens presets */
102
+ presets(): unknown[];
103
+ };
104
+ /** Data export */
105
+ export: {
106
+ /** Export entities to CSV string */
107
+ csv(entities: BimEntity[], options: { columns: string[]; filename?: string; separator?: string }): string;
108
+ /** Export entities to JSON array */
109
+ json(entities: BimEntity[], columns: string[]): Record<string, unknown>[];
110
+ };
111
+ };
@@ -0,0 +1,149 @@
1
+ export {} // module boundary (stripped by transpiler)
2
+
3
+ // ── Data Quality Audit ──────────────────────────────────────────────────
4
+ // Stakeholder: BIM Manager / QA
5
+ //
6
+ // Scans every entity in the model for data completeness issues that would
7
+ // take hours to find by clicking through the UI. Produces a scorecard,
8
+ // color-codes the 3D view by quality level, and exports an issues CSV.
9
+ // ─────────────────────────────────────────────────────────────────────────
10
+
11
+ bim.viewer.resetColors()
12
+ const all = bim.query.all()
13
+ if (all.length === 0) { console.error('No entities loaded'); throw new Error('empty model') }
14
+
15
+ // ── 1. Check every entity for missing attributes ────────────────────────
16
+ interface Issue { entity: BimEntity; field: string }
17
+ const issues: Issue[] = []
18
+ const scores: Record<string, { entity: BimEntity; score: number }> = {}
19
+
20
+ // Track property coverage per type
21
+ const typePsetCoverage: Record<string, { total: number; withPsets: number }> = {}
22
+
23
+ for (const e of all) {
24
+ let score = 0
25
+ const maxScore = 5 // Name, Description, ObjectType, has properties, has quantities
26
+
27
+ if (e.Name && e.Name !== '') score++
28
+ else issues.push({ entity: e, field: 'Name' })
29
+
30
+ if (e.Description && e.Description !== '') score++
31
+ else issues.push({ entity: e, field: 'Description' })
32
+
33
+ if (e.ObjectType && e.ObjectType !== '') score++
34
+ else issues.push({ entity: e, field: 'ObjectType' })
35
+
36
+ const psets = bim.query.properties(e)
37
+ if (psets.length > 0) score++
38
+ else issues.push({ entity: e, field: 'PropertySets' })
39
+
40
+ const qsets = bim.query.quantities(e)
41
+ if (qsets.length > 0) score++
42
+ else issues.push({ entity: e, field: 'Quantities' })
43
+
44
+ scores[e.GlobalId] = { entity: e, score }
45
+
46
+ // Track pset coverage per type
47
+ if (!typePsetCoverage[e.Type]) typePsetCoverage[e.Type] = { total: 0, withPsets: 0 }
48
+ typePsetCoverage[e.Type].total++
49
+ if (psets.length > 0) typePsetCoverage[e.Type].withPsets++
50
+ }
51
+
52
+ // ── 2. Classify entities by quality tier ────────────────────────────────
53
+ const tiers = { complete: [] as BimEntity[], good: [] as BimEntity[], partial: [] as BimEntity[], poor: [] as BimEntity[] }
54
+ for (const { entity, score } of Object.values(scores)) {
55
+ if (score === 5) tiers.complete.push(entity)
56
+ else if (score >= 4) tiers.good.push(entity)
57
+ else if (score >= 2) tiers.partial.push(entity)
58
+ else tiers.poor.push(entity)
59
+ }
60
+
61
+ // ── 3. Color-code by quality ────────────────────────────────────────────
62
+ const batches: Array<{ entities: BimEntity[]; color: string }> = []
63
+ if (tiers.complete.length > 0) batches.push({ entities: tiers.complete, color: '#27ae60' }) // green
64
+ if (tiers.good.length > 0) batches.push({ entities: tiers.good, color: '#f1c40f' }) // yellow
65
+ if (tiers.partial.length > 0) batches.push({ entities: tiers.partial, color: '#e67e22' }) // orange
66
+ if (tiers.poor.length > 0) batches.push({ entities: tiers.poor, color: '#e74c3c' }) // red
67
+ if (batches.length > 0) bim.viewer.colorizeAll(batches)
68
+
69
+ // ── 4. Report ───────────────────────────────────────────────────────────
70
+ const overallScore = ((tiers.complete.length + tiers.good.length * 0.8 + tiers.partial.length * 0.4) / all.length * 100)
71
+ console.log('═══════════════════════════════════════')
72
+ console.log(' DATA QUALITY AUDIT')
73
+ console.log('═══════════════════════════════════════')
74
+ console.log('')
75
+ console.log('Overall score: ' + overallScore.toFixed(1) + '% (' + all.length + ' entities)')
76
+ console.log('')
77
+ console.log(' Complete (5/5): ' + tiers.complete.length + ' ● green')
78
+ console.log(' Good (4/5): ' + tiers.good.length + ' ● yellow')
79
+ console.log(' Partial (2-3): ' + tiers.partial.length + ' ● orange')
80
+ console.log(' Poor (0-1): ' + tiers.poor.length + ' ● red')
81
+
82
+ // ── 5. Issue breakdown by field ─────────────────────────────────────────
83
+ const issuesByField: Record<string, number> = {}
84
+ for (const issue of issues) {
85
+ issuesByField[issue.field] = (issuesByField[issue.field] || 0) + 1
86
+ }
87
+ console.log('')
88
+ console.log('── Missing Data Breakdown ──')
89
+ for (const [field, count] of Object.entries(issuesByField).sort((a, b) => b[1] - a[1])) {
90
+ const pct = (count / all.length * 100).toFixed(1)
91
+ console.log(' ' + field + ': ' + count + ' entities (' + pct + '% missing)')
92
+ }
93
+
94
+ // ── 6. Property coverage per type ───────────────────────────────────────
95
+ console.log('')
96
+ console.log('── Property Coverage by Type ──')
97
+ const coverageSorted = Object.entries(typePsetCoverage)
98
+ .sort((a, b) => b[1].total - a[1].total)
99
+ .slice(0, 15)
100
+ for (const [type, cov] of coverageSorted) {
101
+ const pct = (cov.withPsets / cov.total * 100).toFixed(0)
102
+ const bar = '█'.repeat(Math.round(cov.withPsets / cov.total * 20))
103
+ console.log(' ' + type + ': ' + bar + ' ' + pct + '% (' + cov.withPsets + '/' + cov.total + ')')
104
+ }
105
+
106
+ // ── 7. Worst offenders (first 10 entities with score 0-1) ──────────────
107
+ if (tiers.poor.length > 0) {
108
+ console.log('')
109
+ console.log('── Worst Offenders (score 0-1) ──')
110
+ for (const e of tiers.poor.slice(0, 10)) {
111
+ console.log(' ' + (e.Name || '<no name>') + ' [' + e.Type + '] GlobalId=' + e.GlobalId)
112
+ }
113
+ if (tiers.poor.length > 10) {
114
+ console.log(' ... and ' + (tiers.poor.length - 10) + ' more')
115
+ }
116
+ }
117
+
118
+ // ── 8. Export all entities with discovered property/quantity data ────────
119
+ // Discover common property columns from a sample
120
+ const sampleForDiscovery = all.slice(0, 200)
121
+ const discoveredPropPaths = new Set<string>()
122
+ const discoveredQtyPaths = new Set<string>()
123
+ for (const e of sampleForDiscovery) {
124
+ const psets = bim.query.properties(e)
125
+ for (const pset of psets) {
126
+ for (const p of pset.properties) {
127
+ if (p.value !== null && p.value !== '') {
128
+ discoveredPropPaths.add(pset.name + '.' + p.name)
129
+ }
130
+ }
131
+ }
132
+ const qsets = bim.query.quantities(e)
133
+ for (const qset of qsets) {
134
+ for (const q of qset.quantities) {
135
+ if (q.value !== null && q.value !== 0) {
136
+ discoveredQtyPaths.add(qset.name + '.' + q.name)
137
+ }
138
+ }
139
+ }
140
+ }
141
+ // Cap columns to keep CSV manageable
142
+ const auditPropCols = Array.from(discoveredPropPaths).sort().slice(0, 20)
143
+ const auditQtyCols = Array.from(discoveredQtyPaths).sort().slice(0, 10)
144
+ bim.export.csv(all, {
145
+ columns: ['Name', 'Type', 'GlobalId', 'Description', 'ObjectType', ...auditPropCols, ...auditQtyCols],
146
+ filename: 'data-quality-audit.csv'
147
+ })
148
+ console.log('')
149
+ console.log('Exported ' + all.length + ' entities (' + (5 + auditPropCols.length + auditQtyCols.length) + ' columns) to data-quality-audit.csv')
@@ -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
+ }