@ifc-lite/viewer 1.1.6 → 1.5.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 (164) hide show
  1. package/LICENSE +373 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
  4. package/dist/assets/arrow2-bb-jcVEo.js +2 -0
  5. package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
  6. package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
  7. package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
  8. package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
  9. package/dist/assets/event-DIOks52T.js +1 -0
  10. package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
  11. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  12. package/dist/assets/index-Dgd6vzw_.js +65252 -0
  13. package/dist/assets/index-v3mcCUPN.css +1 -0
  14. package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
  15. package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
  16. package/dist/favicon-16x16-cropped.png +0 -0
  17. package/dist/favicon-16x16.png +0 -0
  18. package/dist/favicon-192x192-cropped.png +0 -0
  19. package/dist/favicon-192x192.png +0 -0
  20. package/dist/favicon-32x32-cropped.png +0 -0
  21. package/dist/favicon-32x32.png +0 -0
  22. package/dist/favicon-48x48-cropped.png +0 -0
  23. package/dist/favicon-48x48.png +0 -0
  24. package/dist/favicon-512x512-cropped.png +0 -0
  25. package/dist/favicon-512x512.png +0 -0
  26. package/dist/favicon-64x64-cropped.png +0 -0
  27. package/dist/favicon-64x64.png +0 -0
  28. package/dist/favicon-96x96-cropped.png +0 -0
  29. package/dist/favicon-96x96.png +0 -0
  30. package/dist/favicon-square-512.png +0 -0
  31. package/dist/favicon.ico +0 -0
  32. package/dist/favicon.png +0 -0
  33. package/dist/favicon.svg +3 -0
  34. package/dist/index.html +44 -0
  35. package/dist/logo.png +0 -0
  36. package/dist/manifest.json +48 -0
  37. package/index.html +33 -2
  38. package/package.json +34 -17
  39. package/public/apple-touch-icon.png +0 -0
  40. package/public/favicon-16x16-cropped.png +0 -0
  41. package/public/favicon-16x16.png +0 -0
  42. package/public/favicon-192x192-cropped.png +0 -0
  43. package/public/favicon-192x192.png +0 -0
  44. package/public/favicon-32x32-cropped.png +0 -0
  45. package/public/favicon-32x32.png +0 -0
  46. package/public/favicon-48x48-cropped.png +0 -0
  47. package/public/favicon-48x48.png +0 -0
  48. package/public/favicon-512x512-cropped.png +0 -0
  49. package/public/favicon-512x512.png +0 -0
  50. package/public/favicon-64x64-cropped.png +0 -0
  51. package/public/favicon-64x64.png +0 -0
  52. package/public/favicon-96x96-cropped.png +0 -0
  53. package/public/favicon-96x96.png +0 -0
  54. package/public/favicon-square-512.png +0 -0
  55. package/public/favicon.ico +0 -0
  56. package/public/favicon.png +0 -0
  57. package/public/favicon.svg +3 -0
  58. package/public/logo.png +0 -0
  59. package/public/manifest.json +48 -0
  60. package/src/App.tsx +2 -0
  61. package/src/components/ui/alert.tsx +62 -0
  62. package/src/components/ui/badge.tsx +39 -0
  63. package/src/components/ui/dialog.tsx +120 -0
  64. package/src/components/ui/label.tsx +27 -0
  65. package/src/components/ui/select.tsx +151 -0
  66. package/src/components/ui/switch.tsx +30 -0
  67. package/src/components/ui/table.tsx +120 -0
  68. package/src/components/ui/tabs.tsx +1 -1
  69. package/src/components/viewer/BCFPanel.tsx +1164 -0
  70. package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
  71. package/src/components/viewer/DataConnector.tsx +840 -0
  72. package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
  73. package/src/components/viewer/EntityContextMenu.tsx +45 -17
  74. package/src/components/viewer/ExportChangesButton.tsx +195 -0
  75. package/src/components/viewer/ExportDialog.tsx +402 -0
  76. package/src/components/viewer/HierarchyPanel.tsx +1132 -218
  77. package/src/components/viewer/IDSPanel.tsx +661 -0
  78. package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
  79. package/src/components/viewer/MainToolbar.tsx +418 -94
  80. package/src/components/viewer/PropertiesPanel.tsx +1355 -91
  81. package/src/components/viewer/PropertyEditor.tsx +611 -0
  82. package/src/components/viewer/Section2DPanel.tsx +3313 -0
  83. package/src/components/viewer/SheetSetupPanel.tsx +502 -0
  84. package/src/components/viewer/StatusBar.tsx +27 -16
  85. package/src/components/viewer/TitleBlockEditor.tsx +437 -0
  86. package/src/components/viewer/ToolOverlays.tsx +935 -127
  87. package/src/components/viewer/ViewerLayout.tsx +40 -11
  88. package/src/components/viewer/Viewport.tsx +1276 -336
  89. package/src/components/viewer/ViewportContainer.tsx +554 -18
  90. package/src/components/viewer/ViewportOverlays.tsx +24 -7
  91. package/src/hooks/useBCF.ts +504 -0
  92. package/src/hooks/useIDS.ts +1065 -0
  93. package/src/hooks/useIfc.ts +1534 -205
  94. package/src/hooks/useIfcCache.ts +279 -0
  95. package/src/hooks/useKeyboardShortcuts.ts +50 -8
  96. package/src/hooks/useModelSelection.ts +61 -0
  97. package/src/hooks/useViewerSelectors.ts +218 -0
  98. package/src/hooks/useWebGPU.ts +80 -0
  99. package/src/index.css +265 -27
  100. package/src/lib/platform.ts +23 -0
  101. package/src/services/cacheService.ts +142 -0
  102. package/src/services/desktop-cache.ts +143 -0
  103. package/src/services/fs-cache.ts +212 -0
  104. package/src/services/ifc-cache.ts +14 -6
  105. package/src/store/constants.ts +85 -0
  106. package/src/store/index.ts +214 -0
  107. package/src/store/slices/bcfSlice.ts +372 -0
  108. package/src/store/slices/cameraSlice.ts +63 -0
  109. package/src/store/slices/dataSlice.test.ts +226 -0
  110. package/src/store/slices/dataSlice.ts +112 -0
  111. package/src/store/slices/drawing2DSlice.ts +340 -0
  112. package/src/store/slices/hoverSlice.ts +40 -0
  113. package/src/store/slices/idsSlice.ts +310 -0
  114. package/src/store/slices/loadingSlice.ts +33 -0
  115. package/src/store/slices/measurementSlice.test.ts +217 -0
  116. package/src/store/slices/measurementSlice.ts +293 -0
  117. package/src/store/slices/modelSlice.test.ts +271 -0
  118. package/src/store/slices/modelSlice.ts +211 -0
  119. package/src/store/slices/mutationSlice.ts +502 -0
  120. package/src/store/slices/sectionSlice.test.ts +125 -0
  121. package/src/store/slices/sectionSlice.ts +58 -0
  122. package/src/store/slices/selectionSlice.test.ts +286 -0
  123. package/src/store/slices/selectionSlice.ts +263 -0
  124. package/src/store/slices/sheetSlice.ts +565 -0
  125. package/src/store/slices/uiSlice.ts +58 -0
  126. package/src/store/slices/visibilitySlice.test.ts +304 -0
  127. package/src/store/slices/visibilitySlice.ts +277 -0
  128. package/src/store/types.test.ts +135 -0
  129. package/src/store/types.ts +248 -0
  130. package/src/store.ts +40 -515
  131. package/src/utils/ifcConfig.ts +82 -0
  132. package/src/utils/localParsingUtils.ts +287 -0
  133. package/src/utils/serverDataModel.ts +783 -0
  134. package/src/utils/spatialHierarchy.ts +283 -0
  135. package/src/utils/viewportUtils.ts +334 -0
  136. package/src/vite-env.d.ts +23 -0
  137. package/src/webgpu-types.d.ts +128 -0
  138. package/src-tauri/Cargo.toml +29 -0
  139. package/src-tauri/build.rs +7 -0
  140. package/src-tauri/capabilities/default.json +18 -0
  141. package/src-tauri/icons/128x128.png +0 -0
  142. package/src-tauri/icons/128x128@2x.png +0 -0
  143. package/src-tauri/icons/32x32.png +0 -0
  144. package/src-tauri/icons/Square107x107Logo.png +0 -0
  145. package/src-tauri/icons/Square142x142Logo.png +0 -0
  146. package/src-tauri/icons/Square150x150Logo.png +0 -0
  147. package/src-tauri/icons/Square284x284Logo.png +0 -0
  148. package/src-tauri/icons/Square30x30Logo.png +0 -0
  149. package/src-tauri/icons/Square310x310Logo.png +0 -0
  150. package/src-tauri/icons/Square44x44Logo.png +0 -0
  151. package/src-tauri/icons/Square71x71Logo.png +0 -0
  152. package/src-tauri/icons/Square89x89Logo.png +0 -0
  153. package/src-tauri/icons/StoreLogo.png +0 -0
  154. package/src-tauri/icons/icon.icns +0 -0
  155. package/src-tauri/icons/icon.ico +0 -0
  156. package/src-tauri/icons/icon.png +0 -0
  157. package/src-tauri/src/lib.rs +21 -0
  158. package/src-tauri/src/main.rs +10 -0
  159. package/src-tauri/tauri.conf.json +39 -0
  160. package/vite.config.ts +174 -26
  161. package/public/ifc-lite_bg.wasm +0 -0
  162. package/public/web-ifc.wasm +0 -0
  163. package/src/components/Viewport.tsx +0 -723
  164. package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
