@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
@@ -0,0 +1,279 @@
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
+ * Hook for IFC file caching operations
7
+ * Handles loading from and saving to binary cache for fast subsequent loads
8
+ *
9
+ * Extracted from useIfc.ts for better separation of concerns
10
+ */
11
+
12
+ import { useCallback } from 'react';
13
+ import {
14
+ BinaryCacheWriter,
15
+ BinaryCacheReader,
16
+ type IfcDataStore as CacheDataStore,
17
+ type GeometryData,
18
+ } from '@ifc-lite/cache';
19
+ import { SpatialHierarchyBuilder, extractLengthUnitScale, type IfcDataStore } from '@ifc-lite/parser';
20
+ import { buildSpatialIndex } from '@ifc-lite/spatial';
21
+ import type { MeshData } from '@ifc-lite/geometry';
22
+
23
+ import { useViewerStore } from '../store.js';
24
+ import { getCached, setCached, deleteCached, type CacheResult } from '../services/cacheService.js';
25
+ import { rebuildSpatialHierarchy, rebuildOnDemandMaps } from '../utils/spatialHierarchy.js';
26
+ import { calculateStoreyHeights } from '../utils/localParsingUtils.js';
27
+
28
+ // Re-export types for convenience
29
+ export type { CacheResult } from '../services/cacheService.js';
30
+ export { getCached, setCached, deleteCached } from '../services/cacheService.js';
31
+
32
+ // ============================================================================
33
+ // Types
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Progress callback for cache operations
38
+ */
39
+ export interface CacheProgress {
40
+ phase: string;
41
+ percent: number;
42
+ }
43
+
44
+ /**
45
+ * Geometry result from cache
46
+ */
47
+ export interface CacheGeometryResult {
48
+ meshes: MeshData[];
49
+ totalVertices: number;
50
+ totalTriangles: number;
51
+ coordinateInfo?: {
52
+ originShift: { x: number; y: number; z: number };
53
+ bounds: { min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } };
54
+ };
55
+ }
56
+
57
+ // ============================================================================
58
+ // Hook
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Hook providing cache loading and saving operations
63
+ */
64
+ export function useIfcCache() {
65
+ const {
66
+ setProgress,
67
+ setIfcDataStore,
68
+ setGeometryResult,
69
+ } = useViewerStore();
70
+
71
+ /**
72
+ * Load from binary cache - INSTANT load for maximum speed
73
+ * Large cached models load all geometry at once for fastest total time
74
+ */
75
+ const loadFromCache = useCallback(async (
76
+ cacheResult: CacheResult,
77
+ fileName: string,
78
+ cacheKey?: string
79
+ ): Promise<boolean> => {
80
+ try {
81
+ const cacheLoadStart = performance.now();
82
+ setProgress({ phase: 'Loading from cache', percent: 10 });
83
+
84
+ // Reset geometry first so Viewport detects this as a new file
85
+ setGeometryResult(null);
86
+
87
+ const reader = new BinaryCacheReader();
88
+ const result = await reader.read(cacheResult.buffer);
89
+ const cacheReadTime = performance.now() - cacheLoadStart;
90
+
91
+ // Convert cache data store to viewer data store format
92
+ const dataStore = result.dataStore as any;
93
+
94
+ // Restore source buffer for on-demand property extraction
95
+ if (cacheResult.sourceBuffer) {
96
+ dataStore.source = new Uint8Array(cacheResult.sourceBuffer);
97
+
98
+ // Quick scan to rebuild entity index with byte offsets (needed for on-demand extraction)
99
+ const { StepTokenizer } = await import('@ifc-lite/parser');
100
+ const tokenizer = new StepTokenizer(dataStore.source);
101
+ const entityIndex = {
102
+ byId: new Map<number, any>(),
103
+ byType: new Map<string, number[]>(),
104
+ };
105
+
106
+ for (const ref of tokenizer.scanEntitiesFast()) {
107
+ entityIndex.byId.set(ref.expressId, {
108
+ expressId: ref.expressId,
109
+ type: ref.type,
110
+ byteOffset: ref.offset,
111
+ byteLength: ref.length,
112
+ lineNumber: ref.line,
113
+ });
114
+ let typeList = entityIndex.byType.get(ref.type);
115
+ if (!typeList) {
116
+ typeList = [];
117
+ entityIndex.byType.set(ref.type, typeList);
118
+ }
119
+ typeList.push(ref.expressId);
120
+ }
121
+ dataStore.entityIndex = entityIndex;
122
+
123
+ // Rebuild on-demand maps from relationships
124
+ // Pass entityIndex which contains ALL entity types including IfcPropertySet/IfcElementQuantity
125
+ // (the entity table may not include these since they're filtered during fresh parse)
126
+ const { onDemandPropertyMap, onDemandQuantityMap } = rebuildOnDemandMaps(
127
+ dataStore.entities,
128
+ dataStore.relationships,
129
+ dataStore.entityIndex
130
+ );
131
+ dataStore.onDemandPropertyMap = onDemandPropertyMap;
132
+ dataStore.onDemandQuantityMap = onDemandQuantityMap;
133
+ } else {
134
+ console.warn('[useIfcCache] No source buffer in cache - on-demand property extraction disabled');
135
+ dataStore.source = new Uint8Array(0);
136
+ }
137
+
138
+ // Rebuild spatial hierarchy from cache data (cache doesn't serialize it)
139
+ // Use SpatialHierarchyBuilder to extract elevations from source buffer
140
+ if (!dataStore.spatialHierarchy && dataStore.entities && dataStore.relationships) {
141
+ // Ensure we have source buffer and entityIndex for elevation extraction
142
+ if (dataStore.source && dataStore.source.length > 0 && dataStore.entityIndex && dataStore.strings) {
143
+ const lengthUnitScale = extractLengthUnitScale(dataStore.source, dataStore.entityIndex);
144
+ const builder = new SpatialHierarchyBuilder();
145
+ dataStore.spatialHierarchy = builder.build(
146
+ dataStore.entities,
147
+ dataStore.relationships,
148
+ dataStore.strings,
149
+ dataStore.source,
150
+ dataStore.entityIndex,
151
+ lengthUnitScale
152
+ );
153
+
154
+ // Calculate storey heights from elevation differences (fallback if no property data)
155
+ if (dataStore.spatialHierarchy.storeyHeights.size === 0 && dataStore.spatialHierarchy.storeyElevations.size > 1) {
156
+ const calculatedHeights = calculateStoreyHeights(dataStore.spatialHierarchy.storeyElevations);
157
+ for (const [storeyId, height] of calculatedHeights) {
158
+ dataStore.spatialHierarchy.storeyHeights.set(storeyId, height);
159
+ }
160
+ }
161
+ } else {
162
+ console.warn('[useIfcCache] Missing data for elevation extraction:', {
163
+ hasSource: !!dataStore.source,
164
+ sourceLength: dataStore.source?.length ?? 0,
165
+ hasEntityIndex: !!dataStore.entityIndex,
166
+ hasStrings: !!dataStore.strings,
167
+ });
168
+ // Fallback: use simplified rebuild if source data not available
169
+ dataStore.spatialHierarchy = rebuildSpatialHierarchy(
170
+ dataStore.entities,
171
+ dataStore.relationships
172
+ );
173
+ }
174
+ }
175
+
176
+ if (result.geometry) {
177
+ const { meshes, coordinateInfo, totalVertices, totalTriangles } = result.geometry;
178
+
179
+ // INSTANT: Set ALL geometry in ONE call - fastest for cached models
180
+ setGeometryResult({
181
+ meshes,
182
+ totalVertices,
183
+ totalTriangles,
184
+ coordinateInfo,
185
+ });
186
+
187
+ // Set data store
188
+ setIfcDataStore(dataStore);
189
+
190
+ // Build spatial index in background (non-blocking)
191
+ if (meshes.length > 0) {
192
+ const buildIndex = () => {
193
+ try {
194
+ const spatialIndex = buildSpatialIndex(meshes);
195
+ dataStore.spatialIndex = spatialIndex;
196
+ setIfcDataStore({ ...dataStore });
197
+ } catch (err) {
198
+ console.warn('[useIfcCache] Failed to build spatial index:', err);
199
+ }
200
+ };
201
+
202
+ if ('requestIdleCallback' in window) {
203
+ (window as any).requestIdleCallback(buildIndex, { timeout: 2000 });
204
+ } else {
205
+ // Fallback for browsers without requestIdleCallback
206
+ setTimeout(buildIndex, 100);
207
+ }
208
+ }
209
+ } else {
210
+ setIfcDataStore(dataStore);
211
+ }
212
+
213
+ setProgress({ phase: 'Complete (from cache)', percent: 100 });
214
+ const totalCacheTime = performance.now() - cacheLoadStart;
215
+ const meshCount = result.geometry?.meshes.length || 0;
216
+ console.log(`[useIfcCache] ✓ ${fileName} (cached) → ${meshCount} meshes | ${totalCacheTime.toFixed(0)}ms`);
217
+
218
+ return true;
219
+ } catch (err) {
220
+ console.error('[useIfcCache] Failed to load from cache:', err);
221
+ // Clear corrupted cache entry if we have the key
222
+ if (cacheKey) {
223
+ try {
224
+ await deleteCached(cacheKey);
225
+ console.log('[useIfcCache] Cleared corrupted cache entry:', cacheKey);
226
+ } catch {
227
+ // Ignore cleanup errors
228
+ }
229
+ }
230
+ return false;
231
+ }
232
+ }, [setProgress, setIfcDataStore, setGeometryResult]);
233
+
234
+ /**
235
+ * Save parsed data and geometry to cache
236
+ */
237
+ const saveToCache = useCallback(async (
238
+ cacheKey: string,
239
+ dataStore: IfcDataStore,
240
+ geometry: GeometryData,
241
+ sourceBuffer: ArrayBuffer,
242
+ fileName: string
243
+ ): Promise<void> => {
244
+ try {
245
+ console.log('[useIfcCache] Starting cache write for:', fileName);
246
+ const writer = new BinaryCacheWriter();
247
+
248
+ // Adapt dataStore to cache format
249
+ const cacheDataStore: CacheDataStore = {
250
+ schema: dataStore.schemaVersion === 'IFC4' ? 1 : dataStore.schemaVersion === 'IFC4X3' ? 2 : 0,
251
+ entityCount: dataStore.entityCount || dataStore.entities?.count || 0,
252
+ strings: dataStore.strings,
253
+ entities: dataStore.entities,
254
+ properties: dataStore.properties,
255
+ quantities: dataStore.quantities,
256
+ relationships: dataStore.relationships,
257
+ spatialHierarchy: dataStore.spatialHierarchy,
258
+ };
259
+
260
+ console.log('[useIfcCache] Writing cache buffer...');
261
+ const cacheBuffer = await writer.write(cacheDataStore, geometry, sourceBuffer, { includeGeometry: true });
262
+ console.log('[useIfcCache] Cache buffer written:', cacheBuffer.byteLength, 'bytes');
263
+
264
+ console.log('[useIfcCache] Saving to cache storage...');
265
+ await setCached(cacheKey, cacheBuffer, fileName, sourceBuffer.byteLength, sourceBuffer);
266
+ console.log('[useIfcCache] ✓ Cache saved successfully');
267
+ } catch (err) {
268
+ console.error('[useIfcCache] Failed to cache model:', err);
269
+ console.error('[useIfcCache] Error stack:', err instanceof Error ? err.stack : 'No stack trace');
270
+ }
271
+ }, []);
272
+
273
+ return {
274
+ loadFromCache,
275
+ saveToCache,
276
+ getCached,
277
+ setCached,
278
+ };
279
+ }
@@ -18,12 +18,20 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
18
18
 
