@ifc-lite/viewer 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
  3. package/dist/assets/browser-BXNIkE8a.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/index-6Mr3byM-.js +216 -0
  13. package/dist/assets/index-CGbokkQ9.css +1 -0
  14. package/dist/assets/index-huvR-kGC.js +98305 -0
  15. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  16. package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
  17. package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
  18. package/dist/index.html +12 -3
  19. package/index.html +10 -1
  20. package/package.json +30 -21
  21. package/src/App.tsx +6 -1
  22. package/src/components/ui/dialog.tsx +8 -6
  23. package/src/components/viewer/CodeEditor.tsx +309 -0
  24. package/src/components/viewer/CommandPalette.tsx +597 -0
  25. package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
  26. package/src/components/viewer/EntityContextMenu.tsx +47 -20
  27. package/src/components/viewer/ExportDialog.tsx +166 -17
  28. package/src/components/viewer/HierarchyPanel.tsx +3 -1
  29. package/src/components/viewer/LensPanel.tsx +848 -85
  30. package/src/components/viewer/MainToolbar.tsx +145 -84
  31. package/src/components/viewer/ScriptPanel.tsx +416 -0
  32. package/src/components/viewer/Section2DPanel.tsx +269 -29
  33. package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
  34. package/src/components/viewer/ViewerLayout.tsx +63 -11
  35. package/src/components/viewer/Viewport.tsx +58 -23
  36. package/src/components/viewer/ViewportContainer.tsx +2 -0
  37. package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
  38. package/src/components/viewer/hierarchy/types.ts +1 -1
  39. package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
  40. package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
  41. package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
  42. package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
  43. package/src/components/viewer/tools/computePolygonArea.ts +72 -0
  44. package/src/components/viewer/useGeometryStreaming.ts +25 -5
  45. package/src/hooks/ids/idsExportService.ts +1 -1
  46. package/src/hooks/useAnnotation2D.ts +551 -0
  47. package/src/hooks/useDrawingExport.ts +83 -1
  48. package/src/hooks/useKeyboardShortcuts.ts +114 -14
  49. package/src/hooks/useLens.ts +40 -55
  50. package/src/hooks/useLensDiscovery.ts +46 -0
  51. package/src/hooks/useModelSelection.ts +5 -22
  52. package/src/hooks/useSandbox.ts +113 -0
  53. package/src/index.css +7 -1
  54. package/src/lib/lens/adapter.ts +127 -1
  55. package/src/lib/lists/columnToAutoColor.ts +33 -0
  56. package/src/lib/recent-files.ts +122 -0
  57. package/src/lib/scripts/persistence.ts +132 -0
  58. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  59. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  60. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  61. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  62. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  63. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  64. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  65. package/src/lib/scripts/templates/reset-view.ts +6 -0
  66. package/src/lib/scripts/templates/space-validation.ts +189 -0
  67. package/src/lib/scripts/templates/tsconfig.json +13 -0
  68. package/src/lib/scripts/templates.ts +86 -0
  69. package/src/sdk/BimProvider.tsx +50 -0
  70. package/src/sdk/adapters/export-adapter.ts +283 -0
  71. package/src/sdk/adapters/lens-adapter.ts +44 -0
  72. package/src/sdk/adapters/model-adapter.ts +32 -0
  73. package/src/sdk/adapters/model-compat.ts +80 -0
  74. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  75. package/src/sdk/adapters/query-adapter.ts +241 -0
  76. package/src/sdk/adapters/selection-adapter.ts +29 -0
  77. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  78. package/src/sdk/adapters/types.ts +11 -0
  79. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  80. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  81. package/src/sdk/local-backend.ts +144 -0
  82. package/src/sdk/useBimHost.ts +69 -0
  83. package/src/store/constants.ts +10 -2
  84. package/src/store/index.ts +28 -2
  85. package/src/store/resolveEntityRef.ts +44 -0
  86. package/src/store/slices/drawing2DSlice.ts +321 -0
  87. package/src/store/slices/lensSlice.ts +46 -4
  88. package/src/store/slices/pinboardSlice.ts +171 -42
  89. package/src/store/slices/scriptSlice.ts +218 -0
  90. package/src/store/slices/uiSlice.ts +2 -0
  91. package/src/store.ts +3 -0
  92. package/tsconfig.json +5 -2
  93. package/vite.config.ts +8 -0
  94. package/dist/assets/index-dgdgiQ9p.js +0 -75456
  95. package/dist/assets/index-yTqs8kgX.css +0 -1
