@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,144 @@
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
+ * LocalBackend — implements BimBackend via per-namespace adapters.
7
+ *
8
+ * This is the viewer's internal backend: zero serialization overhead.
9
+ * Each namespace is a typed property with named methods.
10
+ */
11
+
12
+ import type {
13
+ BimBackend,
14
+ BimEventType,
15
+ ModelBackendMethods,
16
+ QueryBackendMethods,
17
+ SelectionBackendMethods,
18
+ VisibilityBackendMethods,
19
+ ViewerBackendMethods,
20
+ MutateBackendMethods,
21
+ SpatialBackendMethods,
22
+ ExportBackendMethods,
23
+ LensBackendMethods,
24
+ } from '@ifc-lite/sdk';
25
+ import type { StoreApi } from './adapters/types.js';
26
+ import { LEGACY_MODEL_ID } from './adapters/model-compat.js';
27
+ import { createModelAdapter } from './adapters/model-adapter.js';
28
+ import { createQueryAdapter } from './adapters/query-adapter.js';
29
+ import { createSelectionAdapter } from './adapters/selection-adapter.js';
30
+ import { createVisibilityAdapter } from './adapters/visibility-adapter.js';
31
+ import { createViewerAdapter } from './adapters/viewer-adapter.js';
32
+ import { createMutateAdapter } from './adapters/mutate-adapter.js';
33
+ import { createSpatialAdapter } from './adapters/spatial-adapter.js';
34
+ import { createLensAdapter } from './adapters/lens-adapter.js';
35
+ import { createExportAdapter } from './adapters/export-adapter.js';
36
+
37
+ export class LocalBackend implements BimBackend {
38
+ readonly model: ModelBackendMethods;
39
+ readonly query: QueryBackendMethods;
40
+ readonly selection: SelectionBackendMethods;
41
+ readonly visibility: VisibilityBackendMethods;
42
+ readonly viewer: ViewerBackendMethods;
43
+ readonly mutate: MutateBackendMethods;
44
+ readonly spatial: SpatialBackendMethods;
45
+ readonly export: ExportBackendMethods;
46
+ readonly lens: LensBackendMethods;
47
+
48
+ private store: StoreApi;
49
+
50
+ constructor(store: StoreApi) {
51
+ this.store = store;
52
+ this.model = createModelAdapter(store);
53
+ this.query = createQueryAdapter(store);
54
+ this.selection = createSelectionAdapter(store);
55
+ this.visibility = createVisibilityAdapter(store);
56
+ this.viewer = createViewerAdapter(store);
57
+ this.mutate = createMutateAdapter(store);
58
+ this.spatial = createSpatialAdapter(store);
59
+ this.lens = createLensAdapter(store);
60
+ this.export = createExportAdapter(store);
61
+ }
62
+
63
+ subscribe(event: BimEventType, handler: (data: unknown) => void): () => void {
64
+ switch (event) {
65
+ case 'selection:changed':
66
+ return this.store.subscribe((state, prev) => {
67
+ if (state.selectedEntities !== prev.selectedEntities) {
68
+ handler({ refs: state.selectedEntities ?? [] });
69
+ }
70
+ });
71
+
72
+ case 'model:loaded':
73
+ return this.store.subscribe((state, prev) => {
74
+ if (state.models.size > prev.models.size) {
75
+ for (const [id, model] of state.models) {
76
+ if (!prev.models.has(id)) {
77
+ handler({
78
+ model: {
79
+ id: model.id,
80
+ name: model.name,
81
+ schemaVersion: model.schemaVersion,
82
+ entityCount: model.ifcDataStore?.entities?.count ?? 0,
83
+ fileSize: model.fileSize,
84
+ loadedAt: model.loadedAt,
85
+ },
86
+ });
87
+ }
88
+ }
89
+ }
90
+ if (state.ifcDataStore && !prev.ifcDataStore && state.models.size === 0) {
91
+ handler({
92
+ model: {
93
+ id: LEGACY_MODEL_ID,
94
+ name: 'Model',
95
+ schemaVersion: state.ifcDataStore.schemaVersion ?? 'IFC4',
96
+ entityCount: state.ifcDataStore.entities?.count ?? 0,
97
+ fileSize: state.ifcDataStore.source?.byteLength ?? 0,
98
+ loadedAt: 0,
99
+ },
100
+ });
101
+ }
102
+ });
103
+
104
+ case 'model:removed':
105
+ return this.store.subscribe((state, prev) => {
106
+ if (state.models.size < prev.models.size) {
107
+ for (const id of prev.models.keys()) {
108
+ if (!state.models.has(id)) {
109
+ handler({ modelId: id });
110
+ }
111
+ }
112
+ }
113
+ });
114
+
115
+ case 'visibility:changed':
116
+ return this.store.subscribe((state, prev) => {
117
+ if (
118
+ state.hiddenEntities !== prev.hiddenEntities ||
119
+ state.isolatedEntities !== prev.isolatedEntities ||
120
+ state.hiddenEntitiesByModel !== prev.hiddenEntitiesByModel
121
+ ) {
122
+ handler({});
123
+ }
124
+ });
125
+
126
+ case 'mutation:changed':
127
+ return this.store.subscribe((state, prev) => {
128
+ if (state.mutationVersion !== prev.mutationVersion) {
129
+ handler({});
130
+ }
131
+ });
132
+
133
+ case 'lens:changed':
134
+ return this.store.subscribe((state, prev) => {
135
+ if (state.activeLensId !== prev.activeLensId) {
136
+ handler({ lensId: state.activeLensId });
137
+ }
138
+ });
139
+
140
+ default:
141
+ return () => {};
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,69 @@
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
+ * useBimHost — React hook that initializes the SDK and BimHost.
7
+ *
8
+ * This hook:
9
+ * 1. Creates a LocalBackend backed by the Zustand store
10
+ * 2. Creates a BimContext (the `bim` object)
11
+ * 3. Starts a BimHost listening on BroadcastChannel 'ifc-lite'
12
+ * 4. External tools (ifc-scripts, ifc-flow) can connect to control the viewer
13
+ *
14
+ * Usage:
15
+ * function App() {
16
+ * const bim = useBimHost();
17
+ * // bim is available for internal use
18
+ * // External tools can connect via BroadcastChannel 'ifc-lite'
19
+ * }
20
+ */
21
+
22
+ import { useRef, useEffect, useMemo } from 'react';
23
+ import { createBimContext, BimHost, type BimContext } from '@ifc-lite/sdk';
24
+ import { useViewerStore } from '../store/index.js';
25
+ import { LocalBackend } from './local-backend.js';
26
+
27
+ const BROADCAST_CHANNEL = 'ifc-lite';
28
+
29
+ /**
30
+ * Initialize the SDK with a local backend and start the BimHost.
31
+ * Returns the BimContext for internal use.
32
+ */
33
+ export function useBimHost(): BimContext {
34
+ const hostRef = useRef<BimHost | null>(null);
35
+ const backendRef = useRef<LocalBackend | null>(null);
36
+
37
+ // Create local backend and BimContext once — single shared backend
38
+ const bim = useMemo(() => {
39
+ const storeApi = {
40
+ getState: useViewerStore.getState,
41
+ subscribe: useViewerStore.subscribe,
42
+ };
43
+ const backend = new LocalBackend(storeApi);
44
+ backendRef.current = backend;
45
+ return createBimContext({ backend });
46
+ }, []);
47
+
48
+ // Start BimHost for external connections — reuse the same backend
49
+ useEffect(() => {
50
+ const backend = backendRef.current;
51
+ if (!backend) return;
52
+ const host = new BimHost(backend);
53
+
54
+ try {
55
+ host.listenBroadcast(BROADCAST_CHANNEL);
56
+ } catch {
57
+ // BroadcastChannel not available (e.g., in some test environments)
58
+ }
59
+
60
+ hostRef.current = host;
61
+
62
+ return () => {
63
+ host.close();
64
+ hostRef.current = null;
65
+ };
66
+ }, []);
67
+
68
+ return bim;
69
+ }
@@ -51,13 +51,41 @@ export const EDGE_LOCK_DEFAULTS = {
51
51
  // UI Defaults
52
52
  // ============================================================================
53
53
 
54
+ /** Resolve the initial theme: localStorage override > system preference > dark fallback */
55
+ function getInitialTheme(): 'light' | 'dark' {
56
+ if (typeof window === 'undefined') return 'dark';
57
+ const saved = localStorage.getItem('ifc-lite-theme');
58
+ if (saved === 'light' || saved === 'dark') return saved;
59
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
60
+ }
61
+
54
62
  export const UI_DEFAULTS = {
55
63
  /** Default active tool */
56
64
  ACTIVE_TOOL: 'select',
57
- /** Default theme */
58
- THEME: 'dark' as const,
65
+ /** Default theme – respects user's OS colour-scheme preference */
66
+ THEME: getInitialTheme(),
59
67
  /** Default hover tooltips state */
60
68
  HOVER_TOOLTIPS_ENABLED: false,
69
+ /** Global visual enhancement kill switch */
70
+ VISUAL_ENHANCEMENTS_ENABLED: true,
71
+ /** Edge contrast enhancement default */
72
+ EDGE_CONTRAST_ENABLED: true,
73
+ /** Edge contrast intensity */
74
+ EDGE_CONTRAST_INTENSITY: 1.2,
75
+ /** Contact shading quality preset */
76
+ CONTACT_SHADING_QUALITY: 'low' as const,
77
+ /** Contact shading intensity */
78
+ CONTACT_SHADING_INTENSITY: 0.35,
79
+ /** Contact shading radius in pixels */
80
+ CONTACT_SHADING_RADIUS: 1.5,
81
+ /** Separation-line overlay default */
82
+ SEPARATION_LINES_ENABLED: true,
83
+ /** Separation-line quality preset */
84
+ SEPARATION_LINES_QUALITY: 'low' as const,
85
+ /** Separation-line intensity */
86
+ SEPARATION_LINES_INTENSITY: 0.38,
87
+ /** Separation-line radius in pixels */
88
+ SEPARATION_LINES_RADIUS: 1.0,
61
89
  } as const;
62
90
 
63
91
  // ============================================================================
@@ -30,6 +30,7 @@ import { createIdsSlice, type IDSSlice } from './slices/idsSlice.js';
30
30
  import { createListSlice, type ListSlice } from './slices/listSlice.js';
31
31
  import { createPinboardSlice, type PinboardSlice } from './slices/pinboardSlice.js';
32
32
  import { createLensSlice, type LensSlice } from './slices/lensSlice.js';
33
+ import { createScriptSlice, type ScriptSlice } from './slices/scriptSlice.js';
33
34
 
34
35
  // Import constants for reset function
35
36
  import { CAMERA_DEFAULTS, SECTION_PLANE_DEFAULTS, UI_DEFAULTS, TYPE_VISIBILITY_DEFAULTS } from './constants.js';
@@ -67,6 +68,9 @@ export type { PinboardSlice } from './slices/pinboardSlice.js';
67
68
  // Re-export Lens types
68
69
  export type { LensSlice, Lens, LensRule, LensCriteria } from './slices/lensSlice.js';
69
70
 
71
+ // Re-export Script types
72
+ export type { ScriptSlice } from './slices/scriptSlice.js';
73
+
70
74
  // Combined store type
71
75
  export type ViewerState = LoadingSlice &
72
76
  SelectionSlice &
@@ -85,7 +89,8 @@ export type ViewerState = LoadingSlice &
85
89
  IDSSlice &
86
90
  ListSlice &
87
91
  PinboardSlice &
88
- LensSlice & {
92
+ LensSlice &
93
+ ScriptSlice & {
89
94
  resetViewerState: () => void;
90
95
  };
91
96
 
@@ -112,6 +117,7 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
112
117
  ...createListSlice(...args),
113
118
  ...createPinboardSlice(...args),
114
119
  ...createLensSlice(...args),
120
+ ...createScriptSlice(...args),
115
121
 
116
122
  // Reset all viewer state when loading new file
117
123
  // Note: Does NOT clear models - use clearAllModels() for that
@@ -178,6 +184,16 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
178
184
 
179
185
  // UI
180
186
  activeTool: UI_DEFAULTS.ACTIVE_TOOL,
187
+ visualEnhancementsEnabled: UI_DEFAULTS.VISUAL_ENHANCEMENTS_ENABLED,
188
+ edgeContrastEnabled: UI_DEFAULTS.EDGE_CONTRAST_ENABLED,
189
+ edgeContrastIntensity: UI_DEFAULTS.EDGE_CONTRAST_INTENSITY,
190
+ contactShadingQuality: UI_DEFAULTS.CONTACT_SHADING_QUALITY,
191
+ contactShadingIntensity: UI_DEFAULTS.CONTACT_SHADING_INTENSITY,
192
+ contactShadingRadius: UI_DEFAULTS.CONTACT_SHADING_RADIUS,
193
+ separationLinesEnabled: UI_DEFAULTS.SEPARATION_LINES_ENABLED,
194
+ separationLinesQuality: UI_DEFAULTS.SEPARATION_LINES_QUALITY,
195
+ separationLinesIntensity: UI_DEFAULTS.SEPARATION_LINES_INTENSITY,
196
+ separationLinesRadius: UI_DEFAULTS.SEPARATION_LINES_RADIUS,
181
197
 
182
198
  // Drawing 2D
183
199
  drawing2D: null,
@@ -251,6 +267,13 @@ export const useViewerStore = create<ViewerState>()((...args) => ({
251
267
  // Pinboard - clear pinned entities on new file
252
268
  pinboardEntities: new Set<string>(),
253
269
 
270
+ // Script - reset execution state but keep saved scripts and editor content
271
+ scriptPanelVisible: false,
272
+ scriptExecutionState: 'idle' as const,
273
+ scriptLastResult: null,
274
+ scriptLastError: null,
275
+ scriptDeleteConfirmId: null,
276
+
254
277
  // Lens - deactivate but keep saved lenses
255
278
  activeLensId: null,
256
279
  lensPanelVisible: false,
@@ -18,11 +18,11 @@ import type { StateCreator } from 'zustand';
18
18
  import type { EntityRef } from '../types.js';
19
19
  import { entityRefToString, stringToEntityRef } from '../types.js';
20
20
 
21
- /** Minimal interface for accessing isolation + models from the combined store */
22
- interface CombinedStoreAccess {
23
- isolatedEntities?: Set<number> | null;
24
- hiddenEntities?: Set<number>;
25
- 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 }>;
26
26
  }
27
27
 
28
28
  export interface PinboardSlice {
@@ -62,29 +62,30 @@ export interface PinboardSlice {
62
62
  /** Convert basket EntityRefs to global IDs using model offsets */
63
63
  function basketToGlobalIds(
64
64
  basketEntities: Set<string>,
65
- models?: Map<string, { idOffset: number }>,
65
+ models: Map<string, { idOffset: number }>,
66
66
  ): Set<number> {
67
67
  const globalIds = new Set<number>();
68
68
  for (const str of basketEntities) {
69
69
  const ref = stringToEntityRef(str);
70
- if (models) {
71
- const model = models.get(ref.modelId);
72
- const offset = model?.idOffset ?? 0;
73
- globalIds.add(ref.expressId + offset);
74
- } else {
75
- globalIds.add(ref.expressId);
76
- }
70
+ const model = models.get(ref.modelId);
71
+ const offset = model?.idOffset ?? 0;
72
+ globalIds.add(ref.expressId + offset);
77
73
  }
78
74
  return globalIds;
79
75
  }
80
76
 
81
77
  /** Compute a single EntityRef's global ID */
82
- function refToGlobalId(ref: EntityRef, models?: Map<string, { idOffset: number }>): number {
83
- const model = models?.get(ref.modelId);
78
+ function refToGlobalId(ref: EntityRef, models: Map<string, { idOffset: number }>): number {
79
+ const model = models.get(ref.modelId);
84
80
  return ref.expressId + (model?.idOffset ?? 0);
85
81
  }
86
82
 
87
- export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSlice> = (set, get) => ({
83
+ export const createPinboardSlice: StateCreator<
84
+ PinboardSlice & PinboardCrossSliceState,
85
+ [],
86
+ [],
87
+ PinboardSlice
88
+ > = (set, get) => ({
88
89
  // Initial state
89
90
  pinboardEntities: new Set(),
90
91
 
@@ -95,12 +96,11 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
95
96
  for (const ref of refs) {
96
97
  next.add(entityRefToString(ref));
97
98
  }
98
- const store = state as unknown as CombinedStoreAccess;
99
- const isolatedEntities = basketToGlobalIds(next, store.models);
100
- const hiddenEntities = new Set<number>(store.hiddenEntities ?? []);
99
+ const isolatedEntities = basketToGlobalIds(next, state.models);
100
+ const hiddenEntities = new Set<number>(state.hiddenEntities);
101
101
  // Unhide any entities being added to basket
102
102
  for (const ref of refs) {
103
- const model = store.models?.get(ref.modelId);
103
+ const model = state.models.get(ref.modelId);
104
104
  const offset = model?.idOffset ?? 0;
105
105
  hiddenEntities.delete(ref.expressId + offset);
106
106
  }
@@ -117,8 +117,7 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
117
117
  if (next.size === 0) {
118
118
  return { pinboardEntities: next, isolatedEntities: null };
119
119
  }
120
- const store = state as unknown as CombinedStoreAccess;
121
- const isolatedEntities = basketToGlobalIds(next, store.models);
120
+ const isolatedEntities = basketToGlobalIds(next, state.models);
122
121
  return { pinboardEntities: next, isolatedEntities };
123
122
  });
124
123
  },
@@ -132,15 +131,15 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
132
131
  set({ pinboardEntities: next, isolatedEntities: null });
133
132
  return;
134
133
  }
135
- const store = get() as unknown as CombinedStoreAccess;
136
- const hiddenEntities = new Set<number>(store.hiddenEntities ?? []);
134
+ const s = get();
135
+ const hiddenEntities = new Set<number>(s.hiddenEntities);
137
136
  // Unhide basket entities
138
137
  for (const ref of refs) {
139
- const model = store.models?.get(ref.modelId);
138
+ const model = s.models.get(ref.modelId);
140
139
  const offset = model?.idOffset ?? 0;
141
140
  hiddenEntities.delete(ref.expressId + offset);
142
141
  }
143
- const isolatedEntities = basketToGlobalIds(next, store.models);
142
+ const isolatedEntities = basketToGlobalIds(next, s.models);
144
143
  set({ pinboardEntities: next, isolatedEntities, hiddenEntities });
145
144
  },
146
145
 
@@ -149,8 +148,7 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
149
148
  showPinboard: () => {
150
149
  const state = get();
151
150
  if (state.pinboardEntities.size === 0) return;
152
- const store = state as unknown as CombinedStoreAccess;
153
- const isolatedEntities = basketToGlobalIds(state.pinboardEntities, store.models);
151
+ const isolatedEntities = basketToGlobalIds(state.pinboardEntities, state.models);
154
152
  set({ isolatedEntities });
155
153
  },
156
154
 
@@ -181,15 +179,15 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
181
179
  for (const ref of refs) {
182
180
  next.add(entityRefToString(ref));
183
181
  }
184
- const store = get() as unknown as CombinedStoreAccess;
185
- const hiddenEntities = new Set<number>(store.hiddenEntities ?? []);
182
+ const s = get();
183
+ const hiddenEntities = new Set<number>(s.hiddenEntities);
186
184
  // Unhide basket entities
187
185
  for (const ref of refs) {
188
- const model = store.models?.get(ref.modelId);
186
+ const model = s.models.get(ref.modelId);
189
187
  const offset = model?.idOffset ?? 0;
190
188
  hiddenEntities.delete(ref.expressId + offset);
191
189
  }
192
- const isolatedEntities = basketToGlobalIds(next, store.models);
190
+ const isolatedEntities = basketToGlobalIds(next, s.models);
193
191
  set({ pinboardEntities: next, isolatedEntities, hiddenEntities });
194
192
  },
195
193
 
@@ -201,13 +199,12 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
201
199
  for (const ref of refs) {
202
200
  next.add(entityRefToString(ref));
203
201
  }
204
- const store = state as unknown as CombinedStoreAccess;
205
- const hiddenEntities = new Set<number>(store.hiddenEntities ?? []);
202
+ const hiddenEntities = new Set<number>(state.hiddenEntities);
206
203
  // Incrementally add new globalIds to existing isolation set instead of re-parsing all
207
- const prevIsolated = store.isolatedEntities;
208
- const isolatedEntities = prevIsolated ? new Set<number>(prevIsolated) : basketToGlobalIds(state.pinboardEntities, store.models);
204
+ const prevIsolated = state.isolatedEntities;
205
+ const isolatedEntities = prevIsolated ? new Set<number>(prevIsolated) : basketToGlobalIds(state.pinboardEntities, state.models);
209
206
  for (const ref of refs) {
210
- const gid = refToGlobalId(ref, store.models);
207
+ const gid = refToGlobalId(ref, state.models);
211
208
  isolatedEntities.add(gid);
212
209
  hiddenEntities.delete(gid);
213
210
  }
@@ -226,18 +223,17 @@ export const createPinboardSlice: StateCreator<PinboardSlice, [], [], PinboardSl
226
223
  if (next.size === 0) {
227
224
  return { pinboardEntities: next, isolatedEntities: null };
228
225
  }
229
- const store = state as unknown as CombinedStoreAccess;
230
226
  // Incrementally remove globalIds from existing isolation set instead of re-parsing all
231
- const prevIsolated = store.isolatedEntities;
227
+ const prevIsolated = state.isolatedEntities;
232
228
  if (prevIsolated) {
233
229
  const isolatedEntities = new Set<number>(prevIsolated);
234
230
  for (const ref of refs) {
235
- isolatedEntities.delete(refToGlobalId(ref, store.models));
231
+ isolatedEntities.delete(refToGlobalId(ref, state.models));
236
232
  }
237
233
  return { pinboardEntities: next, isolatedEntities };
238
234
  }
239
235
  // Fallback: full recompute if no existing isolation set
240
- const isolatedEntities = basketToGlobalIds(next, store.models);
236
+ const isolatedEntities = basketToGlobalIds(next, state.models);
241
237
  return { pinboardEntities: next, isolatedEntities };
242
238
  });
243
239
  },