package/src/store.ts CHANGED
@@ -3,520 +3,45 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
5
  /**
6
- * Zustand store for viewer state
6
+ * Re-export from modular store for backward compatibility
7
+ *
8
+ * The store has been refactored into domain-specific slices:
9
+ * - loadingSlice: Loading, progress, error state
10
+ * - selectionSlice: Entity and storey selection
11
+ * - visibilitySlice: Hidden/isolated entities, type visibility
12
+ * - uiSlice: Panel state, theme, mobile detection
13
+ * - hoverSlice: Hover and context menu state
14
+ * - cameraSlice: Camera rotation and callbacks
15
+ * - sectionSlice: Section plane state
16
+ * - measurementSlice: Measurements, snapping, edge lock
17
+ * - dataSlice: IFC data and geometry
18
+ *
19
+ * See apps/viewer/src/store/ for the modular implementation.
7
20
  */
8
21
 
9
- import { create } from 'zustand';
10
- import type { IfcDataStore } from '@ifc-lite/parser';
11
- import type { GeometryResult, CoordinateInfo } from '@ifc-lite/geometry';
12
-
13
- // Measurement types
14
- export interface MeasurePoint {
15
- x: number;
16
- y: number;
17
- z: number;
18
- screenX: number;
19
- screenY: number;
20
- }
21
-
22
- export interface Measurement {
23
- id: string;
24
- start: MeasurePoint;
25
- end: MeasurePoint;
26
- distance: number;
27
- }
28
-
29
- // Section plane types
30
- export interface SectionPlane {
31
- axis: 'x' | 'y' | 'z';
32
- position: number; // 0-100 percentage of model bounds
33
- enabled: boolean;
34
- }
35
-
36
- // Hover state
37
- export interface HoverState {
38
- entityId: number | null;
39
- screenX: number;
40
- screenY: number;
41
- }
42
-
43
- // Context menu state
44
- export interface ContextMenuState {
45
- isOpen: boolean;
46
- entityId: number | null;
47
- screenX: number;
48
- screenY: number;
49
- }
50
-
51
- // Box selection state
52
- export interface BoxSelectState {
53
- isSelecting: boolean;
54
- startX: number;
55
- startY: number;
56
- currentX: number;
57
- currentY: number;
58
- }
59
-
60
- interface ViewerState {
61
- // Loading state
62
- loading: boolean;
63
- progress: { phase: string; percent: number } | null;
64
- error: string | null;
65
-
66
- // Data
67
- ifcDataStore: IfcDataStore | null;
68
- geometryResult: GeometryResult | null;
69
-
70
- // Selection
71
- selectedEntityId: number | null;
72
- selectedEntityIds: Set<number>; // Multi-selection support
73
- selectedStorey: number | null;
74
-
75
- // Visibility
76
- hiddenEntities: Set<number>;
77
- isolatedEntities: Set<number> | null; // null = show all, Set = only show these
78
-
79
- // UI State
80
- leftPanelCollapsed: boolean;
81
- rightPanelCollapsed: boolean;
82
- activeTool: string;
83
- theme: 'light' | 'dark';
84
- isMobile: boolean;
85
- hoverTooltipsEnabled: boolean;
86
-
87
- // Hover state
88
- hoverState: HoverState;
89
-
90
- // Context menu state
91
- contextMenu: ContextMenuState;
92
-
93
- // Box selection state
94
- boxSelect: BoxSelectState;
95
-
96
- // Measurement state
97
- measurements: Measurement[];
98
- pendingMeasurePoint: MeasurePoint | null;
99
-
100
- // Section plane state
101
- sectionPlane: SectionPlane;
102
-
103
- // Camera state (for ViewCube sync)
104
- cameraRotation: { azimuth: number; elevation: number };
105
- cameraCallbacks: {
106
- setPresetView?: (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right') => void;
107
- fitAll?: () => void;
108
- home?: () => void; // Reset to isometric view
109
- zoomIn?: () => void;
110
- zoomOut?: () => void;
111
- frameSelection?: () => void; // Center view on selected element (F key)
112
- orbit?: (deltaX: number, deltaY: number) => void; // Orbit camera by delta
113
- };
114
- // Direct callback for real-time ViewCube updates (bypasses React state)
115
- onCameraRotationChange: ((rotation: { azimuth: number; elevation: number }) => void) | null;
116
- // Direct callback for real-time scale bar updates (bypasses React state)
117
- onScaleChange: ((scale: number) => void) | null;
118
-
119
- // Actions
120
- setLoading: (loading: boolean) => void;
121
- setProgress: (progress: { phase: string; percent: number } | null) => void;
122
- setError: (error: string | null) => void;
123
- setIfcDataStore: (result: IfcDataStore | null) => void;
124
- setGeometryResult: (result: GeometryResult | null) => void;
125
- appendGeometryBatch: (meshes: GeometryResult['meshes'], coordinateInfo?: CoordinateInfo) => void;
126
- updateCoordinateInfo: (coordinateInfo: CoordinateInfo) => void;
127
- setSelectedEntityId: (id: number | null) => void;
128
- setSelectedStorey: (id: number | null) => void;
129
- setLeftPanelCollapsed: (collapsed: boolean) => void;
130
- setRightPanelCollapsed: (collapsed: boolean) => void;
131
- setActiveTool: (tool: string) => void;
132
- setTheme: (theme: 'light' | 'dark') => void;
133
- toggleTheme: () => void;
134
- setIsMobile: (isMobile: boolean) => void;
135
- toggleHoverTooltips: () => void;
136
-
137
- // Camera actions
138
- setCameraRotation: (rotation: { azimuth: number; elevation: number }) => void;
139
- setCameraCallbacks: (callbacks: ViewerState['cameraCallbacks']) => void;
140
- setOnCameraRotationChange: (callback: ((rotation: { azimuth: number; elevation: number }) => void) | null) => void;
141
- // Call this for real-time updates (uses callback if available, skips state)
142
- updateCameraRotationRealtime: (rotation: { azimuth: number; elevation: number }) => void;
143
- setOnScaleChange: (callback: ((scale: number) => void) | null) => void;
144
- updateScaleRealtime: (scale: number) => void;
145
-
146
- // Visibility actions
147
- hideEntity: (id: number) => void;
148
- hideEntities: (ids: number[]) => void;
149
- showEntity: (id: number) => void;
150
- showEntities: (ids: number[]) => void;
151
- toggleEntityVisibility: (id: number) => void;
152
- isolateEntity: (id: number) => void;
153
- isolateEntities: (ids: number[]) => void;
154
- clearIsolation: () => void;
155
- showAll: () => void;
156
- isEntityVisible: (id: number) => boolean;
157
-
158
- // Multi-selection actions
159
- addToSelection: (id: number) => void;
160
- removeFromSelection: (id: number) => void;
161
- toggleSelection: (id: number) => void;
162
- setSelectedEntityIds: (ids: number[]) => void;
163
- clearSelection: () => void;
164
-
165
- // Hover actions
166
- setHoverState: (state: HoverState) => void;
167
- clearHover: () => void;
168
-
169
- // Context menu actions
170
- openContextMenu: (entityId: number | null, screenX: number, screenY: number) => void;
171
- closeContextMenu: () => void;
172
-
173
- // Box selection actions
174
- startBoxSelect: (startX: number, startY: number) => void;
175
- updateBoxSelect: (currentX: number, currentY: number) => void;
176
- endBoxSelect: () => void;
177
- cancelBoxSelect: () => void;
178
-
179
- // Measurement actions
180
- addMeasurePoint: (point: MeasurePoint) => void;
181
- completeMeasurement: (endPoint: MeasurePoint) => void;
182
- deleteMeasurement: (id: string) => void;
183
- clearMeasurements: () => void;
184
-
185
- // Section plane actions
186
- setSectionPlaneAxis: (axis: 'x' | 'y' | 'z') => void;
187
- setSectionPlanePosition: (position: number) => void;
188
- toggleSectionPlane: () => void;
189
- flipSectionPlane: () => void;
190
- resetSectionPlane: () => void;
191
-
192
- // Reset all viewer state (called when loading new file)
193
- resetViewerState: () => void;
194
- }
195
-
196
- export const useViewerStore = create<ViewerState>((set, get) => ({
197
- loading: false,
198
- progress: null,
199
- error: null,
200
- ifcDataStore: null,
201
- geometryResult: null,
202
- selectedEntityId: null,
203
- selectedEntityIds: new Set(),
204
- selectedStorey: null,
205
- hiddenEntities: new Set(),
206
- isolatedEntities: null,
207
- leftPanelCollapsed: false,
208
- rightPanelCollapsed: false,
209
- activeTool: 'select',
210
- theme: 'dark',
211
- isMobile: false,
212
- hoverTooltipsEnabled: false,
213
- hoverState: { entityId: null, screenX: 0, screenY: 0 },
214
- contextMenu: { isOpen: false, entityId: null, screenX: 0, screenY: 0 },
215
- boxSelect: { isSelecting: false, startX: 0, startY: 0, currentX: 0, currentY: 0 },
216
- measurements: [],
217
- pendingMeasurePoint: null,
218
- sectionPlane: { axis: 'y', position: 50, enabled: false },
219
- cameraRotation: { azimuth: 45, elevation: 25 },
220
- cameraCallbacks: {},
221
- onCameraRotationChange: null,
222
- onScaleChange: null,
223
-
224
- setLoading: (loading) => set({ loading }),
225
- setProgress: (progress) => set({ progress }),
226
- setError: (error) => set({ error }),
227
- setIfcDataStore: (ifcDataStore) => set({ ifcDataStore }),
228
- setGeometryResult: (geometryResult) => set({ geometryResult }),
229
- appendGeometryBatch: (meshes, coordinateInfo) => set((state) => {
230
- if (!state.geometryResult) {
231
- const totalTriangles = meshes.reduce((sum, m) => sum + (m.indices.length / 3), 0);
232
- const totalVertices = meshes.reduce((sum, m) => sum + (m.positions.length / 3), 0);
233
- return {
234
- geometryResult: {
235
- meshes,
236
- totalTriangles,
237
- totalVertices,
238
- coordinateInfo: coordinateInfo || {
239
- originShift: { x: 0, y: 0, z: 0 },
240
- originalBounds: { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } },
241
- shiftedBounds: { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } },
242
- isGeoReferenced: false,
243
- },
244
- },
245
- };
246
- }
247
- const allMeshes = [...state.geometryResult.meshes, ...meshes];
248
- const totalTriangles = allMeshes.reduce((sum, m) => sum + (m.indices.length / 3), 0);
249
- const totalVertices = allMeshes.reduce((sum, m) => sum + (m.positions.length / 3), 0);
250
- return {
251
- geometryResult: {
252
- ...state.geometryResult,
253
- meshes: allMeshes,
254
- totalTriangles,
255
- totalVertices,
256
- coordinateInfo: coordinateInfo || state.geometryResult.coordinateInfo,
257
- },
258
- };
259
- }),
260
- updateCoordinateInfo: (coordinateInfo) => set((state) => {
261
- if (!state.geometryResult) return {};
262
- return {
263
- geometryResult: {
264
- ...state.geometryResult,
265
- coordinateInfo,
266
- },
267
- };
268
- }),
269
- setSelectedEntityId: (selectedEntityId) => set({ selectedEntityId }),
270
- setSelectedStorey: (selectedStorey) => set({ selectedStorey }),
271
- setLeftPanelCollapsed: (leftPanelCollapsed) => set({ leftPanelCollapsed }),
272
- setRightPanelCollapsed: (rightPanelCollapsed) => set({ rightPanelCollapsed }),
273
- setActiveTool: (activeTool) => set({ activeTool }),
274
- setTheme: (theme) => {
275
- document.documentElement.classList.toggle('dark', theme === 'dark');
276
- set({ theme });
277
- },
278
- toggleTheme: () => {
279
- const newTheme = get().theme === 'dark' ? 'light' : 'dark';
280
- document.documentElement.classList.toggle('dark', newTheme === 'dark');
281
- set({ theme: newTheme });
282
- },
283
- setIsMobile: (isMobile) => set({ isMobile }),
284
- toggleHoverTooltips: () => set((state) => ({ hoverTooltipsEnabled: !state.hoverTooltipsEnabled })),
285
-
286
- // Camera actions
287
- setCameraRotation: (cameraRotation) => set({ cameraRotation }),
288
- setCameraCallbacks: (cameraCallbacks) => set({ cameraCallbacks }),
289
- setOnCameraRotationChange: (onCameraRotationChange) => set({ onCameraRotationChange }),
290
- updateCameraRotationRealtime: (rotation) => {
291
- const callback = get().onCameraRotationChange;
292
- if (callback) {
293
- // Use direct callback - no React state update, no re-renders
294
- callback(rotation);
295
- }
296
- // Don't update store state during real-time updates
297
- },
298
- setOnScaleChange: (onScaleChange) => set({ onScaleChange }),
299
- updateScaleRealtime: (scale) => {
300
- const callback = get().onScaleChange;
301
- if (callback) {
302
- // Use direct callback - no React state update, no re-renders
303
- callback(scale);
304
- }
305
- // Don't update store state during real-time updates
306
- },
307
-
308
- // Visibility actions
309
- hideEntity: (id) => set((state) => {
310
- // Toggle hide: if already hidden, show it; otherwise hide it
311
- const newHidden = new Set(state.hiddenEntities);
312
- if (newHidden.has(id)) {
313
- newHidden.delete(id);
314
- } else {
315
- newHidden.add(id);
316
- }
317
- return { hiddenEntities: newHidden };
318
- }),
319
- hideEntities: (ids) => set((state) => {
320
- // Toggle hide for each entity: if already hidden, show it; otherwise hide it
321
- const newHidden = new Set(state.hiddenEntities);
322
- ids.forEach(id => {
323
- if (newHidden.has(id)) {
324
- newHidden.delete(id);
325
- } else {
326
- newHidden.add(id);
327
- }
328
- });
329
- return { hiddenEntities: newHidden };
330
- }),
331
- showEntity: (id) => set((state) => {
332
- const newHidden = new Set(state.hiddenEntities);
333
- newHidden.delete(id);
334
- return { hiddenEntities: newHidden };
335
- }),
336
- showEntities: (ids) => set((state) => {
337
- const newHidden = new Set(state.hiddenEntities);
338
- ids.forEach(id => newHidden.delete(id));
339
- return { hiddenEntities: newHidden };
340
- }),
341
- toggleEntityVisibility: (id) => set((state) => {
342
- const newHidden = new Set(state.hiddenEntities);
343
- if (newHidden.has(id)) {
344
- newHidden.delete(id);
345
- } else {
346
- newHidden.add(id);
347
- }
348
- return { hiddenEntities: newHidden };
349
- }),
350
- isolateEntity: (id) => set((state) => {
351
- // Toggle isolate: if this entity is already the only isolated one, clear isolation
352
- // Otherwise, isolate it (and unhide it for good UX)
353
- const isAlreadyIsolated = state.isolatedEntities !== null &&
354
- state.isolatedEntities.size === 1 &&
355
- state.isolatedEntities.has(id);
356
-
357
- if (isAlreadyIsolated) {
358
- // Toggle off: clear isolation
359
- return { isolatedEntities: null };
360
- } else {
361
- // Toggle on: isolate this entity (and unhide it)
362
- const newHidden = new Set(state.hiddenEntities);
363
- newHidden.delete(id);
364
- return {
365
- isolatedEntities: new Set([id]),
366
- hiddenEntities: newHidden,
367
- };
368
- }
369
- }),
370
- isolateEntities: (ids) => set((state) => {
371
- // Toggle isolate: if these exact entities are already isolated, clear isolation
372
- // Otherwise, isolate them (and unhide them for good UX)
373
- const idsSet = new Set(ids);
374
- const isAlreadyIsolated = state.isolatedEntities !== null &&
375
- state.isolatedEntities.size === idsSet.size &&
376
- ids.every(id => state.isolatedEntities!.has(id));
377
-
378
- if (isAlreadyIsolated) {
379
- // Toggle off: clear isolation
380
- return { isolatedEntities: null };
381
- } else {
382
- // Toggle on: isolate these entities (and unhide them)
383
- const newHidden = new Set(state.hiddenEntities);
384
- ids.forEach(id => newHidden.delete(id));
385
- return {
386
- isolatedEntities: idsSet,
387
- hiddenEntities: newHidden,
388
- };
389
- }
390
- }),
391
- clearIsolation: () => set({ isolatedEntities: null }),
392
- showAll: () => set({ hiddenEntities: new Set(), isolatedEntities: null }),
393
- isEntityVisible: (id) => {
394
- const state = get();
395
- if (state.hiddenEntities.has(id)) return false;
396
- if (state.isolatedEntities !== null && !state.isolatedEntities.has(id)) return false;
397
- return true;
398
- },
399
-
400
- // Multi-selection actions
401
- addToSelection: (id) => set((state) => {
402
- const newSelection = new Set(state.selectedEntityIds);
403
- newSelection.add(id);
404
- return { selectedEntityIds: newSelection, selectedEntityId: id };
405
- }),
406
- removeFromSelection: (id) => set((state) => {
407
- const newSelection = new Set(state.selectedEntityIds);
408
- newSelection.delete(id);
409
- const remaining = Array.from(newSelection);
410
- return {
411
- selectedEntityIds: newSelection,
412
- selectedEntityId: remaining.length > 0 ? remaining[remaining.length - 1] : null,
413
- };
414
- }),
415
- toggleSelection: (id) => set((state) => {
416
- const newSelection = new Set(state.selectedEntityIds);
417
- if (newSelection.has(id)) {
418
- newSelection.delete(id);
419
- } else {
420
- newSelection.add(id);
421
- }
422
- const remaining = Array.from(newSelection);
423
- return {
424
- selectedEntityIds: newSelection,
425
- selectedEntityId: remaining.length > 0 ? remaining[remaining.length - 1] : null,
426
- };
427
- }),
428
- setSelectedEntityIds: (ids) => set({
429
- selectedEntityIds: new Set(ids),
430
- selectedEntityId: ids.length > 0 ? ids[ids.length - 1] : null,
431
- }),
432
- clearSelection: () => set({
433
- selectedEntityIds: new Set(),
434
- selectedEntityId: null,
435
- }),
436
-
437
- // Hover actions
438
- setHoverState: (hoverState) => set({ hoverState }),
439
- clearHover: () => set({ hoverState: { entityId: null, screenX: 0, screenY: 0 } }),
440
-
441
- // Context menu actions
442
- openContextMenu: (entityId, screenX, screenY) => set({
443
- contextMenu: { isOpen: true, entityId, screenX, screenY },
444
- }),
445
- closeContextMenu: () => set({
446
- contextMenu: { isOpen: false, entityId: null, screenX: 0, screenY: 0 },
447
- }),
448
-
449
- // Box selection actions
450
- startBoxSelect: (startX, startY) => set({
451
- boxSelect: { isSelecting: true, startX, startY, currentX: startX, currentY: startY },
452
- }),
453
- updateBoxSelect: (currentX, currentY) => set((state) => ({
454
- boxSelect: { ...state.boxSelect, currentX, currentY },
455
- })),
456
- endBoxSelect: () => set({
457
- boxSelect: { isSelecting: false, startX: 0, startY: 0, currentX: 0, currentY: 0 },
458
- }),
459
- cancelBoxSelect: () => set({
460
- boxSelect: { isSelecting: false, startX: 0, startY: 0, currentX: 0, currentY: 0 },
461
- }),
462
-
463
- // Measurement actions
464
- addMeasurePoint: (point) => set({ pendingMeasurePoint: point }),
465
- completeMeasurement: (endPoint) => set((state) => {
466
- if (!state.pendingMeasurePoint) return {};
467
- const start = state.pendingMeasurePoint;
468
- const distance = Math.sqrt(
469
- Math.pow(endPoint.x - start.x, 2) +
470
- Math.pow(endPoint.y - start.y, 2) +
471
- Math.pow(endPoint.z - start.z, 2)
472
- );
473
- const measurement: Measurement = {
474
- id: `m-${Date.now()}`,
475
- start,
476
- end: endPoint,
477
- distance,
478
- };
479
- return {
480
- measurements: [...state.measurements, measurement],
481
- pendingMeasurePoint: null,
482
- };
483
- }),
484
- deleteMeasurement: (id) => set((state) => ({
485
- measurements: state.measurements.filter((m) => m.id !== id),
486
- })),
487
- clearMeasurements: () => set({ measurements: [], pendingMeasurePoint: null }),
488
-
489
- // Section plane actions
490
- setSectionPlaneAxis: (axis) => set((state) => ({
491
- sectionPlane: { ...state.sectionPlane, axis },
492
- })),
493
- setSectionPlanePosition: (position) => set((state) => ({
494
- sectionPlane: { ...state.sectionPlane, position },
495
- })),
496
- toggleSectionPlane: () => set((state) => ({
497
- sectionPlane: { ...state.sectionPlane, enabled: !state.sectionPlane.enabled },
498
- })),
499
- flipSectionPlane: () => set((state) => ({
500
- sectionPlane: { ...state.sectionPlane, position: 100 - state.sectionPlane.position },
501
- })),
502
- resetSectionPlane: () => set({
503
- sectionPlane: { axis: 'y', position: 50, enabled: false },
504
- }),
505
-
506
- // Reset all viewer state when loading new file
507
- resetViewerState: () => set({
508
- selectedEntityId: null,
509
- selectedEntityIds: new Set(),
510
- selectedStorey: null,
511
- hiddenEntities: new Set(),
512
- isolatedEntities: null,
513
- hoverState: { entityId: null, screenX: 0, screenY: 0 },
514
- contextMenu: { isOpen: false, entityId: null, screenX: 0, screenY: 0 },
515
- boxSelect: { isSelecting: false, startX: 0, startY: 0, currentX: 0, currentY: 0 },
516
- measurements: [],
517
- pendingMeasurePoint: null,
518
- sectionPlane: { axis: 'y', position: 50, enabled: false },
519
- cameraRotation: { azimuth: 45, elevation: 25 },
520
- activeTool: 'select',
521
- }),
522
- }));
22
+ // Re-export everything from the modular store
23
+ export { useViewerStore } from './store/index.js';
24
+ export type { ViewerState } from './store/index.js';
25
+
26
+ // Re-export types for backward compatibility
27
+ export type {
28
+ MeasurePoint,
29
+ Measurement,
30
+ ActiveMeasurement,
31
+ EdgeLockState,
32
+ SectionPlaneAxis,
33
+ SectionPlane,
34
+ HoverState,
35
+ ContextMenuState,
36
+ SnapVisualization,
37
+ TypeVisibility,
38
+ CameraRotation,
39
+ CameraCallbacks,
40
+ // Multi-model federation types
41
+ EntityRef,
42
+ SchemaVersion,
43
+ FederatedModel,
44
+ } from './store/types.js';
45
+
46
+ // Re-export utility functions for multi-model federation
47
+ export { entityRefToString, stringToEntityRef, entityRefEquals, isIfcxDataStore } from './store/types.js';
@@ -0,0 +1,82 @@
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
+ * IFC loading configuration constants and utilities
7
+ * Extracted from useIfc.ts for reusability
8
+ */
9
+
10
+ import type { DynamicBatchConfig } from '@ifc-lite/geometry';
11
+
12
+ // ============================================================================
13
+ // Server Configuration
14
+ // ============================================================================
15
+
16
+ /** IFC server URL - only set via environment variable, no default (pure client-side by default) */
17
+ export const SERVER_URL = import.meta.env.VITE_IFC_SERVER_URL || import.meta.env.VITE_SERVER_URL || '';
18
+
19
+ /** Enable server parsing - only if server URL is explicitly configured */
20
+ export const USE_SERVER = SERVER_URL !== '' && import.meta.env.VITE_USE_SERVER !== 'false';
21
+
22
+ // ============================================================================
23
+ // File Size Thresholds (in bytes unless noted)
24
+ // ============================================================================
25
+
26
+ /** Minimum file size to cache (10MB) - smaller files parse quickly anyway */
27
+ export const CACHE_SIZE_THRESHOLD = 10 * 1024 * 1024;
28
+
29
+ /** File size thresholds for various optimizations */
30
+ export const THRESHOLDS = {
31
+ /** Use streaming Parquet above this (150MB) */
32
+ STREAMING_MB: 150,
33
+ /** Use Parquet vs JSON above this (10MB) */
34
+ PARQUET_MB: 10,
35
+ /** Large file threshold affecting batch sizing (50MB) */
36
+ LARGE_FILE_MB: 50,
37
+ /** Huge file threshold for aggressive batching (100MB) */
38
+ HUGE_FILE_MB: 100,
39
+ /** Don't cache files smaller than this (10MB) */
40
+ CACHE_MIN_MB: 10,
41
+ } as const;
42
+
43
+ // ============================================================================
44
+ // Platform Detection
45
+ // ============================================================================
46
+
47
+ /** Detect if running in Tauri (desktop) environment */
48
+ export const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
49
+
50
+ // ============================================================================
51
+ // Dynamic Batch Configuration
52
+ // ============================================================================
53
+
54
+ /**
55
+ * Calculate dynamic batch config based on file size
56
+ * Larger files get larger batches for better throughput
57
+ *
58
+ * @param fileSizeMB - File size in megabytes
59
+ * @returns Batch configuration for geometry processing
60
+ */
61
+ export function getDynamicBatchConfig(fileSizeMB: number): DynamicBatchConfig {
62
+ if (fileSizeMB < 10) {
63
+ // Small files: smaller batches for responsiveness
64
+ return { initialBatchSize: 50, maxBatchSize: 200, fileSizeMB };
65
+ } else if (fileSizeMB < 50) {
66
+ // Medium files: balanced batching
67
+ return { initialBatchSize: 100, maxBatchSize: 500, fileSizeMB };
68
+ } else if (fileSizeMB < 100) {
69
+ // Large files: larger batches for throughput
70
+ return { initialBatchSize: 100, maxBatchSize: 1000, fileSizeMB };
71
+ } else {
72
+ // Huge files (100MB+): aggressive batching for maximum throughput
73
+ return { initialBatchSize: 100, maxBatchSize: 3000, fileSizeMB };
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Convert bytes to megabytes
79
+ */
80
+ export function bytesToMB(bytes: number): number {
81
+ return bytes / (1024 * 1024);
82
+ }