@@ -3,20 +3,26 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
5
  /**
6
- * Pinboard state slice
6
+ * Pinboard (Basket) state slice
7
7
  *
8
- * Persistent selection basket for tracking components across sessions.
9
- * Users can pin entities, then isolate/show the pinned set.
8
+ * The basket is an incremental isolation set. Users build it with:
9
+ * = (set) — replace basket with current selection
10
+ * + (add) — add current selection to basket
11
+ * − (remove) — remove current selection from basket
12
+ *
13
+ * When the basket is non-empty, only basket entities are visible (isolation).
14
+ * The basket also syncs to isolatedEntities for renderer consumption.
10
15
  */
11
16
 
12
17
  import type { StateCreator } from 'zustand';
13
18
  import type { EntityRef } from '../types.js';
14
19
  import { entityRefToString, stringToEntityRef } from '../types.js';
15
20
 
16
- /** Minimal interface for accessing isolation + models from the combined store */
17
- interface CombinedStoreAccess {
18
- isolateEntities?: (ids: number[]) => void;
19
- models?: Map<string, { idOffset: number }>;
21
+ /** Cross-slice state that pinboard reads/writes via the combined store */
22
+ interface PinboardCrossSliceState {
23
+ isolatedEntities: Set<number> | null;
24
+ hiddenEntities: Set<number>;
25
+ models: Map<string, { idOffset: number }>;
20
26
  }
21
27
 
22
28
  export interface PinboardSlice {
@@ -25,46 +31,94 @@ export interface PinboardSlice {
25
31
  pinboardEntities: Set<string>;
26
32
 
27
33
  // Actions
28
- /** Add entities to pinboard */
34
+ /** Add entities to pinboard/basket */
29
35
  addToPinboard: (refs: EntityRef[]) => void;
30
- /** Remove entities from pinboard */
36
+ /** Remove entities from pinboard/basket */
31
37
  removeFromPinboard: (refs: EntityRef[]) => void;
32
- /** Replace pinboard contents */
38
+ /** Replace pinboard/basket contents (= operation) */
33
39
  setPinboard: (refs: EntityRef[]) => void;
34
- /** Clear pinboard */
40
+ /** Clear pinboard/basket and isolation */
35
41
  clearPinboard: () => void;
36
- /** Isolate pinboard entities (show only pinned) */
42
+ /** Isolate pinboard entities (sync basket → isolatedEntities) */
37
43
  showPinboard: () => void;
38
- /** Check if entity is pinned */
44
+ /** Check if entity is in basket */
39
45
  isInPinboard: (ref: EntityRef) => boolean;
40
- /** Get pinboard count */
46
+ /** Get basket count */
41
47
  getPinboardCount: () => number;
42
- /** Get all pinboard entities as EntityRef array */
48
+ /** Get all basket entities as EntityRef array */
43
49
  getPinboardEntities: () => EntityRef[];
50
+
51
+ // Basket actions (semantic aliases that also sync isolation)
52
+ /** = Set basket to exactly these entities and isolate them */
53
+ setBasket: (refs: EntityRef[]) => void;
54
+ /** + Add entities to basket and update isolation */
55
+ addToBasket: (refs: EntityRef[]) => void;
56
+ /** − Remove entities from basket and update isolation */
57
+ removeFromBasket: (refs: EntityRef[]) => void;
58
+ /** Clear basket and clear isolation */
59
+ clearBasket: () => void;
60
+ }
61
+
62
+ /** Convert basket EntityRefs to global IDs using model offsets */
63
+ function basketToGlobalIds(
64
+ basketEntities: Set<string>,
65
+ models: Map<string, { idOffset: number }>,
66
+ ): Set<number> {
67
+ const globalIds = new Set<number>();
68
+ for (const str of basketEntities) {
69
+ const ref = stringToEntityRef(str);
70
+ const model = models.get(ref.modelId);
71
+ const offset = model?.idOffset ?? 0;
72
+ globalIds.add(ref.expressId + offset);
73
+ }
74
+ return globalIds;
75
+ }
76
+
77
+ /** Compute a single EntityRef's global ID */
78
+ function refToGlobalId(ref: EntityRef, models: Map<string, { idOffset: number }>): number {
79
+ const model = models.get(ref.modelId);
80
+ return ref.expressId + (model?.idOffset ?? 0);
44
81
  }
45
82
 
46
- export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSlice> = (set, get) => ({
83
+ export const createPinboardSlice: StateCreator<
84
+ PinboardSlice & PinboardCrossSliceState,
85
+ [],
86
+ [],
87
+ PinboardSlice
88
+ > = (set, get) => ({
47
89
  // Initial state
48
90
  pinboardEntities: new Set(),
49
91
 
50
- // Actions
92
+ // Legacy actions (kept for backward compat, but now they also sync isolation)
51
93
  addToPinboard: (refs) => {
52
94
  set((state) => {
53
- const next = new Set(state.pinboardEntities);
95
+ const next = new Set<string>(state.pinboardEntities);
54
96
  for (const ref of refs) {
55
97
  next.add(entityRefToString(ref));
56
98
  }
57
- return { pinboardEntities: next };
99
+ const isolatedEntities = basketToGlobalIds(next, state.models);
100
+ const hiddenEntities = new Set<number>(state.hiddenEntities);
101
+ // Unhide any entities being added to basket
102
+ for (const ref of refs) {
103
+ const model = state.models.get(ref.modelId);
104
+ const offset = model?.idOffset ?? 0;
105
+ hiddenEntities.delete(ref.expressId + offset);
106
+ }
107
+ return { pinboardEntities: next, isolatedEntities, hiddenEntities };
58
108
  });
59
109
  },
60
110
 
61
111
  removeFromPinboard: (refs) => {
62
112
  set((state) => {
63
- const next = new Set(state.pinboardEntities);
113
+ const next = new Set<string>(state.pinboardEntities);
64
114
  for (const ref of refs) {
65
115
  next.delete(entityRefToString(ref));
66
116
  }
67
- return { pinboardEntities: next };
117
+ if (next.size === 0) {
118
+ return { pinboardEntities: next, isolatedEntities: null };
119
+ }
120
+ const isolatedEntities = basketToGlobalIds(next, state.models);
121
+ return { pinboardEntities: next, isolatedEntities };
68
122
  });
69
123
  },
70
124
 
@@ -73,31 +127,29 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
73
127
  for (const ref of refs) {
74
128
  next.add(entityRefToString(ref));
75
129
  }
76
- set({ pinboardEntities: next });
130
+ if (next.size === 0) {
131
+ set({ pinboardEntities: next, isolatedEntities: null });
132
+ return;
133
+ }
134
+ const s = get();
135
+ const hiddenEntities = new Set<number>(s.hiddenEntities);
136
+ // Unhide basket entities
137
+ for (const ref of refs) {
138
+ const model = s.models.get(ref.modelId);
139
+ const offset = model?.idOffset ?? 0;
140
+ hiddenEntities.delete(ref.expressId + offset);
141
+ }
142
+ const isolatedEntities = basketToGlobalIds(next, s.models);
143
+ set({ pinboardEntities: next, isolatedEntities, hiddenEntities });
77
144
  },
78
145
 
79
- clearPinboard: () => set({ pinboardEntities: new Set() }),
146
+ clearPinboard: () => set({ pinboardEntities: new Set(), isolatedEntities: null }),
80
147
 
81
148
  showPinboard: () => {
82
- const entities = get().getPinboardEntities();
83
- if (entities.length === 0) return;
84
-
85
- // Access combined store methods via typed interface
86
- const store = get() as unknown as CombinedStoreAccess;
87
- if (!store.isolateEntities) return;
88
-
89
- // Convert EntityRef to global IDs for isolation
90
- const globalIds: number[] = [];
91
- for (const ref of entities) {
92
- if (store.models) {
93
- const model = store.models.get(ref.modelId);
94
- const offset = model?.idOffset ?? 0;
95
- globalIds.push(ref.expressId + offset);
96
- } else {
97
- globalIds.push(ref.expressId);
98
- }
99
- }
100
- store.isolateEntities(globalIds);
149
+ const state = get();
150
+ if (state.pinboardEntities.size === 0) return;
151
+ const isolatedEntities = basketToGlobalIds(state.pinboardEntities, state.models);
152
+ set({ isolatedEntities });
101
153
  },
102
154
 
103
155
  isInPinboard: (ref) => get().pinboardEntities.has(entityRefToString(ref)),
@@ -111,4 +163,81 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
111
163
  }
112
164
  return result;
113
165
  },
166
+
167
+ // ──────────────────────────────────────────────────────────────────────────
168
+ // Basket actions (= + −)
169
+ // These are the primary API for the new basket-based isolation UX.
170
+ // ──────────────────────────────────────────────────────────────────────────
171
+
172
+ /** = Set basket to exactly these entities and isolate them */
173
+ setBasket: (refs) => {
174
+ if (refs.length === 0) {
175
+ set({ pinboardEntities: new Set(), isolatedEntities: null });
176
+ return;
177
+ }
178
+ const next = new Set<string>();
179
+ for (const ref of refs) {
180
+ next.add(entityRefToString(ref));
181
+ }
182
+ const s = get();
183
+ const hiddenEntities = new Set<number>(s.hiddenEntities);
184
+ // Unhide basket entities
185
+ for (const ref of refs) {
186
+ const model = s.models.get(ref.modelId);
187
+ const offset = model?.idOffset ?? 0;
188
+ hiddenEntities.delete(ref.expressId + offset);
189
+ }
190
+ const isolatedEntities = basketToGlobalIds(next, s.models);
191
+ set({ pinboardEntities: next, isolatedEntities, hiddenEntities });
192
+ },
193
+
194
+ /** + Add entities to basket and update isolation (incremental — avoids re-parsing all strings) */
195
+ addToBasket: (refs) => {
196
+ if (refs.length === 0) return;
197
+ set((state) => {
198
+ const next = new Set<string>(state.pinboardEntities);
199
+ for (const ref of refs) {
200
+ next.add(entityRefToString(ref));
201
+ }
202
+ const hiddenEntities = new Set<number>(state.hiddenEntities);
203
+ // Incrementally add new globalIds to existing isolation set instead of re-parsing all
204
+ const prevIsolated = state.isolatedEntities;
205
+ const isolatedEntities = prevIsolated ? new Set<number>(prevIsolated) : basketToGlobalIds(state.pinboardEntities, state.models);
206
+ for (const ref of refs) {
207
+ const gid = refToGlobalId(ref, state.models);
208
+ isolatedEntities.add(gid);
209
+ hiddenEntities.delete(gid);
210
+ }
211
+ return { pinboardEntities: next, isolatedEntities, hiddenEntities };
212
+ });
213
+ },
214
+
215
+ /** − Remove entities from basket and update isolation (incremental — avoids re-parsing all strings) */
216
+ removeFromBasket: (refs) => {
217
+ if (refs.length === 0) return;
218
+ set((state) => {
219
+ const next = new Set<string>(state.pinboardEntities);
220
+ for (const ref of refs) {
221
+ next.delete(entityRefToString(ref));
222
+ }
223
+ if (next.size === 0) {
224
+ return { pinboardEntities: next, isolatedEntities: null };
225
+ }
226
+ // Incrementally remove globalIds from existing isolation set instead of re-parsing all
227
+ const prevIsolated = state.isolatedEntities;
228
+ if (prevIsolated) {
229
+ const isolatedEntities = new Set<number>(prevIsolated);
230
+ for (const ref of refs) {
231
+ isolatedEntities.delete(refToGlobalId(ref, state.models));
232
+ }
233
+ return { pinboardEntities: next, isolatedEntities };
234
+ }
235
+ // Fallback: full recompute if no existing isolation set
236
+ const isolatedEntities = basketToGlobalIds(next, state.models);
237
+ return { pinboardEntities: next, isolatedEntities };
238
+ });
239
+ },
240
+
241
+ /** Clear basket and clear isolation */
242
+ clearBasket: () => set({ pinboardEntities: new Set(), isolatedEntities: null }),
114
243
  });
@@ -0,0 +1,218 @@
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
+ * Script state slice — manages script editor state, saved scripts,
7
+ * and execution results.
8
+ */
9
+
10
+ import type { StateCreator } from 'zustand';
11
+ import type { SavedScript } from '../../lib/scripts/persistence.js';
12
+ import { loadSavedScripts, saveScripts, validateScriptName, canCreateScript, isScriptWithinSizeLimit } from '../../lib/scripts/persistence.js';
13
+
14
+ export type ScriptExecutionState = 'idle' | 'running' | 'error' | 'success';
15
+
16
+ export interface LogEntry {
17
+ level: 'log' | 'warn' | 'error' | 'info';
18
+ args: unknown[];
19
+ timestamp: number;
20
+ }
21
+
22
+ export interface ScriptResult {
23
+ value: unknown;
24
+ logs: LogEntry[];
25
+ durationMs: number;
26
+ }
27
+
28
+ export interface ScriptSlice {
29
+ // State
30
+ savedScripts: SavedScript[];
31
+ activeScriptId: string | null;
32
+ scriptEditorContent: string;
33
+ scriptEditorDirty: boolean;
34
+ scriptExecutionState: ScriptExecutionState;
35
+ scriptLastResult: ScriptResult | null;
36
+ scriptLastError: string | null;
37
+ scriptPanelVisible: boolean;
38
+ scriptDeleteConfirmId: string | null;
39
+
40
+ // Actions
41
+ createScript: (name: string, code?: string) => string;
42
+ saveActiveScript: () => void;
43
+ deleteScript: (id: string) => void;
44
+ renameScript: (id: string, name: string) => void;
45
+ setActiveScriptId: (id: string | null) => void;
46
+ setScriptEditorContent: (content: string) => void;
47
+ setScriptExecutionState: (state: ScriptExecutionState) => void;
48
+ setScriptResult: (result: ScriptResult | null) => void;
49
+ setScriptError: (error: string | null) => void;
50
+ setScriptPanelVisible: (visible: boolean) => void;
51
+ toggleScriptPanel: () => void;
52
+ setScriptDeleteConfirmId: (id: string | null) => void;
53
+ }
54
+
55
+ const DEFAULT_CODE = `// Write your BIM script here
56
+ // The 'bim' object provides access to the SDK
57
+ const models = bim.model.list()
58
+ console.log('Loaded models:', models.length)
59
+
60
+ // Query all entities
61
+ const all = bim.query.all()
62
+ console.log('Total entities:', all.length)
63
+
64
+ // Count by type
65
+ const counts = {}
66
+ for (const e of all) {
67
+ counts[e.type] = (counts[e.type] || 0) + 1
68
+ }
69
+ for (const [type, count] of Object.entries(counts).sort((a, b) => b[1] - a[1])) {
70
+ console.log(' ' + type + ': ' + count)
71
+ }
72
+ `;
73
+
74
+ export const createScriptSlice: StateCreator<ScriptSlice, [], [], ScriptSlice> = (set, get) => ({
75
+ // Initial state
76
+ savedScripts: loadSavedScripts(),
77
+ activeScriptId: null,
78
+ scriptEditorContent: DEFAULT_CODE,
79
+ scriptEditorDirty: false,
80
+ scriptExecutionState: 'idle',
81
+ scriptLastResult: null,
82
+ scriptLastError: null,
83
+ scriptPanelVisible: false,
84
+ scriptDeleteConfirmId: null,
85
+
86
+ // Actions
87
+ createScript: (name, code) => {
88
+ const { savedScripts } = get();
89
+ if (!canCreateScript(savedScripts.length)) {
90
+ console.warn('[Scripts] Maximum script limit reached');
91
+ return '';
92
+ }
93
+
94
+ const validName = validateScriptName(name) ?? 'Untitled Script';
95
+ const scriptCode = code ?? DEFAULT_CODE;
96
+ if (!isScriptWithinSizeLimit(scriptCode)) {
97
+ console.warn('[Scripts] Script code exceeds maximum size limit');
98
+ return '';
99
+ }
100
+ const id = crypto.randomUUID();
101
+ const now = Date.now();
102
+ const script: SavedScript = {
103
+ id,
104
+ name: validName,
105
+ code: scriptCode,
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ version: 1,
109
+ };
110
+ const updated = [...savedScripts, script];
111
+ set({
112
+ savedScripts: updated,
113
+ activeScriptId: id,
114
+ scriptEditorContent: script.code,
115
+ scriptEditorDirty: false,
116
+ });
117
+ const result = saveScripts(updated);
118
+ if (!result.ok) {
119
+ console.warn('[Scripts] Save failed:', result.message);
120
+ }
121
+ return id;
122
+ },
123
+
124
+ saveActiveScript: () => {
125
+ const { activeScriptId, scriptEditorContent, savedScripts } = get();
126
+ if (!activeScriptId) return;
127
+ const updated = savedScripts.map((s) =>
128
+ s.id === activeScriptId
129
+ ? { ...s, code: scriptEditorContent, updatedAt: Date.now() }
130
+ : s,
131
+ );
132
+ set({ savedScripts: updated, scriptEditorDirty: false });
133
+ const result = saveScripts(updated);
134
+ if (!result.ok) {
135
+ console.warn('[Scripts] Save failed:', result.message);
136
+ }
137
+ },
138
+
139
+ deleteScript: (id) => {
140
+ const updated = get().savedScripts.filter((s) => s.id !== id);
141
+ const activeScriptId = get().activeScriptId === id ? null : get().activeScriptId;
142
+ const scriptEditorContent = activeScriptId === null ? DEFAULT_CODE : get().scriptEditorContent;
143
+ set({
144
+ savedScripts: updated,
145
+ activeScriptId,
146
+ scriptEditorContent,
147
+ scriptEditorDirty: false,
148
+ scriptDeleteConfirmId: null,
149
+ });
150
+ saveScripts(updated);
151
+ },
152
+
153
+ renameScript: (id, name) => {
154
+ const validName = validateScriptName(name);
155
+ if (!validName) return;
156
+ const updated = get().savedScripts.map((s) =>
157
+ s.id === id ? { ...s, name: validName, updatedAt: Date.now() } : s,
158
+ );
159
+ set({ savedScripts: updated });
160
+ saveScripts(updated);
161
+ },
162
+
163
+ setActiveScriptId: (activeScriptId) => {
164
+ // Save current before switching
165
+ const { activeScriptId: current, scriptEditorDirty } = get();
166
+ if (current && scriptEditorDirty) {
167
+ get().saveActiveScript();
168
+ }
169
+
170
+ if (activeScriptId) {
171
+ const script = get().savedScripts.find((s) => s.id === activeScriptId);
172
+ if (script) {
173
+ set({
174
+ activeScriptId,
175
+ scriptEditorContent: script.code,
176
+ scriptEditorDirty: false,
177
+ scriptLastResult: null,
178
+ scriptLastError: null,
179
+ scriptExecutionState: 'idle',
180
+ });
181
+ return;
182
+ }
183
+ }
184
+ set({
185
+ activeScriptId: null,
186
+ scriptEditorContent: DEFAULT_CODE,
187
+ scriptEditorDirty: false,
188
+ scriptLastResult: null,
189
+ scriptLastError: null,
190
+ scriptExecutionState: 'idle',
191
+ });
192
+ },
193
+
194
+ setScriptEditorContent: (scriptEditorContent) => {
195
+ set({ scriptEditorContent, scriptEditorDirty: true });
196
+ },
197
+
198
+ setScriptExecutionState: (scriptExecutionState) => set({ scriptExecutionState }),
199
+
200
+ setScriptResult: (scriptLastResult) =>
201
+ set({ scriptLastResult, scriptLastError: null, scriptExecutionState: 'success' }),
202
+
203
+ // Error and execution state are set independently — clearing an error
204
+ // does NOT change execution state unless explicitly transitioned
205
+ setScriptError: (scriptLastError) => {
206
+ if (scriptLastError) {
207
+ set({ scriptLastError, scriptExecutionState: 'error' });
208
+ } else {
209
+ set({ scriptLastError: null });
210
+ }
211
+ },
212
+
213
+ setScriptPanelVisible: (scriptPanelVisible) => set({ scriptPanelVisible }),
214
+
215
+ toggleScriptPanel: () => set((state) => ({ scriptPanelVisible: !state.scriptPanelVisible })),
216
+
217
+ setScriptDeleteConfirmId: (scriptDeleteConfirmId) => set({ scriptDeleteConfirmId }),
218
+ });
@@ -44,12 +44,14 @@ export const createUISlice: StateCreator<UISlice, [], [], UISlice> = (set, get)
44
44
 
45
45
  setTheme: (theme) => {
46
46
  document.documentElement.classList.toggle('dark', theme === 'dark');
47
+ localStorage.setItem('ifc-lite-theme', theme);
47
48
  set({ theme });
48
49
  },
49
50
 
50
51
  toggleTheme: () => {
51
52
  const newTheme = get().theme === 'dark' ? 'light' : 'dark';
52
53
  document.documentElement.classList.toggle('dark', newTheme === 'dark');
54
+ localStorage.setItem('ifc-lite-theme', newTheme);
53
55
  set({ theme: newTheme });
54
56
  },
55
57
 
package/src/store.ts CHANGED
@@ -45,3 +45,6 @@ export type {
45
45
 
46
46
  // Re-export utility functions for multi-model federation
47
47
  export { entityRefToString, stringToEntityRef, entityRefEquals, isIfcxDataStore } from './store/types.js';
48
+
49
+ // Re-export single source of truth for globalId → EntityRef resolution
50
+ export { resolveEntityRef } from './store/resolveEntityRef.js';
package/tsconfig.json CHANGED
@@ -13,8 +13,11 @@
13
13
  "@ifc-lite/spatial": ["../../packages/spatial/src"],
14
14
  "@ifc-lite/data": ["../../packages/data/src"],
15
15
  "@ifc-lite/export": ["../../packages/export/src"],
16
- "@ifc-lite/cache": ["../../packages/cache/src"]
16
+ "@ifc-lite/cache": ["../../packages/cache/src"],
17
+ "@ifc-lite/sdk": ["../../packages/sdk/src"],
18
+ "@ifc-lite/lens": ["../../packages/lens/src"]
17
19
  }
18
20
  },
