@ifc-lite/viewer 1.9.0 → 1.11.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.
- package/CHANGELOG.md +48 -0
- package/dist/assets/{Arrow.dom-CusgkT03.js → Arrow.dom-IIkrrCZ0.js} +1 -1
- package/dist/assets/{browser-BXNIkE8a.js → browser-BoonPy8d.js} +1 -1
- package/dist/assets/ifc-lite_bg-B6s-pcv0.wasm +0 -0
- package/dist/assets/{index-huvR-kGC.js → index-CQkEOlYf.js} +49090 -46453
- package/dist/assets/{index-6Mr3byM-.js → index-ClZCG7KA.js} +4 -4
- package/dist/assets/index-qxIHWl_B.css +1 -0
- package/dist/assets/{native-bridge-DsHOKdgD.js → native-bridge-Beg4Kf9O.js} +1 -1
- package/dist/assets/{wasm-bridge-Bd73HXn-.js → wasm-bridge-CY8jkr7u.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +19 -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 +32 -89
- package/src/components/viewer/Section2DPanel.tsx +8 -1
- package/src/components/viewer/Viewport.tsx +107 -98
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/ViewportOverlays.tsx +9 -3
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +3 -1
- package/src/components/viewer/useAnimationLoop.ts +4 -1
- package/src/components/viewer/useKeyboardControls.ts +2 -2
- package/src/components/viewer/useRenderUpdates.ts +16 -4
- package/src/hooks/useKeyboardShortcuts.ts +51 -84
- package/src/hooks/useViewerSelectors.ts +22 -0
- package/src/index.css +6 -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/constants.ts +20 -0
- package/src/store/homeView.ts +21 -0
- package/src/store/index.ts +17 -0
- 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/slices/uiSlice.ts +41 -0
- package/src/store/types.ts +11 -0
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/index-CGbokkQ9.css +0 -1
|
@@ -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 } => {
|
|
@@ -34,11 +34,11 @@ import {
|
|
|
34
34
|
SquareX,
|
|
35
35
|
Building2,
|
|
36
36
|
Plus,
|
|
37
|
-
Minus,
|
|
38
37
|
MessageSquare,
|
|
39
38
|
ClipboardCheck,
|
|
40
39
|
Palette,
|
|
41
40
|
Orbit,
|
|
41
|
+
LayoutTemplate,
|
|
42
42
|
} from 'lucide-react';
|
|
43
43
|
import { Button } from '@/components/ui/button';
|
|
44
44
|
import { Separator } from '@/components/ui/separator';
|
|
@@ -55,8 +55,9 @@ import {
|
|
|
55
55
|
DropdownMenuSubContent,
|
|
56
56
|
} from '@/components/ui/dropdown-menu';
|
|
57
57
|
import { Progress } from '@/components/ui/progress';
|
|
58
|
-
import { useViewerStore, isIfcxDataStore
|
|
59
|
-
import
|
|
58
|
+
import { useViewerStore, isIfcxDataStore } from '@/store';
|
|
59
|
+
import { goHomeFromStore, resetVisibilityForHomeFromStore } from '@/store/homeView';
|
|
60
|
+
import { executeBasketIsolate } from '@/store/basket/basketCommands';
|
|
60
61
|
import { useIfc } from '@/hooks/useIfc';
|
|
61
62
|
import { cn } from '@/lib/utils';
|
|
62
63
|
import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
|
|
@@ -169,8 +170,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
169
170
|
const toggleTheme = useViewerStore((state) => state.toggleTheme);
|
|
170
171
|
const selectedEntityId = useViewerStore((state) => state.selectedEntityId);
|
|
171
172
|
const hideEntities = useViewerStore((state) => state.hideEntities);
|
|
172
|
-
const showAll = useViewerStore((state) => state.showAll);
|
|
173
|
-
const clearStoreySelection = useViewerStore((state) => state.clearStoreySelection);
|
|
174
173
|
const error = useViewerStore((state) => state.error);
|
|
175
174
|
const cameraCallbacks = useViewerStore((state) => state.cameraCallbacks);
|
|
176
175
|
const hoverTooltipsEnabled = useViewerStore((state) => state.hoverTooltipsEnabled);
|
|
@@ -189,14 +188,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
189
188
|
const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
|
|
190
189
|
const projectionMode = useViewerStore((state) => state.projectionMode);
|
|
191
190
|
const toggleProjectionMode = useViewerStore((state) => state.toggleProjectionMode);
|
|
192
|
-
// Basket state
|
|
191
|
+
// Basket presentation state
|
|
193
192
|
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.
|
|
193
|
+
const basketViewCount = useViewerStore((state) => state.basketViews.length);
|
|
194
|
+
const basketPresentationVisible = useViewerStore((state) => state.basketPresentationVisible);
|
|
195
|
+
const toggleBasketPresentationVisible = useViewerStore((state) => state.toggleBasketPresentationVisible);
|
|
200
196
|
// Lens state
|
|
201
197
|
const lensPanelVisible = useViewerStore((state) => state.lensPanelVisible);
|
|
202
198
|
const toggleLensPanel = useViewerStore((state) => state.toggleLensPanel);
|
|
@@ -306,68 +302,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
306
302
|
e.target.value = '';
|
|
307
303
|
}, [loadFilesSequentially, addIfcxOverlays, ifcDataStore]);
|
|
308
304
|
|
|
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
305
|
const hasSelection = selectedEntityId !== null;
|
|
326
306
|
|
|
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
307
|
const clearSelection = useViewerStore((state) => state.clearSelection);
|
|
372
308
|
|
|
373
309
|
const handleHide = useCallback(() => {
|
|
@@ -383,9 +319,16 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
383
319
|
}, [selectedEntityId, hideEntities, clearSelection]);
|
|
384
320
|
|
|
385
321
|
const handleShowAll = useCallback(() => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
322
|
+
resetVisibilityForHomeFromStore();
|
|
323
|
+
}, []);
|
|
324
|
+
|
|
325
|
+
const handleIsolate = useCallback(() => {
|
|
326
|
+
executeBasketIsolate();
|
|
327
|
+
}, []);
|
|
328
|
+
|
|
329
|
+
const handleHome = useCallback(() => {
|
|
330
|
+
goHomeFromStore();
|
|
331
|
+
}, []);
|
|
389
332
|
|
|
390
333
|
const handleExportGLB = useCallback(() => {
|
|
391
334
|
if (!geometryResult) return;
|
|
@@ -759,36 +702,35 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
759
702
|
|
|
760
703
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
761
704
|
|
|
762
|
-
{/* ── Basket
|
|
705
|
+
{/* ── Basket Presentation ── */}
|
|
763
706
|
<Tooltip>
|
|
764
707
|
<TooltipTrigger asChild>
|
|
765
708
|
<Button
|
|
766
|
-
variant={
|
|
709
|
+
variant={basketPresentationVisible ? 'default' : 'ghost'}
|
|
767
710
|
size="icon-sm"
|
|
768
711
|
onClick={(e) => {
|
|
769
712
|
(e.currentTarget as HTMLButtonElement).blur();
|
|
770
|
-
|
|
713
|
+
toggleBasketPresentationVisible();
|
|
771
714
|
}}
|
|
772
|
-
disabled={
|
|
715
|
+
disabled={models.size === 0 && !geometryResult}
|
|
773
716
|
className={cn(
|
|
774
|
-
pinboardEntities.size > 0 && '
|
|
717
|
+
(basketPresentationVisible || pinboardEntities.size > 0) && 'relative',
|
|
775
718
|
)}
|
|
776
719
|
>
|
|
777
|
-
<
|
|
778
|
-
{pinboardEntities.size > 0 && (
|
|
720
|
+
<LayoutTemplate className="h-4 w-4" />
|
|
721
|
+
{(basketViewCount > 0 || pinboardEntities.size > 0) && (
|
|
779
722
|
<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}
|
|
723
|
+
{basketViewCount > 0 ? `${basketViewCount}/${pinboardEntities.size}` : pinboardEntities.size}
|
|
781
724
|
</span>
|
|
782
725
|
)}
|
|
783
726
|
</Button>
|
|
784
727
|
</TooltipTrigger>
|
|
785
728
|
<TooltipContent>
|
|
786
|
-
|
|
729
|
+
Basket Presentation Dock (Views: {basketViewCount}, Entities: {pinboardEntities.size})
|
|
787
730
|
</TooltipContent>
|
|
788
731
|
</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
732
|
|
|
733
|
+
<ActionButton icon={Equal} label="Isolate (Set Basket)" onClick={handleIsolate} shortcut="I / =" />
|
|
792
734
|
<ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del / Space" disabled={!hasSelection} />
|
|
793
735
|
<ActionButton icon={Eye} label="Show All (Reset Filters)" onClick={handleShowAll} shortcut="A" />
|
|
794
736
|
<ActionButton icon={Maximize2} label="Fit All" onClick={() => cameraCallbacks.fitAll?.()} shortcut="Z" />
|
|
@@ -869,7 +811,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
869
811
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
870
812
|
|
|
871
813
|
{/* ── Camera & View ── */}
|
|
872
|
-
<ActionButton icon={Home} label="Home (Isometric)" onClick={
|
|
814
|
+
<ActionButton icon={Home} label="Home (Isometric + Reset Visibility)" onClick={handleHome} shortcut="H" />
|
|
873
815
|
|
|
874
816
|
{/* Orthographic / Perspective toggle */}
|
|
875
817
|
<Tooltip>
|
|
@@ -924,8 +866,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
924
866
|
<TooltipContent>Preset Views</TooltipContent>
|
|
925
867
|
</Tooltip>
|
|
926
868
|
<DropdownMenuContent>
|
|
927
|
-
<DropdownMenuItem onClick={
|
|
928
|
-
<Box className="h-4 w-4 mr-2" /> Isometric <span className="ml-auto text-xs opacity-60">
|
|
869
|
+
<DropdownMenuItem onClick={handleHome}>
|
|
870
|
+
<Box className="h-4 w-4 mr-2" /> Isometric <span className="ml-auto text-xs opacity-60">H</span>
|
|
929
871
|
</DropdownMenuItem>
|
|
930
872
|
<DropdownMenuSeparator />
|
|
931
873
|
<DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('top')}>
|
|
@@ -988,6 +930,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
988
930
|
</TooltipTrigger>
|
|
989
931
|
<TooltipContent>Info (?)</TooltipContent>
|
|
990
932
|
</Tooltip>
|
|
933
|
+
|
|
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
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
10
|
-
import { Renderer } from '@ifc-lite/renderer';
|
|
10
|
+
import { Renderer, type VisualEnhancementOptions } from '@ifc-lite/renderer';
|
|
11
11
|
import type { MeshData, CoordinateInfo } from '@ifc-lite/geometry';
|
|
12
12
|
import { useViewerStore, resolveEntityRef, type MeasurePoint, type SnapVisualization } from '@/store';
|
|
13
13
|
import {
|
|
@@ -158,7 +158,20 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
158
158
|
const { updateCameraRotationRealtime, updateScaleRealtime, setCameraCallbacks } = useCameraState();
|
|
159
159
|
|
|
160
160
|
// Theme state
|
|
161
|
-
const {
|
|
161
|
+
const {
|
|
162
|
+
theme,
|
|
163
|
+
isMobile,
|
|
164
|
+
visualEnhancementsEnabled,
|
|
165
|
+
edgeContrastEnabled,
|
|
166
|
+
edgeContrastIntensity,
|
|
167
|
+
contactShadingQuality,
|
|
168
|
+
contactShadingIntensity,
|
|
169
|
+
contactShadingRadius,
|
|
170
|
+
separationLinesEnabled,
|
|
171
|
+
separationLinesQuality,
|
|
172
|
+
separationLinesIntensity,
|
|
173
|
+
separationLinesRadius,
|
|
174
|
+
} = useThemeState();
|
|
162
175
|
|
|
163
176
|
// Hover state
|
|
164
177
|
const { hoverTooltipsEnabled, setHoverState, clearHover } = useHoverState();
|
|
@@ -215,6 +228,37 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
215
228
|
// Theme-aware clear color ref (updated when theme changes)
|
|
216
229
|
// Tokyo Night storm: #1a1b26 = rgb(26, 27, 38)
|
|
217
230
|
const clearColorRef = useRef<[number, number, number, number]>([0.102, 0.106, 0.149, 1]);
|
|
231
|
+
const visualEnhancement = useMemo<VisualEnhancementOptions>(() => ({
|
|
232
|
+
enabled: visualEnhancementsEnabled,
|
|
233
|
+
edgeContrast: {
|
|
234
|
+
enabled: edgeContrastEnabled,
|
|
235
|
+
intensity: edgeContrastIntensity,
|
|
236
|
+
},
|
|
237
|
+
contactShading: {
|
|
238
|
+
quality: isMobile ? 'off' : contactShadingQuality,
|
|
239
|
+
intensity: contactShadingIntensity,
|
|
240
|
+
radius: contactShadingRadius,
|
|
241
|
+
},
|
|
242
|
+
separationLines: {
|
|
243
|
+
enabled: separationLinesEnabled,
|
|
244
|
+
quality: isMobile ? 'low' : separationLinesQuality,
|
|
245
|
+
intensity: isMobile ? Math.min(0.4, separationLinesIntensity) : separationLinesIntensity,
|
|
246
|
+
radius: isMobile ? 1.0 : separationLinesRadius,
|
|
247
|
+
},
|
|
248
|
+
}), [
|
|
249
|
+
visualEnhancementsEnabled,
|
|
250
|
+
edgeContrastEnabled,
|
|
251
|
+
edgeContrastIntensity,
|
|
252
|
+
isMobile,
|
|
253
|
+
contactShadingQuality,
|
|
254
|
+
contactShadingIntensity,
|
|
255
|
+
contactShadingRadius,
|
|
256
|
+
separationLinesEnabled,
|
|
257
|
+
separationLinesQuality,
|
|
258
|
+
separationLinesIntensity,
|
|
259
|
+
separationLinesRadius,
|
|
260
|
+
]);
|
|
261
|
+
const visualEnhancementRef = useRef<VisualEnhancementOptions>(visualEnhancement);
|
|
218
262
|
|
|
219
263
|
// Animation frame ref
|
|
220
264
|
const animationFrameRef = useRef<number | null>(null);
|
|
@@ -330,6 +374,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
330
374
|
useEffect(() => { measurementConstraintEdgeRef.current = measurementConstraintEdge; }, [measurementConstraintEdge]);
|
|
331
375
|
useEffect(() => { sectionPlaneRef.current = sectionPlane; }, [sectionPlane]);
|
|
332
376
|
useEffect(() => { sectionRangeRef.current = sectionRange; }, [sectionRange]);
|
|
377
|
+
useEffect(() => { visualEnhancementRef.current = visualEnhancement; }, [visualEnhancement]);
|
|
333
378
|
useEffect(() => {
|
|
334
379
|
geometryRef.current = geometry;
|
|
335
380
|
}, [geometry]);
|
|
@@ -445,6 +490,21 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
445
490
|
setIsInitialized(true);
|
|
446
491
|
|
|
447
492
|
const camera = renderer.getCamera();
|
|
493
|
+
const renderCurrent = () => {
|
|
494
|
+
renderer.render({
|
|
495
|
+
hiddenIds: hiddenEntitiesRef.current,
|
|
496
|
+
isolatedIds: isolatedEntitiesRef.current,
|
|
497
|
+
selectedId: selectedEntityIdRef.current,
|
|
498
|
+
selectedModelIndex: selectedModelIndexRef.current,
|
|
499
|
+
clearColor: clearColorRef.current,
|
|
500
|
+
visualEnhancement: visualEnhancementRef.current,
|
|
501
|
+
sectionPlane: activeToolRef.current === 'section' ? {
|
|
502
|
+
...sectionPlaneRef.current,
|
|
503
|
+
min: sectionRangeRef.current?.min,
|
|
504
|
+
max: sectionRangeRef.current?.max,
|
|
505
|
+
} : undefined,
|
|
506
|
+
});
|
|
507
|
+
};
|
|
448
508
|
|
|
449
509
|
// Register camera callbacks for ViewCube and other controls
|
|
450
510
|
setCameraCallbacks({
|
|
@@ -453,18 +513,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
453
513
|
const rotation = coordinateInfoRef.current?.buildingRotation;
|
|
454
514
|
camera.setPresetView(view, geometryBoundsRef.current, rotation);
|
|
455
515
|
// Initial render - animation loop will continue rendering during animation
|
|
456
|
-
|
|
457
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
458
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
459
|
-
selectedId: selectedEntityIdRef.current,
|
|
460
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
461
|
-
clearColor: clearColorRef.current,
|
|
462
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
463
|
-
...sectionPlaneRef.current,
|
|
464
|
-
min: sectionRangeRef.current?.min,
|
|
465
|
-
max: sectionRangeRef.current?.max,
|
|
466
|
-
} : undefined,
|
|
467
|
-
});
|
|
516
|
+
renderCurrent();
|
|
468
517
|
calculateScale();
|
|
469
518
|
},
|
|
470
519
|
fitAll: () => {
|
|
@@ -479,34 +528,12 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
479
528
|
},
|
|
480
529
|
zoomIn: () => {
|
|
481
530
|
camera.zoom(-50, false);
|
|
482
|
-
|
|
483
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
484
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
485
|
-
selectedId: selectedEntityIdRef.current,
|
|
486
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
487
|
-
clearColor: clearColorRef.current,
|
|
488
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
489
|
-
...sectionPlaneRef.current,
|
|
490
|
-
min: sectionRangeRef.current?.min,
|
|
491
|
-
max: sectionRangeRef.current?.max,
|
|
492
|
-
} : undefined,
|
|
493
|
-
});
|
|
531
|
+
renderCurrent();
|
|
494
532
|
calculateScale();
|
|
495
533
|
},
|
|
496
534
|
zoomOut: () => {
|
|
497
535
|
camera.zoom(50, false);
|
|
498
|
-
|
|
499
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
500
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
501
|
-
selectedId: selectedEntityIdRef.current,
|
|
502
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
503
|
-
clearColor: clearColorRef.current,
|
|
504
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
505
|
-
...sectionPlaneRef.current,
|
|
506
|
-
min: sectionRangeRef.current?.min,
|
|
507
|
-
max: sectionRangeRef.current?.max,
|
|
508
|
-
} : undefined,
|
|
509
|
-
});
|
|
536
|
+
renderCurrent();
|
|
510
537
|
calculateScale();
|
|
511
538
|
},
|
|
512
539
|
frameSelection: () => {
|
|
@@ -528,18 +555,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
528
555
|
orbit: (deltaX: number, deltaY: number) => {
|
|
529
556
|
// Orbit camera from ViewCube drag
|
|
530
557
|
camera.orbit(deltaX, deltaY, false);
|
|
531
|
-
|
|
532
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
533
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
534
|
-
selectedId: selectedEntityIdRef.current,
|
|
535
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
536
|
-
clearColor: clearColorRef.current,
|
|
537
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
538
|
-
...sectionPlaneRef.current,
|
|
539
|
-
min: sectionRangeRef.current?.min,
|
|
540
|
-
max: sectionRangeRef.current?.max,
|
|
541
|
-
} : undefined,
|
|
542
|
-
});
|
|
558
|
+
renderCurrent();
|
|
543
559
|
updateCameraRotationRealtime(camera.getRotation());
|
|
544
560
|
calculateScale();
|
|
545
561
|
},
|
|
@@ -551,37 +567,47 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
551
567
|
},
|
|
552
568
|
setProjectionMode: (mode) => {
|
|
553
569
|
camera.setProjectionMode(mode);
|
|
554
|
-
|
|
555
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
556
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
557
|
-
selectedId: selectedEntityIdRef.current,
|
|
558
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
559
|
-
clearColor: clearColorRef.current,
|
|
560
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
561
|
-
...sectionPlaneRef.current,
|
|
562
|
-
min: sectionRangeRef.current?.min,
|
|
563
|
-
max: sectionRangeRef.current?.max,
|
|
564
|
-
} : undefined,
|
|
565
|
-
});
|
|
570
|
+
renderCurrent();
|
|
566
571
|
calculateScale();
|
|
567
572
|
},
|
|
568
573
|
toggleProjectionMode: () => {
|
|
569
574
|
camera.toggleProjectionMode();
|
|
570
|
-
|
|
571
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
572
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
573
|
-
selectedId: selectedEntityIdRef.current,
|
|
574
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
575
|
-
clearColor: clearColorRef.current,
|
|
576
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
577
|
-
...sectionPlaneRef.current,
|
|
578
|
-
min: sectionRangeRef.current?.min,
|
|
579
|
-
max: sectionRangeRef.current?.max,
|
|
580
|
-
} : undefined,
|
|
581
|
-
});
|
|
575
|
+
renderCurrent();
|
|
582
576
|
calculateScale();
|
|
583
577
|
},
|
|
584
578
|
getProjectionMode: () => camera.getProjectionMode(),
|
|
579
|
+
getViewpoint: () => ({
|
|
580
|
+
position: camera.getPosition(),
|
|
581
|
+
target: camera.getTarget(),
|
|
582
|
+
up: camera.getUp(),
|
|
583
|
+
fov: camera.getFOV(),
|
|
584
|
+
projectionMode: camera.getProjectionMode(),
|
|
585
|
+
orthoSize: camera.getProjectionMode() === 'orthographic' ? camera.getOrthoSize() : undefined,
|
|
586
|
+
}),
|
|
587
|
+
applyViewpoint: (viewpoint, animate = true, durationMs = 300) => {
|
|
588
|
+
camera.setProjectionMode(viewpoint.projectionMode);
|
|
589
|
+
useViewerStore.setState({ projectionMode: viewpoint.projectionMode });
|
|
590
|
+
camera.setFOV(viewpoint.fov);
|
|
591
|
+
if (
|
|
592
|
+
viewpoint.projectionMode === 'orthographic' &&
|
|
593
|
+
typeof viewpoint.orthoSize === 'number' &&
|
|
594
|
+
Number.isFinite(viewpoint.orthoSize)
|
|
595
|
+
) {
|
|
596
|
+
camera.setOrthoSize(viewpoint.orthoSize);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (animate) {
|
|
600
|
+
camera.animateToWithUp(viewpoint.position, viewpoint.target, viewpoint.up, durationMs);
|
|
601
|
+
} else {
|
|
602
|
+
camera.setPosition(viewpoint.position.x, viewpoint.position.y, viewpoint.position.z);
|
|
603
|
+
camera.setTarget(viewpoint.target.x, viewpoint.target.y, viewpoint.target.z);
|
|
604
|
+
camera.setUp(viewpoint.up.x, viewpoint.up.y, viewpoint.up.z);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
renderCurrent();
|
|
608
|
+
updateCameraRotationRealtime(camera.getRotation());
|
|
609
|
+
calculateScale();
|
|
610
|
+
},
|
|
585
611
|
});
|
|
586
612
|
|
|
587
613
|
// ResizeObserver
|
|
@@ -592,34 +618,12 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
592
618
|
const w = alignToWebGPU(Math.max(1, Math.floor(rect.width)));
|
|
593
619
|
const h = Math.max(1, Math.floor(rect.height));
|
|
594
620
|
renderer.resize(w, h);
|
|
595
|
-
|
|
596
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
597
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
598
|
-
selectedId: selectedEntityIdRef.current,
|
|
599
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
600
|
-
clearColor: clearColorRef.current,
|
|
601
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
602
|
-
...sectionPlaneRef.current,
|
|
603
|
-
min: sectionRangeRef.current?.min,
|
|
604
|
-
max: sectionRangeRef.current?.max,
|
|
605
|
-
} : undefined,
|
|
606
|
-
});
|
|
621
|
+
renderCurrent();
|
|
607
622
|
});
|
|
608
623
|
resizeObserver.observe(canvas);
|
|
609
624
|
|
|
610
625
|
// Initial render
|
|
611
|
-
|
|
612
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
613
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
614
|
-
selectedId: selectedEntityIdRef.current,
|
|
615
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
616
|
-
clearColor: clearColorRef.current,
|
|
617
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
618
|
-
...sectionPlaneRef.current,
|
|
619
|
-
min: sectionRangeRef.current?.min,
|
|
620
|
-
max: sectionRangeRef.current?.max,
|
|
621
|
-
} : undefined,
|
|
622
|
-
});
|
|
626
|
+
renderCurrent();
|
|
623
627
|
});
|
|
624
628
|
|
|
625
629
|
return () => {
|
|
@@ -643,6 +647,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
643
647
|
// ===== Drawing 2D state for render updates =====
|
|
644
648
|
const drawing2D = useViewerStore((s) => s.drawing2D);
|
|
645
649
|
const show3DOverlay = useViewerStore((s) => s.drawing2DDisplayOptions.show3DOverlay);
|
|
650
|
+
const showHiddenLines = useViewerStore((s) => s.drawing2DDisplayOptions.showHiddenLines);
|
|
646
651
|
|
|
647
652
|
// ===== Streaming progress =====
|
|
648
653
|
const progress = useViewerStore((state) => state.progress);
|
|
@@ -767,6 +772,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
767
772
|
clearColorRef,
|
|
768
773
|
sectionPlaneRef,
|
|
769
774
|
sectionRangeRef,
|
|
775
|
+
visualEnhancementRef,
|
|
770
776
|
lastCameraStateRef,
|
|
771
777
|
updateCameraRotationRealtime,
|
|
772
778
|
calculateScale,
|
|
@@ -791,6 +797,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
791
797
|
isInitialized,
|
|
792
798
|
theme,
|
|
793
799
|
clearColorRef,
|
|
800
|
+
visualEnhancementRef,
|
|
794
801
|
hiddenEntities,
|
|
795
802
|
isolatedEntities,
|
|
796
803
|
selectedEntityId,
|
|
@@ -810,11 +817,13 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
810
817
|
activeToolRef,
|
|
811
818
|
drawing2D,
|
|
812
819
|
show3DOverlay,
|
|
820
|
+
showHiddenLines,
|
|
813
821
|
});
|
|
814
822
|
|
|
815
823
|
return (
|
|
816
824
|
<canvas
|
|
817
825
|
ref={canvasRef}
|
|
826
|
+
data-viewport="main"
|
|
818
827
|
className="w-full h-full block"
|
|
819
828
|
/>
|
|
820
829
|
);
|
|
@@ -7,6 +7,7 @@ import { Viewport } from './Viewport';
|
|
|
7
7
|
import { ViewportOverlays } from './ViewportOverlays';
|
|
8
8
|
import { ToolOverlays } from './ToolOverlays';
|
|
9
9
|
import { Section2DPanel } from './Section2DPanel';
|
|
10
|
+
import { BasketPresentationDock } from './BasketPresentationDock';
|
|
10
11
|
import { useViewerStore } from '@/store';
|
|
11
12
|
import { useIfc } from '@/hooks/useIfc';
|
|
12
13
|
import { useWebGPU } from '@/hooks/useWebGPU';
|
|
@@ -585,6 +586,7 @@ export function ViewportContainer() {
|
|
|
585
586
|
/>
|
|
586
587
|
<ViewportOverlays />
|
|
587
588
|
<ToolOverlays />
|
|
589
|
+
<BasketPresentationDock />
|
|
588
590
|
<Section2DPanel
|
|
589
591
|
mergedGeometry={mergedGeometryResult}
|
|
590
592
|
computedIsolatedIds={computedIsolatedIds}
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
import { Button } from '@/components/ui/button';
|
|
13
13
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
14
14
|
import { useViewerStore } from '@/store';
|
|
15
|
+
import { goHomeFromStore } from '@/store/homeView';
|
|
15
16
|
import { useIfc } from '@/hooks/useIfc';
|
|
17
|
+
import { cn } from '@/lib/utils';
|
|
16
18
|
import { ViewCube, type ViewCubeRef } from './ViewCube';
|
|
17
19
|
import { AxisHelper } from './AxisHelper';
|
|
18
20
|
|
|
@@ -20,6 +22,7 @@ export function ViewportOverlays() {
|
|
|
20
22
|
const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
|
|
21
23
|
const hiddenEntities = useViewerStore((s) => s.hiddenEntities);
|
|
22
24
|
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
25
|
+
const basketPresentationVisible = useViewerStore((s) => s.basketPresentationVisible);
|
|
23
26
|
const cameraCallbacks = useViewerStore((s) => s.cameraCallbacks);
|
|
24
27
|
const setOnCameraRotationChange = useViewerStore((s) => s.setOnCameraRotationChange);
|
|
25
28
|
const setOnScaleChange = useViewerStore((s) => s.setOnScaleChange);
|
|
@@ -99,8 +102,8 @@ export function ViewportOverlays() {
|
|
|
99
102
|
}, [cameraCallbacks]);
|
|
100
103
|
|
|
101
104
|
const handleHome = useCallback(() => {
|
|
102
|
-
|
|
103
|
-
}, [
|
|
105
|
+
goHomeFromStore();
|
|
106
|
+
}, []);
|
|
104
107
|
|
|
105
108
|
const handleFitAll = useCallback(() => {
|
|
106
109
|
cameraCallbacks.fitAll?.();
|
|
@@ -161,7 +164,10 @@ export function ViewportOverlays() {
|
|
|
161
164
|
|
|
162
165
|
{/* Context Info (bottom-center) - Storey names */}
|
|
163
166
|
{storeyNames && storeyNames.length > 0 && (
|
|
164
|
-
<div className=
|
|
167
|
+
<div className={cn(
|
|
168
|
+
'absolute left-1/2 -translate-x-1/2 px-4 py-2 bg-background/80 backdrop-blur-sm rounded-full border shadow-sm',
|
|
169
|
+
basketPresentationVisible ? 'bottom-28' : 'bottom-4',
|
|
170
|
+
)}>
|
|
165
171
|
<div className="flex items-center gap-2 text-sm">
|
|
166
172
|
<Layers className="h-4 w-4 text-primary" />
|
|
167
173
|
<span className="font-medium">
|