19
19
  const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
20
20
  const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
21
+ const activeTool = useViewerStore((s) => s.activeTool);
21
22
  const setActiveTool = useViewerStore((s) => s.setActiveTool);
22
23
  const isolateEntity = useViewerStore((s) => s.isolateEntity);
23
24
  const hideEntity = useViewerStore((s) => s.hideEntity);
24
25
  const showAll = useViewerStore((s) => s.showAll);
26
+ const clearStoreySelection = useViewerStore((s) => s.clearStoreySelection);
25
27
  const toggleTheme = useViewerStore((s) => s.toggleTheme);
26
28
 
29
+ // Measure tool specific actions
30
+ const activeMeasurement = useViewerStore((s) => s.activeMeasurement);
31
+ const cancelMeasurement = useViewerStore((s) => s.cancelMeasurement);
32
+ const clearMeasurements = useViewerStore((s) => s.clearMeasurements);
33
+ const toggleSnap = useViewerStore((s) => s.toggleSnap);
34
+
27
35
  const handleKeyDown = useCallback((e: KeyboardEvent) => {
28
36
  // Ignore if typing in an input or textarea
29
37
  const target = e.target as HTMLElement;
@@ -65,10 +73,6 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
65
73
  e.preventDefault();
66
74
  setActiveTool('section');
67
75
  }
68
- if (key === 'b' && !ctrl && !shift) {
69
- e.preventDefault();
70
- setActiveTool('boxselect');
71
- }
72
76
 
73
77
  // Visibility controls
74
78
  if (key === 'i' && !ctrl && !shift && selectedEntityId) {
@@ -82,6 +86,35 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
82
86
  if (key === 'a' && !ctrl && !shift) {
83
87
  e.preventDefault();
84
88
  showAll();
89
+ clearStoreySelection(); // Also clear storey filtering
90
+ }
91
+
92
+ // Measure tool shortcuts
93
+ if (activeTool === 'measure') {
94
+ // Cancel active measurement with ESC
95
+ if (key === 'escape' && activeMeasurement) {
96
+ e.preventDefault();
97
+ cancelMeasurement();
98
+ return;
99
+ }
100
+ // Clear all measurements with Ctrl+C or Cmd+C
101
+ if (key === 'c' && ctrl && !shift) {
102
+ e.preventDefault();
103
+ clearMeasurements();
104
+ return;
105
+ }
106
+ // Toggle snapping with S
107
+ if (key === 's' && !ctrl && !shift) {
108
+ e.preventDefault();
109
+ toggleSnap();
110
+ return;
111
+ }
112
+ // Delete/Backspace clears measurements (when nothing is selected)
113
+ if ((key === 'delete' || key === 'backspace') && !ctrl && !shift && !selectedEntityId) {
114
+ e.preventDefault();
115
+ clearMeasurements();
116
+ return;
117
+ }
85
118
  }
86
119
 
87
120
  // Selection - Escape clears selection and switches to select tool
@@ -89,6 +122,7 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
89
122
  e.preventDefault();
90
123
  setSelectedEntityId(null);
91
124
  showAll();
125
+ clearStoreySelection(); // Also clear storey filtering
92
126
  setActiveTool('select');
93
127
  }
94
128
 
@@ -103,11 +137,17 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
103
137
  }, [
104
138
  selectedEntityId,
105
139
  setSelectedEntityId,
140
+ activeTool,
106
141
  setActiveTool,
107
142
  isolateEntity,
108
143
  hideEntity,
109
144
  showAll,
145
+ clearStoreySelection,
110
146
  toggleTheme,
147
+ activeMeasurement,
148
+ cancelMeasurement,
149
+ clearMeasurements,
150
+ toggleSnap,
111
151
  ]);