19
- "include": ["src/**/*", "../../packages/renderer/src/webgpu-types.d.ts"]
21
+ "include": ["src/**/*", "../../packages/renderer/src/webgpu-types.d.ts"],
22
+ "exclude": ["src/lib/scripts/templates"]
20
23
  }
package/vite.config.ts CHANGED
@@ -182,6 +182,14 @@ export default defineConfig({
182
182
  '@ifc-lite/cache': path.resolve(__dirname, '../../packages/cache/src'),
183
183
  '@ifc-lite/ifcx': path.resolve(__dirname, '../../packages/ifcx/src'),
184
184
  '@ifc-lite/wasm': path.resolve(__dirname, '../../packages/wasm/pkg/ifc-lite.js'),
185
+ '@ifc-lite/sdk': path.resolve(__dirname, '../../packages/sdk/src'),
186
+ '@ifc-lite/lens': path.resolve(__dirname, '../../packages/lens/src'),
187
+ '@ifc-lite/mutations': path.resolve(__dirname, '../../packages/mutations/src'),
188
+ '@ifc-lite/bcf': path.resolve(__dirname, '../../packages/bcf/src'),
189
+ '@ifc-lite/drawing-2d': path.resolve(__dirname, '../../packages/drawing-2d/src'),
190
+ '@ifc-lite/encoding': path.resolve(__dirname, '../../packages/encoding/src'),
191
+ '@ifc-lite/ids': path.resolve(__dirname, '../../packages/ids/src'),
192
+ '@ifc-lite/lists': path.resolve(__dirname, '../../packages/lists/src'),
185
193
  },
186
194
  },
187
195
  server: {