@ifc-lite/viewer 1.10.0 → 1.11.1
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.
- package/CHANGELOG.md +52 -0
- package/dist/assets/{Arrow.dom-Bw5JMdDs.js → Arrow.dom-p9ppgFLr.js} +1 -1
- package/dist/assets/{browser-DdRf3aWl.js → browser-lKzgHsnJ.js} +1 -1
- package/dist/assets/{ifc-lite_bg-C1-gLAHo.wasm → ifc-lite_bg-B6s-pcv0.wasm} +0 -0
- package/dist/assets/index-BoYyWYAu.css +1 -0
- package/dist/assets/{index-1ff6P0kc.js → index-CF854G-8.js} +42703 -41097
- package/dist/assets/{index-Bz7vHRxl.js → index-DQlpY6aJ.js} +4 -4
- package/dist/assets/{native-bridge-C5hD5vae.js → native-bridge-BgRWyawy.js} +1 -1
- package/dist/assets/{wasm-bridge-CaNKXFGM.js → wasm-bridge-BZxGtE7z.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +20 -19
- package/src/components/viewer/BasketPresentationDock.tsx +422 -0
- package/src/components/viewer/CommandPalette.tsx +29 -32
- package/src/components/viewer/EntityContextMenu.tsx +37 -22
- package/src/components/viewer/HierarchyPanel.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +56 -113
- package/src/components/viewer/Section2DPanel.tsx +8 -1
- package/src/components/viewer/ThemeSwitch.tsx +55 -0
- package/src/components/viewer/Viewport.tsx +66 -105
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/ViewportOverlays.tsx +9 -3
- package/src/components/viewer/useGeometryStreaming.ts +25 -0
- package/src/components/viewer/useKeyboardControls.ts +2 -2
- package/src/components/viewer/useRenderUpdates.ts +10 -3
- package/src/hooks/meshColorUpdates.test.ts +56 -0
- package/src/hooks/meshColorUpdates.ts +20 -0
- package/src/hooks/useIDS.ts +7 -8
- package/src/hooks/useIfcLoader.ts +25 -1
- package/src/hooks/useKeyboardShortcuts.ts +51 -84
- package/src/hooks/useViewerSelectors.ts +4 -0
- package/src/store/basket/basketCommands.ts +81 -0
- package/src/store/basket/basketViewActivator.ts +54 -0
- package/src/store/basketSave.ts +122 -0
- package/src/store/basketVisibleSet.test.ts +161 -0
- package/src/store/basketVisibleSet.ts +487 -0
- package/src/store/homeView.ts +21 -0
- package/src/store/index.ts +8 -0
- package/src/store/slices/dataSlice.test.ts +53 -4
- package/src/store/slices/dataSlice.ts +13 -5
- package/src/store/slices/drawing2DSlice.ts +5 -0
- package/src/store/slices/pinboardSlice.test.ts +160 -0
- package/src/store/slices/pinboardSlice.ts +248 -18
- package/src/store/types.ts +11 -0
- package/dist/assets/index-mvbV6NHd.css +0 -1
|
@@ -17,30 +17,34 @@ import {
|
|
|
17
17
|
Copy,
|
|
18
18
|
Maximize2,
|
|
19
19
|
Building2,
|
|
20
|
+
Save,
|
|
20
21
|
} from 'lucide-react';
|
|
21
22
|
import { useViewerStore, resolveEntityRef } from '@/store';
|
|
23
|
+
import { resetVisibilityForHomeFromStore } from '@/store/homeView';
|
|
24
|
+
import {
|
|
25
|
+
executeBasketSet,
|
|
26
|
+
executeBasketAdd,
|
|
27
|
+
executeBasketRemove,
|
|
28
|
+
executeBasketSaveView,
|
|
29
|
+
} from '@/store/basket/basketCommands';
|
|
22
30
|
import { useIfc } from '@/hooks/useIfc';
|
|
23
31
|
|
|
24
32
|
export function EntityContextMenu() {
|
|
25
33
|
const contextMenu = useViewerStore((s) => s.contextMenu);
|
|
26
34
|
const closeContextMenu = useViewerStore((s) => s.closeContextMenu);
|
|
27
35
|
const hideEntity = useViewerStore((s) => s.hideEntity);
|
|
28
|
-
const showAll = useViewerStore((s) => s.showAll);
|
|
29
36
|
const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
|
|
30
37
|
const setSelectedEntityIds = useViewerStore((s) => s.setSelectedEntityIds);
|
|
31
38
|
const cameraCallbacks = useViewerStore((s) => s.cameraCallbacks);
|
|
32
39
|
// Basket actions
|
|
33
|
-
const setBasket = useViewerStore((s) => s.setBasket);
|
|
34
|
-
const addToBasket = useViewerStore((s) => s.addToBasket);
|
|
35
|
-
const removeFromBasket = useViewerStore((s) => s.removeFromBasket);
|
|
36
40
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
37
41
|
const { ifcDataStore, models } = useIfc();
|
|
38
42
|
|
|
39
43
|
// Resolve contextMenu.entityId (globalId) to original expressId and model
|
|
40
44
|
// This is needed because IfcDataStore uses original expressIds, not globalIds
|
|
41
|
-
const { resolvedExpressId,
|
|
45
|
+
const { resolvedExpressId, activeDataStore, contextEntityRef } = useMemo(() => {
|
|
42
46
|
if (!contextMenu.entityId) {
|
|
43
|
-
return { resolvedExpressId: null,
|
|
47
|
+
return { resolvedExpressId: null, activeDataStore: ifcDataStore, contextEntityRef: null };
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// Single source of truth for globalId → EntityRef resolution
|
|
@@ -49,12 +53,16 @@ export function EntityContextMenu() {
|
|
|
49
53
|
const model = models.get(ref.modelId);
|
|
50
54
|
return {
|
|
51
55
|
resolvedExpressId: ref.expressId,
|
|
52
|
-
resolvedModelId: ref.modelId,
|
|
53
56
|
activeDataStore: model?.ifcDataStore ?? ifcDataStore,
|
|
57
|
+
contextEntityRef: ref,
|
|
54
58
|
};
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
resolvedExpressId: contextMenu.entityId,
|
|
63
|
+
activeDataStore: ifcDataStore,
|
|
64
|
+
contextEntityRef: null,
|
|
65
|
+
};
|
|
58
66
|
}, [contextMenu.entityId, models, ifcDataStore]);
|
|
59
67
|
|
|
60
68
|
// Close menu when clicking outside
|
|
@@ -95,27 +103,33 @@ export function EntityContextMenu() {
|
|
|
95
103
|
|
|
96
104
|
// Basket: = Set basket to this entity
|
|
97
105
|
const handleSetBasket = useCallback(() => {
|
|
98
|
-
|
|
99
|
-
setBasket([{ modelId: resolvedModelId, expressId: resolvedExpressId }]);
|
|
100
|
-
}
|
|
106
|
+
executeBasketSet(contextEntityRef);
|
|
101
107
|
closeContextMenu();
|
|
102
|
-
}, [
|
|
108
|
+
}, [contextEntityRef, closeContextMenu]);
|
|
103
109
|
|
|
104
110
|
// Basket: + Add to basket
|
|
105
111
|
const handleAddToBasket = useCallback(() => {
|
|
106
|
-
|
|
107
|
-
addToBasket([{ modelId: resolvedModelId, expressId: resolvedExpressId }]);
|
|
108
|
-
}
|
|
112
|
+
executeBasketAdd(contextEntityRef);
|
|
109
113
|
closeContextMenu();
|
|
110
|
-
}, [
|
|
114
|
+
}, [contextEntityRef, closeContextMenu]);
|
|
111
115
|
|
|
112
116
|
// Basket: − Remove from basket
|
|
113
117
|
const handleRemoveFromBasket = useCallback(() => {
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
executeBasketRemove(contextEntityRef);
|
|
119
|
+
closeContextMenu();
|
|
120
|
+
}, [contextEntityRef, closeContextMenu]);
|
|
121
|
+
|
|
122
|
+
const handleSaveBasketView = useCallback(() => {
|
|
123
|
+
const state = useViewerStore.getState();
|
|
124
|
+
if (state.pinboardEntities.size === 0) {
|
|
125
|
+
closeContextMenu();
|
|
126
|
+
return;
|
|
116
127
|
}
|
|
128
|
+
executeBasketSaveView().catch((err) => {
|
|
129
|
+
console.error('[EntityContextMenu] Failed to save basket view:', err);
|
|
130
|
+
});
|
|
117
131
|
closeContextMenu();
|
|
118
|
-
}, [
|
|
132
|
+
}, [closeContextMenu]);
|
|
119
133
|
|
|
120
134
|
const handleHide = useCallback(() => {
|
|
121
135
|
if (contextMenu.entityId) {
|
|
@@ -125,9 +139,9 @@ export function EntityContextMenu() {
|
|
|
125
139
|
}, [contextMenu.entityId, hideEntity, closeContextMenu]);
|
|
126
140
|
|
|
127
141
|
const handleShowAll = useCallback(() => {
|
|
128
|
-
|
|
142
|
+
resetVisibilityForHomeFromStore();
|
|
129
143
|
closeContextMenu();
|
|
130
|
-
}, [
|
|
144
|
+
}, [closeContextMenu]);
|
|
131
145
|
|
|
132
146
|
const handleSelectSimilar = useCallback(() => {
|
|
133
147
|
// Use resolvedExpressId (original ID) for IfcDataStore lookups
|
|
@@ -230,9 +244,10 @@ export function EntityContextMenu() {
|
|
|
230
244
|
<div className="h-px bg-border my-1" />
|
|
231
245
|
|
|
232
246
|
{/* Basket operations */}
|
|
233
|
-
<MenuItem icon={Equal} label="Set
|
|
247
|
+
<MenuItem icon={Equal} label="Set Basket (=)" onClick={handleSetBasket} />
|
|
234
248
|
<MenuItem icon={Plus} label="Add to Basket (+)" onClick={handleAddToBasket} />
|
|
235
249
|
<MenuItem icon={Minus} label="Remove from Basket (−)" onClick={handleRemoveFromBasket} />
|
|
250
|
+
<MenuItem icon={Save} label="Save Basket View (B)" onClick={handleSaveBasketView} />
|
|
236
251
|
|
|
237
252
|
<div className="h-px bg-border my-1" />
|
|
238
253
|
|
|
@@ -36,6 +36,7 @@ export function HierarchyPanel() {
|
|
|
36
36
|
} = useIfc();
|
|
37
37
|
const selectedEntityId = useViewerStore((s) => s.selectedEntityId);
|
|
38
38
|
const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
|
|
39
|
+
const setSelectedEntityIds = useViewerStore((s) => s.setSelectedEntityIds);
|
|
39
40
|
const setSelectedEntity = useViewerStore((s) => s.setSelectedEntity);
|
|
40
41
|
const setSelectedEntities = useViewerStore((s) => s.setSelectedEntities);
|
|
41
42
|
const setSelectedModelId = useViewerStore((s) => s.setSelectedModelId);
|
|
@@ -44,6 +45,7 @@ export function HierarchyPanel() {
|
|
|
44
45
|
const setStoreysSelection = useViewerStore((s) => s.setStoreysSelection);
|
|
45
46
|
const clearStoreySelection = useViewerStore((s) => s.clearStoreySelection);
|
|
46
47
|
const isolateEntities = useViewerStore((s) => s.isolateEntities);
|
|
48
|
+
const setHierarchyBasketSelection = useViewerStore((s) => s.setHierarchyBasketSelection);
|
|
47
49
|
|
|
48
50
|
const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
|
|
49
51
|
const hideEntities = useViewerStore((s) => s.hideEntities);
|
|
@@ -180,10 +182,26 @@ export function HierarchyPanel() {
|
|
|
180
182
|
return;
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
const hierarchyRefs: Array<{ modelId: string; expressId: number }> = [];
|
|
186
|
+
for (const globalId of getNodeElements(node)) {
|
|
187
|
+
const ref = resolveEntityRef(globalId);
|
|
188
|
+
if (ref) hierarchyRefs.push(ref);
|
|
189
|
+
}
|
|
190
|
+
if (hierarchyRefs.length > 0) {
|
|
191
|
+
setHierarchyBasketSelection(hierarchyRefs);
|
|
192
|
+
} else if (isSpatialContainer(node.type) && node.expressIds.length > 0) {
|
|
193
|
+
setHierarchyBasketSelection([{
|
|
194
|
+
modelId: node.modelIds[0] || 'legacy',
|
|
195
|
+
expressId: node.expressIds[0],
|
|
196
|
+
}]);
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
// Type group nodes - click to isolate entities, expand via chevron only
|
|
184
200
|
if (node.type === 'type-group') {
|
|
185
201
|
const elements = getNodeElements(node);
|
|
186
202
|
if (elements.length > 0) {
|
|
203
|
+
setSelectedEntityIds(elements);
|
|
204
|
+
setSelectedEntity(resolveEntityRef(elements[0]));
|
|
187
205
|
isolateEntities(elements);
|
|
188
206
|
}
|
|
189
207
|
return;
|
|
@@ -284,7 +302,7 @@ export function HierarchyPanel() {
|
|
|
284
302
|
setSelectedEntity(resolveEntityRef(elementId));
|
|
285
303
|
}
|
|
286
304
|
}
|
|
287
|
-
}, [selectedStoreys, setStoreysSelection, clearStoreySelection, setSelectedEntityId, setSelectedEntity, setSelectedEntities, setActiveModel, toggleExpand, unifiedStoreys, models, isolateEntities, getNodeElements]);
|
|
305
|
+
}, [selectedStoreys, setStoreysSelection, clearStoreySelection, setSelectedEntityId, setSelectedEntityIds, setSelectedEntity, setSelectedEntities, setActiveModel, toggleExpand, unifiedStoreys, models, isolateEntities, getNodeElements, setHierarchyBasketSelection]);
|
|
288
306
|
|
|
289
307
|
// Compute selection and visibility state for a node
|
|
290
308
|
const computeNodeState = useCallback((node: TreeNode): { isSelected: boolean; nodeHidden: boolean; modelVisible?: boolean } => {
|
|
@@ -24,8 +24,6 @@ import {
|
|
|
24
24
|
ArrowLeft,
|
|
25
25
|
ArrowRight,
|
|
26
26
|
Box,
|
|
27
|
-
Sun,
|
|
28
|
-
Moon,
|
|
29
27
|
HelpCircle,
|
|
30
28
|
Loader2,
|
|
31
29
|
Camera,
|
|
@@ -34,11 +32,11 @@ import {
|
|
|
34
32
|
SquareX,
|
|
35
33
|
Building2,
|
|
36
34
|
Plus,
|
|
37
|
-
Minus,
|
|
38
35
|
MessageSquare,
|
|
39
36
|
ClipboardCheck,
|
|
40
37
|
Palette,
|
|
41
38
|
Orbit,
|
|
39
|
+
LayoutTemplate,
|
|
42
40
|
} from 'lucide-react';
|
|
43
41
|
import { Button } from '@/components/ui/button';
|
|
44
42
|
import { Separator } from '@/components/ui/separator';
|
|
@@ -55,8 +53,9 @@ import {
|
|
|
55
53
|
DropdownMenuSubContent,
|
|
56
54
|
} from '@/components/ui/dropdown-menu';
|
|
57
55
|
import { Progress } from '@/components/ui/progress';
|
|
58
|
-
import { useViewerStore, isIfcxDataStore
|
|
59
|
-
import
|
|
56
|
+
import { useViewerStore, isIfcxDataStore } from '@/store';
|
|
57
|
+
import { goHomeFromStore, resetVisibilityForHomeFromStore } from '@/store/homeView';
|
|
58
|
+
import { executeBasketIsolate } from '@/store/basket/basketCommands';
|
|
60
59
|
import { useIfc } from '@/hooks/useIfc';
|
|
61
60
|
import { cn } from '@/lib/utils';
|
|
62
61
|
import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
|
|
@@ -67,6 +66,7 @@ import { DataConnector } from './DataConnector';
|
|
|
67
66
|
import { ExportChangesButton } from './ExportChangesButton';
|
|
68
67
|
import { useFloorplanView } from '@/hooks/useFloorplanView';
|
|
69
68
|
import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
|
|
69
|
+
import { ThemeSwitch } from './ThemeSwitch';
|
|
70
70
|
|
|
71
71
|
type Tool = 'select' | 'pan' | 'orbit' | 'walk' | 'measure' | 'section';
|
|
72
72
|
|
|
@@ -165,12 +165,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
165
165
|
const hasModelsLoaded = models.size > 0 || (geometryResult?.meshes && geometryResult.meshes.length > 0);
|
|
166
166
|
const activeTool = useViewerStore((state) => state.activeTool);
|
|
167
167
|
const setActiveTool = useViewerStore((state) => state.setActiveTool);
|
|
168
|
-
const theme = useViewerStore((state) => state.theme);
|
|
169
|
-
const toggleTheme = useViewerStore((state) => state.toggleTheme);
|
|
170
168
|
const selectedEntityId = useViewerStore((state) => state.selectedEntityId);
|
|
171
169
|
const hideEntities = useViewerStore((state) => state.hideEntities);
|
|
172
|
-
const showAll = useViewerStore((state) => state.showAll);
|
|
173
|
-
const clearStoreySelection = useViewerStore((state) => state.clearStoreySelection);
|
|
174
170
|
const error = useViewerStore((state) => state.error);
|
|
175
171
|
const cameraCallbacks = useViewerStore((state) => state.cameraCallbacks);
|
|
176
172
|
const hoverTooltipsEnabled = useViewerStore((state) => state.hoverTooltipsEnabled);
|
|
@@ -189,14 +185,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
189
185
|
const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
|
|
190
186
|
const projectionMode = useViewerStore((state) => state.projectionMode);
|
|
191
187
|
const toggleProjectionMode = useViewerStore((state) => state.toggleProjectionMode);
|
|
192
|
-
// Basket state
|
|
188
|
+
// Basket presentation state
|
|
193
189
|
const pinboardEntities = useViewerStore((state) => state.pinboardEntities);
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const clearBasket = useViewerStore((state) => state.clearBasket);
|
|
198
|
-
// NOTE: selectedEntity and selectedEntitiesSet accessed via getState() in callbacks
|
|
199
|
-
// to avoid re-rendering MainToolbar on every Cmd+Click selection change.
|
|
190
|
+
const basketViewCount = useViewerStore((state) => state.basketViews.length);
|
|
191
|
+
const basketPresentationVisible = useViewerStore((state) => state.basketPresentationVisible);
|
|
192
|
+
const toggleBasketPresentationVisible = useViewerStore((state) => state.toggleBasketPresentationVisible);
|
|
200
193
|
// Lens state
|
|
201
194
|
const lensPanelVisible = useViewerStore((state) => state.lensPanelVisible);
|
|
202
195
|
const toggleLensPanel = useViewerStore((state) => state.toggleLensPanel);
|
|
@@ -306,68 +299,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
306
299
|
e.target.value = '';
|
|
307
300
|
}, [loadFilesSequentially, addIfcxOverlays, ifcDataStore]);
|
|
308
301
|
|
|
309
|
-
/** Get current selection as EntityRef[] — uses getState() to avoid reactive subscriptions */
|
|
310
|
-
const getSelectionRefs = useCallback((): EntityRef[] => {
|
|
311
|
-
const state = useViewerStore.getState();
|
|
312
|
-
if (state.selectedEntitiesSet.size > 0) {
|
|
313
|
-
const refs: EntityRef[] = [];
|
|
314
|
-
for (const str of state.selectedEntitiesSet) {
|
|
315
|
-
refs.push(stringToEntityRef(str));
|
|
316
|
-
}
|
|
317
|
-
return refs;
|
|
318
|
-
}
|
|
319
|
-
if (state.selectedEntity) {
|
|
320
|
-
return [state.selectedEntity];
|
|
321
|
-
}
|
|
322
|
-
return [];
|
|
323
|
-
}, []);
|
|
324
|
-
|
|
325
302
|
const hasSelection = selectedEntityId !== null;
|
|
326
303
|
|
|
327
|
-
// Basket state
|
|
328
|
-
const showPinboard = useViewerStore((state) => state.showPinboard);
|
|
329
|
-
|
|
330
|
-
// Clear multi-select state after basket operations so subsequent − targets a single entity
|
|
331
|
-
const clearMultiSelect = useCallback(() => {
|
|
332
|
-
const state = useViewerStore.getState();
|
|
333
|
-
if (state.selectedEntitiesSet.size > 0) {
|
|
334
|
-
useViewerStore.setState({ selectedEntitiesSet: new Set(), selectedEntityIds: new Set() });
|
|
335
|
-
}
|
|
336
|
-
}, []);
|
|
337
|
-
|
|
338
|
-
// Basket operations
|
|
339
|
-
const handleSetBasket = useCallback(() => {
|
|
340
|
-
const state = useViewerStore.getState();
|
|
341
|
-
// If basket already exists and user hasn't explicitly multi-selected,
|
|
342
|
-
// re-apply the basket instead of replacing it with a stale single selection.
|
|
343
|
-
// Only an explicit multi-selection (Ctrl+Click) should replace an existing basket.
|
|
344
|
-
if (state.pinboardEntities.size > 0 && state.selectedEntitiesSet.size === 0) {
|
|
345
|
-
showPinboard();
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const refs = getSelectionRefs();
|
|
349
|
-
if (refs.length > 0) {
|
|
350
|
-
setBasket(refs);
|
|
351
|
-
clearMultiSelect();
|
|
352
|
-
}
|
|
353
|
-
}, [getSelectionRefs, setBasket, showPinboard, clearMultiSelect]);
|
|
354
|
-
|
|
355
|
-
const handleAddToBasket = useCallback(() => {
|
|
356
|
-
const refs = getSelectionRefs();
|
|
357
|
-
if (refs.length > 0) {
|
|
358
|
-
addToBasket(refs);
|
|
359
|
-
clearMultiSelect();
|
|
360
|
-
}
|
|
361
|
-
}, [getSelectionRefs, addToBasket, clearMultiSelect]);
|
|
362
|
-
|
|
363
|
-
const handleRemoveFromBasket = useCallback(() => {
|
|
364
|
-
const refs = getSelectionRefs();
|
|
365
|
-
if (refs.length > 0) {
|
|
366
|
-
removeFromBasket(refs);
|
|
367
|
-
clearMultiSelect();
|
|
368
|
-
}
|
|
369
|
-
}, [getSelectionRefs, removeFromBasket, clearMultiSelect]);
|
|
370
|
-
|
|
371
304
|
const clearSelection = useViewerStore((state) => state.clearSelection);
|
|
372
305
|
|
|
373
306
|
const handleHide = useCallback(() => {
|
|
@@ -383,9 +316,16 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
383
316
|
}, [selectedEntityId, hideEntities, clearSelection]);
|
|
384
317
|
|
|
385
318
|
const handleShowAll = useCallback(() => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
319
|
+
resetVisibilityForHomeFromStore();
|
|
320
|
+
}, []);
|
|
321
|
+
|
|
322
|
+
const handleIsolate = useCallback(() => {
|
|
323
|
+
executeBasketIsolate();
|
|
324
|
+
}, []);
|
|
325
|
+
|
|
326
|
+
const handleHome = useCallback(() => {
|
|
327
|
+
goHomeFromStore();
|
|
328
|
+
}, []);
|
|
389
329
|
|
|
390
330
|
const handleExportGLB = useCallback(() => {
|
|
391
331
|
if (!geometryResult) return;
|
|
@@ -759,36 +699,35 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
759
699
|
|
|
760
700
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
761
701
|
|
|
762
|
-
{/* ── Basket
|
|
702
|
+
{/* ── Basket Presentation ── */}
|
|
763
703
|
<Tooltip>
|
|
764
704
|
<TooltipTrigger asChild>
|
|
765
705
|
<Button
|
|
766
|
-
variant={
|
|
706
|
+
variant={basketPresentationVisible ? 'default' : 'ghost'}
|
|
767
707
|
size="icon-sm"
|
|
768
708
|
onClick={(e) => {
|
|
769
709
|
(e.currentTarget as HTMLButtonElement).blur();
|
|
770
|
-
|
|
710
|
+
toggleBasketPresentationVisible();
|
|
771
711
|
}}
|
|
772
|
-
disabled={
|
|
712
|
+
disabled={models.size === 0 && !geometryResult}
|
|
773
713
|
className={cn(
|
|
774
|
-
pinboardEntities.size > 0 && '
|
|
714
|
+
(basketPresentationVisible || pinboardEntities.size > 0) && 'relative',
|
|
775
715
|
)}
|
|
776
716
|
>
|
|
777
|
-
<
|
|
778
|
-
{pinboardEntities.size > 0 && (
|
|
717
|
+
<LayoutTemplate className="h-4 w-4" />
|
|
718
|
+
{(basketViewCount > 0 || pinboardEntities.size > 0) && (
|
|
779
719
|
<span className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-[9px] font-bold rounded-full min-w-[14px] h-[14px] flex items-center justify-center px-0.5 border border-background">
|
|
780
|
-
{pinboardEntities.size}
|
|
720
|
+
{basketViewCount > 0 ? `${basketViewCount}/${pinboardEntities.size}` : pinboardEntities.size}
|
|
781
721
|
</span>
|
|
782
722
|
)}
|
|
783
723
|
</Button>
|
|
784
724
|
</TooltipTrigger>
|
|
785
725
|
<TooltipContent>
|
|
786
|
-
|
|
726
|
+
Basket Presentation Dock (Views: {basketViewCount}, Entities: {pinboardEntities.size})
|
|
787
727
|
</TooltipContent>
|
|
788
728
|
</Tooltip>
|
|
789
|
-
<ActionButton icon={Plus} label="Add to Basket" onClick={handleAddToBasket} shortcut="+" disabled={!hasSelection} />
|
|
790
|
-
<ActionButton icon={Minus} label="Remove from Basket" onClick={handleRemoveFromBasket} shortcut="−" disabled={!hasSelection} />
|
|
791
729
|
|
|
730
|
+
<ActionButton icon={Equal} label="Isolate (Set Basket)" onClick={handleIsolate} shortcut="I / =" />
|
|
792
731
|
<ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del / Space" disabled={!hasSelection} />
|
|
793
732
|
<ActionButton icon={Eye} label="Show All (Reset Filters)" onClick={handleShowAll} shortcut="A" />
|
|
794
733
|
<ActionButton icon={Maximize2} label="Fit All" onClick={() => cameraCallbacks.fitAll?.()} shortcut="Z" />
|
|
@@ -869,7 +808,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
869
808
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
870
809
|
|
|
871
810
|
{/* ── Camera & View ── */}
|
|
872
|
-
<ActionButton icon={Home} label="Home (Isometric)" onClick={
|
|
811
|
+
<ActionButton icon={Home} label="Home (Isometric + Reset Visibility)" onClick={handleHome} shortcut="H" />
|
|
873
812
|
|
|
874
813
|
{/* Orthographic / Perspective toggle */}
|
|
875
814
|
<Tooltip>
|
|
@@ -924,8 +863,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
924
863
|
<TooltipContent>Preset Views</TooltipContent>
|
|
925
864
|
</Tooltip>
|
|
926
865
|
<DropdownMenuContent>
|
|
927
|
-
<DropdownMenuItem onClick={
|
|
928
|
-
<Box className="h-4 w-4 mr-2" /> Isometric <span className="ml-auto text-xs opacity-60">
|
|
866
|
+
<DropdownMenuItem onClick={handleHome}>
|
|
867
|
+
<Box className="h-4 w-4 mr-2" /> Isometric <span className="ml-auto text-xs opacity-60">H</span>
|
|
929
868
|
</DropdownMenuItem>
|
|
930
869
|
<DropdownMenuSeparator />
|
|
931
870
|
<DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('top')}>
|
|
@@ -967,27 +906,31 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
967
906
|
)}
|
|
968
907
|
|
|
969
908
|
{/* Right Side Actions */}
|
|
970
|
-
<
|
|
971
|
-
<
|
|
972
|
-
<
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
909
|
+
<div className="flex items-center gap-2 ml-2 pl-2 border-l border-zinc-200 dark:border-zinc-700/60">
|
|
910
|
+
<Tooltip>
|
|
911
|
+
<TooltipTrigger asChild>
|
|
912
|
+
<div>
|
|
913
|
+
<ThemeSwitch />
|
|
914
|
+
</div>
|
|
915
|
+
</TooltipTrigger>
|
|
916
|
+
<TooltipContent>Toggle theme</TooltipContent>
|
|
917
|
+
</Tooltip>
|
|
918
|
+
|
|
919
|
+
<Tooltip>
|
|
920
|
+
<TooltipTrigger asChild>
|
|
921
|
+
<Button
|
|
922
|
+
variant="ghost"
|
|
923
|
+
size="icon"
|
|
924
|
+
className="rounded-full"
|
|
925
|
+
onClick={() => onShowShortcuts?.()}
|
|
926
|
+
>
|
|
927
|
+
<HelpCircle className="!h-[22px] !w-[22px]" />
|
|
928
|
+
</Button>
|
|
929
|
+
</TooltipTrigger>
|
|
930
|
+
<TooltipContent>Info (?)</TooltipContent>
|
|
931
|
+
</Tooltip>
|
|
932
|
+
</div>
|
|
978
933
|
|
|
979
|
-
<Tooltip>
|
|
980
|
-
<TooltipTrigger asChild>
|
|
981
|
-
<Button
|
|
982
|
-
variant="ghost"
|
|
983
|
-
size="icon-sm"
|
|
984
|
-
onClick={() => onShowShortcuts?.()}
|
|
985
|
-
>
|
|
986
|
-
<HelpCircle className="h-4 w-4" />
|
|
987
|
-
</Button>
|
|
988
|
-
</TooltipTrigger>
|
|
989
|
-
<TooltipContent>Info (?)</TooltipContent>
|
|
990
|
-
</Tooltip>
|
|
991
934
|
</div>
|
|
992
935
|
);
|
|
993
936
|
}
|
|
@@ -52,6 +52,8 @@ export function Section2DPanel({
|
|
|
52
52
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
53
53
|
const panelVisible = useViewerStore((s) => s.drawing2DPanelVisible);
|
|
54
54
|
const setDrawingPanelVisible = useViewerStore((s) => s.setDrawing2DPanelVisible);
|
|
55
|
+
const suppressNextSection2DPanelAutoOpen = useViewerStore((s) => s.suppressNextSection2DPanelAutoOpen);
|
|
56
|
+
const setSuppressNextSection2DPanelAutoOpen = useViewerStore((s) => s.setSuppressNextSection2DPanelAutoOpen);
|
|
55
57
|
const drawing = useViewerStore((s) => s.drawing2D);
|
|
56
58
|
const setDrawing = useViewerStore((s) => s.setDrawing2D);
|
|
57
59
|
const status = useViewerStore((s) => s.drawing2DStatus);
|
|
@@ -148,10 +150,15 @@ export function Section2DPanel({
|
|
|
148
150
|
useEffect(() => {
|
|
149
151
|
// Section tool was just activated
|
|
150
152
|
if (activeTool === 'section' && prevActiveToolRef.current !== 'section' && geometryResult?.meshes) {
|
|
153
|
+
if (suppressNextSection2DPanelAutoOpen) {
|
|
154
|
+
setSuppressNextSection2DPanelAutoOpen(false);
|
|
155
|
+
prevActiveToolRef.current = activeTool;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
151
158
|
setDrawingPanelVisible(true);
|
|
152
159
|
}
|
|
153
160
|
prevActiveToolRef.current = activeTool;
|
|
154
|
-
}, [activeTool, geometryResult, setDrawingPanelVisible]);
|
|
161
|
+
}, [activeTool, geometryResult, setDrawingPanelVisible, suppressNextSection2DPanelAutoOpen, setSuppressNextSection2DPanelAutoOpen]);
|
|
155
162
|
|
|
156
163
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
157
164
|
// LOCAL STATE
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { useEffect, useRef } from 'react';
|
|
6
|
+
import { ThemeToggle } from 'beautiful-theme-toggle';
|
|
7
|
+
import { useViewerStore } from '@/store';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Animated SVG theme toggle (sun/moon) powered by beautiful-theme-toggle.
|
|
11
|
+
*
|
|
12
|
+
* Bidirectional sync:
|
|
13
|
+
* - User clicks the widget → onChange → store.setTheme
|
|
14
|
+
* - External change (keyboard shortcut / command palette) → store updates → widget.setTheme
|
|
15
|
+
*/
|
|
16
|
+
export function ThemeSwitch() {
|
|
17
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const toggleRef = useRef<ThemeToggle | null>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!containerRef.current) return;
|
|
22
|
+
|
|
23
|
+
const currentTheme = useViewerStore.getState().theme;
|
|
24
|
+
|
|
25
|
+
const toggle = new ThemeToggle({
|
|
26
|
+
element: containerRef.current,
|
|
27
|
+
size: 80,
|
|
28
|
+
initialState: currentTheme,
|
|
29
|
+
onChange: (state) => {
|
|
30
|
+
useViewerStore.getState().setTheme(state);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
toggleRef.current = toggle;
|
|
35
|
+
|
|
36
|
+
// Subscribe to external theme changes so the widget stays in sync
|
|
37
|
+
let prevTheme = currentTheme;
|
|
38
|
+
const unsub = useViewerStore.subscribe((state) => {
|
|
39
|
+
if (state.theme !== prevTheme) {
|
|
40
|
+
prevTheme = state.theme;
|
|
41
|
+
if (toggleRef.current && toggleRef.current.getTheme() !== state.theme) {
|
|
42
|
+
toggleRef.current.setTheme(state.theme, false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
unsub();
|
|
49
|
+
toggle.destroy();
|
|
50
|
+
toggleRef.current = null;
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
return <div ref={containerRef} className="flex items-center cursor-pointer opacity-80 hover:opacity-100 transition-opacity" />;
|
|
55
|
+
}
|