112
152
 
113
153
  useEffect(() => {
@@ -123,20 +163,22 @@ export function useKeyboardShortcuts(options: KeyboardShortcutsOptions = {}) {
123
163
  // Export shortcut definitions for UI display
124
164
  export const KEYBOARD_SHORTCUTS = [
125
165
  { key: 'V', description: 'Select tool', category: 'Tools' },
126
- { key: 'B', description: 'Box select tool', category: 'Tools' },
127
166
  { key: 'P', description: 'Pan tool', category: 'Tools' },
128
167
  { key: 'O', description: 'Orbit tool', category: 'Tools' },
129
168
  { key: 'C', description: 'Walk mode', category: 'Tools' },
130
169
  { key: 'M', description: 'Measure tool', category: 'Tools' },
131
170
  { key: 'X', description: 'Section tool', category: 'Tools' },
171
+ { key: 'S', description: 'Toggle snapping (Measure tool)', category: 'Tools' },
172
+ { key: 'Esc', description: 'Cancel measurement (Measure tool)', category: 'Tools' },
173
+ { key: 'Ctrl+C', description: 'Clear measurements (Measure tool)', category: 'Tools' },
132
174
  { key: 'I', description: 'Isolate selection', category: 'Visibility' },
133
175
  { key: 'Del', description: 'Hide selection', category: 'Visibility' },
134
- { key: 'A', description: 'Show all', category: 'Visibility' },
176
+ { key: 'A', description: 'Show all (reset filters)', category: 'Visibility' },
135
177
  { key: 'H', description: 'Home (Isometric view)', category: 'Camera' },
136
178
  { key: 'Z', description: 'Fit all (zoom extents)', category: 'Camera' },
137
179
  { key: 'F', description: 'Frame selection', category: 'Camera' },
138
180
  { key: '0-6', description: 'Preset views', category: 'Camera' },
139
181
  { key: 'T', description: 'Toggle theme', category: 'UI' },
140
- { key: 'Esc', description: 'Clear selection & switch to Select tool', category: 'Selection' },
141
- { key: '?', description: 'Show keyboard shortcuts', category: 'Help' },
182
+ { key: 'Esc', description: 'Reset all (clear selection, filters, isolation)', category: 'Selection' },
183
+ { key: '?', description: 'Show info panel', category: 'Help' },
142
184
  ] as const;
@@ -0,0 +1,61 @@
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
+ * Hook to sync selectedEntityId with selectedEntity (model-aware selection)
7
+ *
8
+ * When an entity is selected (via click or other means), this hook:
9
+ * 1. Watches for changes to selectedEntityId (which is now a globalId)
10
+ * 2. Uses store-based resolver to find (modelId, originalExpressId)
11
+ * 3. Updates selectedEntity with { modelId, expressId } for PropertiesPanel
12
+ *
13
+ * IMPORTANT: selectedEntityId is a globalId (transformed at load time)
14
+ * The EntityRef.expressId is the ORIGINAL expressId for property lookup
15
+ *
16
+ * NOTE: We use resolveGlobalIdFromModels (store-based) instead of the singleton
17
+ * federationRegistry because it's more reliable - the Zustand store is always
18
+ * in sync with React, whereas the singleton might have bundling issues.
19
+ */
20
+
21
+ import { useEffect } from 'react';
22
+ import { useViewerStore } from '../store.js';
23
+
24
+ export function useModelSelection() {
25
+ const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
26
+ const setSelectedEntity = useViewerStore((s) => s.setSelectedEntity);
27
+ // Subscribe to models for reactivity (when models are added/removed)
28
+ const models = useViewerStore((s) => s.models);
29
+ // Use the bulletproof store-based resolver
30
+ const resolveGlobalIdFromModels = useViewerStore((s) => s.resolveGlobalIdFromModels);
31
+
32
+ useEffect(() => {
33
+ if (selectedEntityId === null) {
34
+ // Don't clear selectedEntity when selectedEntityId is null
35
+ // This allows selectedModelId to remain set when clicking model headers
36
+ // The model selection flow: setSelectedModelId -> sets selectedEntityId=null
37
+ // If we called setSelectedEntity(null) here, it would clear selectedModelId
38
+ return;
39
+ }
40
+
41
+ // selectedEntityId is now a globalId
42
+ // Resolve it back to (modelId, originalExpressId) using the store-based resolver
43
+ // This is more reliable than the singleton registry which might have bundling issues
44
+ const resolved = resolveGlobalIdFromModels(selectedEntityId);
45
+ if (resolved) {
46
+ // Set EntityRef with ORIGINAL expressId (for property lookup in IfcDataStore)
47
+ setSelectedEntity({ modelId: resolved.modelId, expressId: resolved.expressId });
48
+ } else {
49
+ // Fallback for single-model mode (offset = 0, globalId = expressId)
50
+ // In this case, try to find the first model and use the globalId as expressId
51
+ if (models.size > 0) {
52
+ const firstModelId = Array.from(models.keys())[0];
53
+ setSelectedEntity({ modelId: firstModelId, expressId: selectedEntityId });
54
+ } else {
55
+ // Legacy single-model mode: use 'legacy' as modelId
56
+ // This allows PropertiesPanel to fall back to the legacy query
57
+ setSelectedEntity({ modelId: 'legacy', expressId: selectedEntityId });
58
+ }
59
+ }
60
+ }, [selectedEntityId, setSelectedEntity, models, resolveGlobalIdFromModels]);
61
+ }
@@ -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
+ * Organized store selectors for the Viewport component
7
+ * Groups 47+ store subscriptions into logical categories for better maintainability
8
+ * Extracted from Viewport.tsx for reusability
9
+ */
10
+
11
+ import { useViewerStore } from '../store.js';
12
+
13
+ /**
14
+ * Selection-related store state and actions
15
+ */
16
+ export function useSelectionState() {
17
+ const selectedEntityId = useViewerStore((state) => state.selectedEntityId);
18
+ const selectedEntityIds = useViewerStore((state) => state.selectedEntityIds);
19
+ const setSelectedEntityId = useViewerStore((state) => state.setSelectedEntityId);
20
+ const setSelectedEntity = useViewerStore((state) => state.setSelectedEntity);
21
+ const toggleSelection = useViewerStore((state) => state.toggleSelection);
22
+ const models = useViewerStore((state) => state.models);
23
+
24
+ return {
25
+ selectedEntityId,
26
+ selectedEntityIds,
27
+ setSelectedEntityId,
28
+ setSelectedEntity,
29
+ toggleSelection,
30
+ models,
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Visibility-related store state (hidden/isolated entities)
36
+ */
37
+ export function useVisibilityState() {
38
+ const hiddenEntities = useViewerStore((state) => state.hiddenEntities);
39
+ const isolatedEntities = useViewerStore((state) => state.isolatedEntities);
40
+
41
+ return {
42
+ hiddenEntities,
43
+ isolatedEntities,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Tool-related store state and actions
49
+ */
50
+ export function useToolState() {
51
+ const activeTool = useViewerStore((state) => state.activeTool);
52
+ const sectionPlane = useViewerStore((state) => state.sectionPlane);
53
+
54
+ return {
55
+ activeTool,
56
+ sectionPlane,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Measurement-related store state and actions
62
+ */
63
+ export function useMeasurementState() {
64
+ // Basic measurement state
65
+ const measurements = useViewerStore((state) => state.measurements);
66
+ const pendingMeasurePoint = useViewerStore((state) => state.pendingMeasurePoint);
67
+ const activeMeasurement = useViewerStore((state) => state.activeMeasurement);
68
+
69
+ // Measurement actions
70
+ const addMeasurePoint = useViewerStore((state) => state.addMeasurePoint);
71
+ const completeMeasurement = useViewerStore((state) => state.completeMeasurement);
72
+ const startMeasurement = useViewerStore((state) => state.startMeasurement);
73
+ const updateMeasurement = useViewerStore((state) => state.updateMeasurement);
74
+ const finalizeMeasurement = useViewerStore((state) => state.finalizeMeasurement);
75
+ const cancelMeasurement = useViewerStore((state) => state.cancelMeasurement);
76
+ const updateMeasurementScreenCoords = useViewerStore((state) => state.updateMeasurementScreenCoords);
77
+
78
+ // Snap state
79
+ const snapEnabled = useViewerStore((state) => state.snapEnabled);
80
+ const setSnapTarget = useViewerStore((state) => state.setSnapTarget);
81
+ const setSnapVisualization = useViewerStore((state) => state.setSnapVisualization);
82
+
83
+ // Edge lock state for magnetic snapping
84
+ const edgeLockState = useViewerStore((state) => state.edgeLockState);
85
+ const setEdgeLock = useViewerStore((state) => state.setEdgeLock);
86
+ const updateEdgeLockPosition = useViewerStore((state) => state.updateEdgeLockPosition);
87
+ const clearEdgeLock = useViewerStore((state) => state.clearEdgeLock);
88
+ const incrementEdgeLockStrength = useViewerStore((state) => state.incrementEdgeLockStrength);
89
+
90
+ // Orthogonal constraint for shift+drag measurements
91
+ const measurementConstraintEdge = useViewerStore((state) => state.measurementConstraintEdge);
92
+ const setMeasurementConstraintEdge = useViewerStore((state) => state.setMeasurementConstraintEdge);
93
+ const updateConstraintActiveAxis = useViewerStore((state) => state.updateConstraintActiveAxis);
94
+ const clearMeasurementConstraintEdge = useViewerStore((state) => state.clearMeasurementConstraintEdge);
95
+
96
+ return {
97
+ // State
98
+ measurements,
99
+ pendingMeasurePoint,
100
+ activeMeasurement,
101
+ snapEnabled,
102
+ edgeLockState,
103
+ measurementConstraintEdge,
104
+
105
+ // Actions
106
+ addMeasurePoint,
107
+ completeMeasurement,
108
+ startMeasurement,
109
+ updateMeasurement,
110
+ finalizeMeasurement,
111
+ cancelMeasurement,
112
+ updateMeasurementScreenCoords,
113
+ setSnapTarget,
114
+ setSnapVisualization,
115
+ setEdgeLock,
116
+ updateEdgeLockPosition,
117
+ clearEdgeLock,
118
+ incrementEdgeLockStrength,
119
+ setMeasurementConstraintEdge,
120
+ updateConstraintActiveAxis,
121
+ clearMeasurementConstraintEdge,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Camera-related store actions
127
+ */
128
+ export function useCameraState() {
129
+ const updateCameraRotationRealtime = useViewerStore((state) => state.updateCameraRotationRealtime);
130
+ const updateScaleRealtime = useViewerStore((state) => state.updateScaleRealtime);
131
+ const setCameraCallbacks = useViewerStore((state) => state.setCameraCallbacks);
132
+
133
+ return {
134
+ updateCameraRotationRealtime,
135
+ updateScaleRealtime,
136
+ setCameraCallbacks,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Hover/tooltip-related store state and actions
142
+ */
143
+ export function useHoverState() {
144
+ const hoverTooltipsEnabled = useViewerStore((state) => state.hoverTooltipsEnabled);
145
+ const setHoverState = useViewerStore((state) => state.setHoverState);
146
+ const clearHover = useViewerStore((state) => state.clearHover);
147
+
148
+ return {
149
+ hoverTooltipsEnabled,
150
+ setHoverState,
151
+ clearHover,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Theme-related store state
157
+ */
158
+ export function useThemeState() {
159
+ const theme = useViewerStore((state) => state.theme);
160
+
161
+ return {
162
+ theme,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Context menu-related store actions
168
+ */
169
+ export function useContextMenuState() {
170
+ const openContextMenu = useViewerStore((state) => state.openContextMenu);
171
+
172
+ return {
173
+ openContextMenu,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Color update-related store state and actions
179
+ */
180
+ export function useColorUpdateState() {
181
+ const pendingColorUpdates = useViewerStore((state) => state.pendingColorUpdates);
182
+ const clearPendingColorUpdates = useViewerStore((state) => state.clearPendingColorUpdates);
183
+
184
+ return {
185
+ pendingColorUpdates,
186
+ clearPendingColorUpdates,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * IFC data store state
192
+ */
193
+ export function useIfcDataState() {
194
+ const ifcDataStore = useViewerStore((state) => state.ifcDataStore);
195
+
196
+ return {
197
+ ifcDataStore,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * All viewport-related selectors combined
203
+ * Use individual hooks above for more granular control
204
+ */
205
+ export function useViewerSelectors() {
206
+ return {
207
+ selection: useSelectionState(),
208
+ visibility: useVisibilityState(),
209
+ tools: useToolState(),
210
+ measurement: useMeasurementState(),
211
+ camera: useCameraState(),
212
+ hover: useHoverState(),
213
+ theme: useThemeState(),
214
+ contextMenu: useContextMenuState(),
215
+ colorUpdates: useColorUpdateState(),
216
+ ifcData: useIfcDataState(),
217
+ };
218
+ }