@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,6 @@
1
+ export {} // module boundary (stripped by transpiler)
2
+
3
+ // Reset colors and visibility
4
+ bim.viewer.resetColors()
5
+ bim.viewer.resetVisibility()
6
+ console.log('View reset — all colors and visibility restored')
@@ -0,0 +1,189 @@
1
+ export {} // module boundary (stripped by transpiler)
2
+
3
+ // ── Space & Room Validation ─────────────────────────────────────────────
4
+ // Stakeholder: Facility Manager / Architect
5
+ //
6
+ // Validates IfcSpace entities for operations handover readiness. Checks
7
+ // that every space has a Name, LongName, area and volume quantities,
8
+ // and required properties. Generates a room schedule with measurements
9
+ // and flags incomplete spaces. This would take hours of clicking through
10
+ // individual spaces in the property panel.
11
+ // ─────────────────────────────────────────────────────────────────────────
12
+
13
+ bim.viewer.resetColors()
14
+ bim.viewer.resetVisibility()
15
+
16
+ const spaces = bim.query.byType('IfcSpace')
17
+
18
+ if (spaces.length === 0) {
19
+ console.warn('No IfcSpace entities found in this model.')
20
+ console.log('')
21
+ console.log('This script validates room/space data. Models without')
22
+ console.log('IfcSpace entities are missing spatial programming data.')
23
+ console.log('')
24
+ // Show what spatial types exist
25
+ const spatialTypes = bim.query.byType('IfcBuildingStorey', 'IfcBuilding', 'IfcSite')
26
+ if (spatialTypes.length > 0) {
27
+ console.log('Spatial structure found:')
28
+ for (const e of spatialTypes) {
29
+ console.log(' ' + e.Type + ': ' + (e.Name || '<unnamed>'))
30
+ }
31
+ }
32
+ throw new Error('no spaces')
33
+ }
34
+
35
+ // ── 1. Extract space data ───────────────────────────────────────────────
36
+ interface SpaceData {
37
+ entity: BimEntity
38
+ area: number | null
39
+ volume: number | null
40
+ perimeter: number | null
41
+ height: number | null
42
+ longName: string | null
43
+ category: string | null
44
+ occupancy: string | null
45
+ issues: string[]
46
+ }
47
+
48
+ const spaceData: SpaceData[] = []
49
+ // Collect property/quantity paths for CSV export
50
+ const spacePropPaths = new Set<string>()
51
+ const spaceQtyPaths = new Set<string>()
52
+
53
+ for (const space of spaces) {
54
+ const data: SpaceData = {
55
+ entity: space, area: null, volume: null, perimeter: null, height: null,
56
+ longName: null, category: null, occupancy: null, issues: []
57
+ }
58
+
59
+ // Extract quantities
60
+ const qsets = bim.query.quantities(space)
61
+ for (const qset of qsets) {
62
+ for (const q of qset.quantities) {
63
+ const lower = q.name.toLowerCase()
64
+ if (lower.includes('area') && !lower.includes('wall') && data.area === null) data.area = q.value
65
+ if (lower.includes('volume') && data.volume === null) data.volume = q.value
66
+ if (lower.includes('perimeter') && data.perimeter === null) data.perimeter = q.value
67
+ if (lower.includes('height') && data.height === null) data.height = q.value
68
+ if (q.value !== null && q.value !== 0) spaceQtyPaths.add(qset.name + '.' + q.name)
69
+ }
70
+ }
71
+
72
+ // Extract properties
73
+ const psets = bim.query.properties(space)
74
+ for (const pset of psets) {
75
+ for (const p of pset.properties) {
76
+ const lower = p.name.toLowerCase()
77
+ if (lower === 'longname' && p.value) data.longName = String(p.value)
78
+ if (lower === 'category' && p.value) data.category = String(p.value)
79
+ if (lower === 'occupancytype' && p.value) data.occupancy = String(p.value)
80
+ if (p.value !== null && p.value !== '') spacePropPaths.add(pset.name + '.' + p.name)
81
+ }
82
+ }
83
+
84
+ // Also check entity attributes
85
+ if (!data.longName && space.Description) data.longName = space.Description
86
+
87
+ // Check for issues
88
+ if (!space.Name || space.Name === '') data.issues.push('Missing Name')
89
+ if (!data.longName) data.issues.push('Missing LongName/Description')
90
+ if (data.area === null) data.issues.push('Missing Area')
91
+ if (data.volume === null) data.issues.push('Missing Volume')
92
+
93
+ spaceData.push(data)
94
+ }
95
+
96
+ // ── 2. Classify spaces ─────────────────────────────────────────────────
97
+ const complete = spaceData.filter(s => s.issues.length === 0)
98
+ const incomplete = spaceData.filter(s => s.issues.length > 0)
99
+
100
+ // ── 3. Color-code ───────────────────────────────────────────────────────
101
+ const batches: Array<{ entities: BimEntity[]; color: string }> = []
102
+ if (complete.length > 0) batches.push({ entities: complete.map(s => s.entity), color: '#27ae60' })
103
+ const minor = incomplete.filter(s => s.issues.length <= 2)
104
+ const major = incomplete.filter(s => s.issues.length > 2)
105
+ if (minor.length > 0) batches.push({ entities: minor.map(s => s.entity), color: '#f39c12' })
106
+ if (major.length > 0) batches.push({ entities: major.map(s => s.entity), color: '#e74c3c' })
107
+ if (batches.length > 0) bim.viewer.colorizeAll(batches)
108
+
109
+ // ── 4. Report ───────────────────────────────────────────────────────────
110
+ console.log('═══════════════════════════════════════')
111
+ console.log(' SPACE & ROOM VALIDATION')
112
+ console.log('═══════════════════════════════════════')
113
+ console.log('')
114
+ console.log('Spaces found: ' + spaces.length)
115
+ console.log(' Complete: ' + complete.length + ' ● green')
116
+ console.log(' Minor issues: ' + minor.length + ' ● orange (1-2 issues)')
117
+ console.log(' Major issues: ' + major.length + ' ● red (3+ issues)')
118
+
119
+ // ── 5. Room schedule ────────────────────────────────────────────────────
120
+ console.log('')
121
+ console.log('── Room Schedule ──')
122
+ console.log('Name | Area m² | Volume m³ | Height m | Status')
123
+ console.log('------------------------+----------+-----------+----------+-------')
124
+
125
+ // Sort by name
126
+ const sorted = [...spaceData].sort((a, b) => {
127
+ const nameA = a.entity.Name || 'zzz'
128
+ const nameB = b.entity.Name || 'zzz'
129
+ return nameA.localeCompare(nameB)
130
+ })
131
+
132
+ let totalArea = 0
133
+ let totalVolume = 0
134
+
135
+ for (const s of sorted) {
136
+ const name = ((s.entity.Name || '<unnamed>') + ' ').slice(0, 24)
137
+ const area = s.area !== null ? (s.area.toFixed(1) + ' ').slice(0, 8) : '- '
138
+ const vol = s.volume !== null ? (s.volume.toFixed(1) + ' ').slice(0, 9) : '- '
139
+ const height = s.height !== null ? (s.height.toFixed(2) + ' ').slice(0, 8) : '- '
140
+ const status = s.issues.length === 0 ? 'OK' : s.issues.length + ' issues'
141
+ console.log(name + '| ' + area + ' | ' + vol + ' | ' + height + ' | ' + status)
142
+ if (s.area !== null) totalArea += s.area
143
+ if (s.volume !== null) totalVolume += s.volume
144
+ }
145
+
146
+ console.log('------------------------+----------+-----------+----------+-------')
147
+ console.log('TOTALS | ' + (totalArea.toFixed(1) + ' ').slice(0, 8) + ' | ' + (totalVolume.toFixed(1) + ' ').slice(0, 9) + ' |')
148
+
149
+ // ── 6. Category breakdown ───────────────────────────────────────────────
150
+ const categories: Record<string, { count: number; area: number }> = {}
151
+ for (const s of spaceData) {
152
+ const cat = s.category || s.entity.ObjectType || 'Uncategorized'
153
+ if (!categories[cat]) categories[cat] = { count: 0, area: 0 }
154
+ categories[cat].count++
155
+ if (s.area !== null) categories[cat].area += s.area
156
+ }
157
+
158
+ if (Object.keys(categories).length > 1) {
159
+ console.log('')
160
+ console.log('── By Category ──')
161
+ for (const [cat, data] of Object.entries(categories).sort((a, b) => b[1].area - a[1].area)) {
162
+ console.log(' ' + cat + ': ' + data.count + ' spaces, ' + data.area.toFixed(1) + ' m²')
163
+ }
164
+ }
165
+
166
+ // ── 7. Issue details ────────────────────────────────────────────────────
167
+ if (incomplete.length > 0) {
168
+ console.log('')
169
+ console.warn('── Incomplete Spaces ──')
170
+ const issueCount: Record<string, number> = {}
171
+ for (const s of incomplete) {
172
+ for (const issue of s.issues) {
173
+ issueCount[issue] = (issueCount[issue] || 0) + 1
174
+ }
175
+ }
176
+ for (const [issue, count] of Object.entries(issueCount).sort((a, b) => b[1] - a[1])) {
177
+ console.warn(' ' + issue + ': ' + count + ' spaces')
178
+ }
179
+ }
180
+
181
+ // ── 8. Export ────────────────────────────────────────────────────────────
182
+ const spPropCols = Array.from(spacePropPaths).sort().slice(0, 15)
183
+ const spQtyCols = Array.from(spaceQtyPaths).sort().slice(0, 15)
184
+ bim.export.csv(spaces, {
185
+ columns: ['Name', 'Type', 'GlobalId', 'Description', 'ObjectType', ...spPropCols, ...spQtyCols],
186
+ filename: 'room-schedule.csv'
187
+ })
188
+ console.log('')
189
+ console.log('Exported ' + spaces.length + ' spaces (' + (5 + spPropCols.length + spQtyCols.length) + ' columns) to room-schedule.csv')
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["bim-globals.d.ts", "*.ts"],
12
+ "exclude": ["tsconfig.json"]
13
+ }
@@ -0,0 +1,86 @@
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
+ * Built-in script templates for the script editor.
7
+ *
8
+ * Templates are real .ts files in ./templates/ that are type-checked
9
+ * against the bim-globals.d.ts declaration. They are loaded as raw
10
+ * strings via Vite's ?raw import and served to the sandbox transpiler.
11
+ *
12
+ * Each template targets a specific stakeholder and combines multiple
13
+ * API calls into automated workflows that go beyond what the UI can
14
+ * do through manual clicking.
15
+ */
16
+
17
+ // Raw source imports — Vite returns the file content as a string
18
+ import dataQualityAudit from './templates/data-quality-audit.ts?raw';
19
+ import fireSafetyCheck from './templates/fire-safety-check.ts?raw';
20
+ import quantityTakeoff from './templates/quantity-takeoff.ts?raw';
21
+ import envelopeCheck from './templates/envelope-check.ts?raw';
22
+ import mepEquipmentSchedule from './templates/mep-equipment-schedule.ts?raw';
23
+ import spaceValidation from './templates/space-validation.ts?raw';
24
+ import federationCompare from './templates/federation-compare.ts?raw';
25
+ import resetView from './templates/reset-view.ts?raw';
26
+
27
+ export interface ScriptTemplate {
28
+ name: string;
29
+ description: string;
30
+ code: string;
31
+ }
32
+
33
+ /** Strip the `export {}` module boundary line that enables type checking */
34
+ function stripModuleLine(raw: string): string {
35
+ return raw.replace(/^export \{\}[^\n]*\n\n?/, '');
36
+ }
37
+
38
+ export const SCRIPT_TEMPLATES: ScriptTemplate[] = [
39
+ {
40
+ name: 'Data quality audit',
41
+ description:
42
+ 'BIM Manager — scan all entities for missing names, properties, and quantities; score model completeness; color-code by data quality',
43
+ code: stripModuleLine(dataQualityAudit),
44
+ },
45
+ {
46
+ name: 'Fire safety compliance',
47
+ description:
48
+ 'Architect — check fire ratings across walls, doors, and slabs; flag load-bearing elements without ratings; export non-compliant list',
49
+ code: stripModuleLine(fireSafetyCheck),
50
+ },
51
+ {
52
+ name: 'Quantity takeoff',
53
+ description:
54
+ 'Cost Estimator — aggregate area, volume, and length quantities across all element types; generate material takeoff table and CSV',
55
+ code: stripModuleLine(quantityTakeoff),
56
+ },
57
+ {
58
+ name: 'Envelope & thermal check',
59
+ description:
60
+ 'Energy Consultant — identify external elements, check thermal transmittance values, isolate building envelope, flag missing data',
61
+ code: stripModuleLine(envelopeCheck),
62
+ },
63
+ {
64
+ name: 'MEP equipment schedule',
65
+ description:
66
+ 'HVAC Engineer — discover all distribution elements, extract equipment properties, generate schedule, isolate and color by system',
67
+ code: stripModuleLine(mepEquipmentSchedule),
68
+ },
69
+ {
70
+ name: 'Space & room validation',
71
+ description:
72
+ 'Facility Manager — validate IfcSpace entities for area, volume, naming; generate room schedule with totals; flag incomplete spaces',
73
+ code: stripModuleLine(spaceValidation),
74
+ },
75
+ {
76
+ name: 'Federation comparison',
77
+ description:
78
+ 'Project Manager — compare multiple loaded models side by side: entity counts, type coverage, naming consistency, coordination issues',
79
+ code: stripModuleLine(federationCompare),
80
+ },
81
+ {
82
+ name: 'Reset view',
83
+ description: 'Utility — remove all color overrides and show all entities',
84
+ code: stripModuleLine(resetView),
85
+ },
86
+ ];
@@ -0,0 +1,50 @@
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
+ * BimProvider — React context for the SDK's BimContext.
7
+ *
8
+ * Wraps useBimHost() and makes the `bim` object available to all children
9
+ * via useBim(). This enables gradual migration: components can progressively
10
+ * switch from direct Zustand store calls to SDK calls.
11
+ *
12
+ * Usage:
13
+ * <BimProvider>
14
+ * <App />
15
+ * </BimProvider>
16
+ *
17
+ * // In any component:
18
+ * const bim = useBim();
19
+ * const walls = bim.query().byType('IfcWall').toArray();
20
+ */
21
+
22
+ import { createContext, useContext, type ReactNode } from 'react';
23
+ import type { BimContext } from '@ifc-lite/sdk';
24
+ import { useBimHost } from './useBimHost.js';
25
+
26
+ const BimReactContext = createContext<BimContext | null>(null);
27
+
28
+ /** Provider that initializes the SDK and makes it available via useBim() */
29
+ export function BimProvider({ children }: { children: ReactNode }) {
30
+ const bim = useBimHost();
31
+ return (
32
+ <BimReactContext.Provider value={bim}>
33
+ {children}
34
+ </BimReactContext.Provider>
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Access the BimContext from any component.
40
+ * Must be rendered inside a <BimProvider>.
41
+ *
42
+ * @throws if used outside a BimProvider
43
+ */
44
+ export function useBim(): BimContext {
45
+ const ctx = useContext(BimReactContext);
46
+ if (!ctx) {
47
+ throw new Error('useBim() must be used within a <BimProvider>');
48
+ }
49
+ return ctx;
50
+ }
@@ -0,0 +1,283 @@
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
+ import type { StoreApi } from './types.js';
6
+ import type { EntityRef, EntityData, PropertySetData, QuantitySetData, ExportBackendMethods } from '@ifc-lite/sdk';
7
+ import { EntityNode } from '@ifc-lite/query';
8
+ import { getModelForRef } from './model-compat.js';
9
+
10
+ /** Options for CSV export */
11
+ interface CsvOptions {
12
+ columns: string[];
13
+ separator?: string;
14
+ filename?: string;
15
+ }
16
+
17
+ /**
18
+ * Validate that a value is a CsvOptions object.
19
+ */
20
+ function isCsvOptions(v: unknown): v is CsvOptions {
21
+ if (v === null || typeof v !== 'object' || !('columns' in v)) return false;
22
+ const columns = (v as CsvOptions).columns;
23
+ if (!Array.isArray(columns)) return false;
24
+ // Validate all column entries are strings
25
+ return columns.every((c): c is string => typeof c === 'string');
26
+ }
27
+
28
+ /**
29
+ * Validate that a value is an array of EntityRef objects.
30
+ */
31
+ function isEntityRefArray(v: unknown): v is EntityRef[] {
32
+ if (!Array.isArray(v)) return false;
33
+ if (v.length === 0) return true;
34
+ const first = v[0] as Record<string, unknown>;
35
+ // Accept both raw EntityRef and entity proxy objects with .ref
36
+ if ('modelId' in first && 'expressId' in first) {
37
+ return typeof first.modelId === 'string' && typeof first.expressId === 'number';
38
+ }
39
+ if ('ref' in first && first.ref !== null && typeof first.ref === 'object') {
40
+ const ref = first.ref as Record<string, unknown>;
41
+ return typeof ref.modelId === 'string' && typeof ref.expressId === 'number';
42
+ }
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * Normalize entity refs — entities from the sandbox may be EntityData
48
+ * objects with a .ref property, or raw EntityRef { modelId, expressId }.
49
+ */
50
+ function normalizeRefs(raw: unknown[]): EntityRef[] {
51
+ return raw.map((item) => {
52
+ const r = item as Record<string, unknown>;
53
+ if (r.ref && typeof r.ref === 'object') {
54
+ return r.ref as EntityRef;
55
+ }
56
+ return { modelId: r.modelId as string, expressId: r.expressId as number };
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Escape a CSV cell value — wrap in quotes if it contains the separator,
62
+ * double-quotes, or newlines.
63
+ */
64
+ function escapeCsv(value: string, sep: string): string {
65
+ if (value.includes(sep) || value.includes('"') || value.includes('\n')) {
66
+ return `"${value.replace(/"/g, '""')}"`;
67
+ }
68
+ return value;
69
+ }
70
+
71
+ /**
72
+ * Export adapter — implements CSV and JSON export directly.
73
+ *
74
+ * This adapter resolves entity data by dispatching to the query adapter
75
+ * on the same LocalBackend, providing full export support for both
76
+ * direct dispatch calls and SDK namespace usage.
77
+ */
78
+ export function createExportAdapter(store: StoreApi): ExportBackendMethods {
79
+ /** Resolve entity data via the query subsystem */
80
+ function getEntityData(ref: EntityRef): EntityData | null {
81
+ const state = store.getState();
82
+ const model = getModelForRef(state, ref.modelId);
83
+ if (!model?.ifcDataStore) return null;
84
+
85
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
86
+ return {
87
+ ref,
88
+ globalId: node.globalId,
89
+ name: node.name,
90
+ type: node.type,
91
+ description: node.description,
92
+ objectType: node.objectType,
93
+ };
94
+ }
95
+
96
+ /** Resolve property sets for an entity */
97
+ function getProperties(ref: EntityRef): PropertySetData[] {
98
+ const state = store.getState();
99
+ const model = getModelForRef(state, ref.modelId);
100
+ if (!model?.ifcDataStore) return [];
101
+
102
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
103
+ return node.properties().map((pset: { name: string; globalId?: string; properties: Array<{ name: string; type: number; value: string | number | boolean | null }> }) => ({
104
+ name: pset.name,
105
+ globalId: pset.globalId,
106
+ properties: pset.properties.map((p: { name: string; type: number; value: string | number | boolean | null }) => ({
107
+ name: p.name,
108
+ type: p.type,
109
+ value: p.value,
110
+ })),
111
+ }));
112
+ }
113
+
114
+ /** Resolve quantity sets for an entity */
115
+ function getQuantities(ref: EntityRef): QuantitySetData[] {
116
+ const state = store.getState();
117
+ const model = getModelForRef(state, ref.modelId);
118
+ if (!model?.ifcDataStore) return [];
119
+
120
+ const node = new EntityNode(model.ifcDataStore, ref.expressId);
121
+ return node.quantities().map((qset: { name: string; quantities: Array<{ name: string; type: number; value: number }> }) => ({
122
+ name: qset.name,
123
+ quantities: qset.quantities.map((q: { name: string; type: number; value: number }) => ({
124
+ name: q.name,
125
+ type: q.type,
126
+ value: q.value,
127
+ })),
128
+ }));
129
+ }
130
+
131
+ /** Resolve a single column value from entity data + properties + quantities.
132
+ * Accepts both IFC PascalCase (Name, GlobalId) and legacy camelCase (name, globalId).
133
+ * Dot-path columns (e.g. "Pset_WallCommon.FireRating" or "Qto_WallBaseQuantities.GrossVolume")
134
+ * resolve against property sets first, then quantity sets. */
135
+ function resolveColumnValue(
136
+ data: EntityData,
137
+ col: string,
138
+ getProps: () => PropertySetData[],
139
+ getQties: () => QuantitySetData[],
140
+ ): string {
141
+ // IFC schema attribute names (PascalCase) + legacy camelCase
142
+ switch (col) {
143
+ case 'Name': case 'name': return data.name;
144
+ case 'Type': case 'type': return data.type;
145
+ case 'GlobalId': case 'globalId': return data.globalId;
146
+ case 'Description': case 'description': return data.description;
147
+ case 'ObjectType': case 'objectType': return data.objectType;
148
+ case 'modelId': return data.ref.modelId;
149
+ case 'expressId': return String(data.ref.expressId);
150
+ }
151
+
152
+ // Property/Quantity path: "SetName.ValueName"
153
+ const dotIdx = col.indexOf('.');
154
+ if (dotIdx > 0) {
155
+ const setName = col.slice(0, dotIdx);
156
+ const valueName = col.slice(dotIdx + 1);
157
+
158
+ // Try property sets first
159
+ const psets = getProps();
160
+ const pset = psets.find(p => p.name === setName);
161
+ if (pset) {
162
+ const prop = pset.properties.find(p => p.name === valueName);
163
+ if (prop?.value != null) return String(prop.value);
164
+ }
165
+
166
+ // Fall back to quantity sets
167
+ const qsets = getQties();
168
+ const qset = qsets.find(q => q.name === setName);
169
+ if (qset) {
170
+ const qty = qset.quantities.find(q => q.name === valueName);
171
+ if (qty?.value != null) return String(qty.value);
172
+ }
173
+
174
+ return '';
175
+ }
176
+
177
+ return '';
178
+ }
179
+
180
+ return {
181
+ csv(rawRefs: unknown, rawOptions: unknown) {
182
+ if (!isEntityRefArray(rawRefs)) {
183
+ throw new Error('export.csv: first argument must be an array of entity references');
184
+ }
185
+ if (!isCsvOptions(rawOptions)) {
186
+ throw new Error('export.csv: second argument must be { columns: string[], separator?: string }');
187
+ }
188
+
189
+ const refs = normalizeRefs(rawRefs);
190
+ const options = rawOptions;
191
+ const sep = options.separator ?? ',';
192
+ const rows: string[][] = [];
193
+
194
+ // Header row
195
+ rows.push(options.columns);
196
+
197
+ // Data rows
198
+ for (const ref of refs) {
199
+ const data = getEntityData(ref);
200
+ if (!data) continue;
201
+
202
+ // Lazy-load properties/quantities only if a column needs them
203
+ let cachedProps: PropertySetData[] | null = null;
204
+ const getProps = (): PropertySetData[] => {
205
+ if (!cachedProps) cachedProps = getProperties(ref);
206
+ return cachedProps;
207
+ };
208
+ let cachedQties: QuantitySetData[] | null = null;
209
+ const getQties = (): QuantitySetData[] => {
210
+ if (!cachedQties) cachedQties = getQuantities(ref);
211
+ return cachedQties;
212
+ };
213
+
214
+ const row = options.columns.map(col => resolveColumnValue(data, col, getProps, getQties));
215
+ rows.push(row);
216
+ }
217
+
218
+ const csvString = rows.map(r => r.map(cell => escapeCsv(cell, sep)).join(sep)).join('\n');
219
+
220
+ // If filename specified, trigger browser download
221
+ if (options.filename) {
222
+ triggerDownload(csvString, options.filename, 'text/csv;charset=utf-8;');
223
+ }
224
+
225
+ return csvString;
226
+ },
227
+
228
+ json(rawRefs: unknown, columns: unknown) {
229
+ if (!isEntityRefArray(rawRefs)) {
230
+ throw new Error('export.json: first argument must be an array of entity references');
231
+ }
232
+ if (!Array.isArray(columns)) {
233
+ throw new Error('export.json: second argument must be a string[] of column names');
234
+ }
235
+
236
+ const refs = normalizeRefs(rawRefs);
237
+ const result: Record<string, unknown>[] = [];
238
+
239
+ for (const ref of refs) {
240
+ const data = getEntityData(ref);
241
+ if (!data) continue;
242
+
243
+ let cachedProps: PropertySetData[] | null = null;
244
+ const getProps = (): PropertySetData[] => {
245
+ if (!cachedProps) cachedProps = getProperties(ref);
246
+ return cachedProps;
247
+ };
248
+ let cachedQties: QuantitySetData[] | null = null;
249
+ const getQties = (): QuantitySetData[] => {
250
+ if (!cachedQties) cachedQties = getQuantities(ref);
251
+ return cachedQties;
252
+ };
253
+
254
+ const row: Record<string, unknown> = {};
255
+ for (const col of columns as string[]) {
256
+ const value = resolveColumnValue(data, col, getProps, getQties);
257
+ // Try to parse numeric values
258
+ const numVal = Number(value);
259
+ row[col] = value === '' ? null : !isNaN(numVal) && value.trim() !== '' ? numVal : value;
260
+ }
261
+ result.push(row);
262
+ }
263
+
264
+ return result;
265
+ },
266
+
267
+ download(content: string, filename: string, mimeType?: string) {
268
+ triggerDownload(content, filename, mimeType ?? 'text/plain');
269
+ return undefined;
270
+ },
271
+ };
272
+ }
273
+
274
+ /** Trigger a browser file download */
275
+ function triggerDownload(content: string, filename: string, mimeType: string): void {
276
+ const blob = new Blob([content], { type: mimeType });
277
+ const url = URL.createObjectURL(blob);
278
+ const a = document.createElement('a');
279
+ a.href = url;
280
+ a.download = filename;
281
+ a.click();
282
+ URL.revokeObjectURL(url);
283
+ }
@@ -0,0 +1,44 @@
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
+ import type { LensBackendMethods } from '@ifc-lite/sdk';
6
+ import type { StoreApi } from './types.js';
7
+ import { BUILTIN_LENSES } from '@ifc-lite/lens';
8
+
9
+ /** Type guard for lens config object */
10
+ function isLensConfig(v: unknown): v is Record<string, unknown> {
11
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
12
+ }
13
+
14
+ export function createLensAdapter(store: StoreApi): LensBackendMethods {
15
+ return {
16
+ presets() {
17
+ return BUILTIN_LENSES;
18
+ },
19
+ create(config: unknown) {
20
+ if (!isLensConfig(config)) {
21
+ throw new Error('lens.create: argument must be a lens configuration object');
22
+ }
23
+ const id = crypto.randomUUID();
24
+ return { ...config, id };
25
+ },
26
+ activate(lensId: unknown) {
27
+ if (typeof lensId !== 'string') {
28
+ throw new Error('lens.activate: argument must be a lens ID string');
29
+ }
30
+ const state = store.getState();
31
+ state.setActiveLens?.(lensId);
32
+ return undefined;
33
+ },
34
+ deactivate() {
35
+ const state = store.getState();
36
+ state.setActiveLens?.(null);
37
+ return undefined;
38
+ },
39
+ getActive() {
40
+ const state = store.getState();
41
+ return state.activeLensId ?? null;
42
+ },
43
+ };
44
+ }