@ifc-lite/viewer 1.10.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 +24 -0
- package/dist/assets/{Arrow.dom-Bw5JMdDs.js → Arrow.dom-IIkrrCZ0.js} +1 -1
- package/dist/assets/{browser-DdRf3aWl.js → browser-BoonPy8d.js} +1 -1
- package/dist/assets/{ifc-lite_bg-C1-gLAHo.wasm → ifc-lite_bg-B6s-pcv0.wasm} +0 -0
- package/dist/assets/{index-1ff6P0kc.js → index-CQkEOlYf.js} +40975 -40044
- package/dist/assets/{index-Bz7vHRxl.js → index-ClZCG7KA.js} +4 -4
- package/dist/assets/index-qxIHWl_B.css +1 -0
- package/dist/assets/{native-bridge-C5hD5vae.js → native-bridge-Beg4Kf9O.js} +1 -1
- package/dist/assets/{wasm-bridge-CaNKXFGM.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 +58 -104
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/ViewportOverlays.tsx +9 -3
- package/src/components/viewer/useKeyboardControls.ts +2 -2
- package/src/components/viewer/useRenderUpdates.ts +10 -3
- package/src/hooks/useKeyboardShortcuts.ts +51 -84
- 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 +7 -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/types.ts +11 -0
- package/dist/assets/index-mvbV6NHd.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
|
|
@@ -490,6 +490,21 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
490
490
|
setIsInitialized(true);
|
|
491
491
|
|
|
492
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
|
+
};
|
|
493
508
|
|
|
494
509
|
// Register camera callbacks for ViewCube and other controls
|
|
495
510
|
setCameraCallbacks({
|
|
@@ -498,19 +513,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
498
513
|
const rotation = coordinateInfoRef.current?.buildingRotation;
|
|
499
514
|
camera.setPresetView(view, geometryBoundsRef.current, rotation);
|
|
500
515
|
// Initial render - animation loop will continue rendering during animation
|
|
501
|
-
|
|
502
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
503
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
504
|
-
selectedId: selectedEntityIdRef.current,
|
|
505
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
506
|
-
clearColor: clearColorRef.current,
|
|
507
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
508
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
509
|
-
...sectionPlaneRef.current,
|
|
510
|
-
min: sectionRangeRef.current?.min,
|
|
511
|
-
max: sectionRangeRef.current?.max,
|
|
512
|
-
} : undefined,
|
|
513
|
-
});
|
|
516
|
+
renderCurrent();
|
|
514
517
|
calculateScale();
|
|
515
518
|
},
|
|
516
519
|
fitAll: () => {
|
|
@@ -525,36 +528,12 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
525
528
|
},
|
|
526
529
|
zoomIn: () => {
|
|
527
530
|
camera.zoom(-50, false);
|
|
528
|
-
|
|
529
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
530
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
531
|
-
selectedId: selectedEntityIdRef.current,
|
|
532
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
533
|
-
clearColor: clearColorRef.current,
|
|
534
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
535
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
536
|
-
...sectionPlaneRef.current,
|
|
537
|
-
min: sectionRangeRef.current?.min,
|
|
538
|
-
max: sectionRangeRef.current?.max,
|
|
539
|
-
} : undefined,
|
|
540
|
-
});
|
|
531
|
+
renderCurrent();
|
|
541
532
|
calculateScale();
|
|
542
533
|
},
|
|
543
534
|
zoomOut: () => {
|
|
544
535
|
camera.zoom(50, false);
|
|
545
|
-
|
|
546
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
547
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
548
|
-
selectedId: selectedEntityIdRef.current,
|
|
549
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
550
|
-
clearColor: clearColorRef.current,
|
|
551
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
552
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
553
|
-
...sectionPlaneRef.current,
|
|
554
|
-
min: sectionRangeRef.current?.min,
|
|
555
|
-
max: sectionRangeRef.current?.max,
|
|
556
|
-
} : undefined,
|
|
557
|
-
});
|
|
536
|
+
renderCurrent();
|
|
558
537
|
calculateScale();
|
|
559
538
|
},
|
|
560
539
|
frameSelection: () => {
|
|
@@ -576,19 +555,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
576
555
|
orbit: (deltaX: number, deltaY: number) => {
|
|
577
556
|
// Orbit camera from ViewCube drag
|
|
578
557
|
camera.orbit(deltaX, deltaY, false);
|
|
579
|
-
|
|
580
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
581
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
582
|
-
selectedId: selectedEntityIdRef.current,
|
|
583
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
584
|
-
clearColor: clearColorRef.current,
|
|
585
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
586
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
587
|
-
...sectionPlaneRef.current,
|
|
588
|
-
min: sectionRangeRef.current?.min,
|
|
589
|
-
max: sectionRangeRef.current?.max,
|
|
590
|
-
} : undefined,
|
|
591
|
-
});
|
|
558
|
+
renderCurrent();
|
|
592
559
|
updateCameraRotationRealtime(camera.getRotation());
|
|
593
560
|
calculateScale();
|
|
594
561
|
},
|
|
@@ -600,39 +567,47 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
600
567
|
},
|
|
601
568
|
setProjectionMode: (mode) => {
|
|
602
569
|
camera.setProjectionMode(mode);
|
|
603
|
-
|
|
604
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
605
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
606
|
-
selectedId: selectedEntityIdRef.current,
|
|
607
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
608
|
-
clearColor: clearColorRef.current,
|
|
609
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
610
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
611
|
-
...sectionPlaneRef.current,
|
|
612
|
-
min: sectionRangeRef.current?.min,
|
|
613
|
-
max: sectionRangeRef.current?.max,
|
|
614
|
-
} : undefined,
|
|
615
|
-
});
|
|
570
|
+
renderCurrent();
|
|
616
571
|
calculateScale();
|
|
617
572
|
},
|
|
618
573
|
toggleProjectionMode: () => {
|
|
619
574
|
camera.toggleProjectionMode();
|
|
620
|
-
|
|
621
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
622
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
623
|
-
selectedId: selectedEntityIdRef.current,
|
|
624
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
625
|
-
clearColor: clearColorRef.current,
|
|
626
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
627
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
628
|
-
...sectionPlaneRef.current,
|
|
629
|
-
min: sectionRangeRef.current?.min,
|
|
630
|
-
max: sectionRangeRef.current?.max,
|
|
631
|
-
} : undefined,
|
|
632
|
-
});
|
|
575
|
+
renderCurrent();
|
|
633
576
|
calculateScale();
|
|
634
577
|
},
|
|
635
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
|
+
},
|
|
636
611
|
});
|
|
637
612
|
|
|
638
613
|
// ResizeObserver
|
|
@@ -643,36 +618,12 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
643
618
|
const w = alignToWebGPU(Math.max(1, Math.floor(rect.width)));
|
|
644
619
|
const h = Math.max(1, Math.floor(rect.height));
|
|
645
620
|
renderer.resize(w, h);
|
|
646
|
-
|
|
647
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
648
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
649
|
-
selectedId: selectedEntityIdRef.current,
|
|
650
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
651
|
-
clearColor: clearColorRef.current,
|
|
652
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
653
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
654
|
-
...sectionPlaneRef.current,
|
|
655
|
-
min: sectionRangeRef.current?.min,
|
|
656
|
-
max: sectionRangeRef.current?.max,
|
|
657
|
-
} : undefined,
|
|
658
|
-
});
|
|
621
|
+
renderCurrent();
|
|
659
622
|
});
|
|
660
623
|
resizeObserver.observe(canvas);
|
|
661
624
|
|
|
662
625
|
// Initial render
|
|
663
|
-
|
|
664
|
-
hiddenIds: hiddenEntitiesRef.current,
|
|
665
|
-
isolatedIds: isolatedEntitiesRef.current,
|
|
666
|
-
selectedId: selectedEntityIdRef.current,
|
|
667
|
-
selectedModelIndex: selectedModelIndexRef.current,
|
|
668
|
-
clearColor: clearColorRef.current,
|
|
669
|
-
visualEnhancement: visualEnhancementRef.current,
|
|
670
|
-
sectionPlane: activeToolRef.current === 'section' ? {
|
|
671
|
-
...sectionPlaneRef.current,
|
|
672
|
-
min: sectionRangeRef.current?.min,
|
|
673
|
-
max: sectionRangeRef.current?.max,
|
|
674
|
-
} : undefined,
|
|
675
|
-
});
|
|
626
|
+
renderCurrent();
|
|
676
627
|
});
|
|
677
628
|
|
|
678
629
|
return () => {
|
|
@@ -696,6 +647,7 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
696
647
|
// ===== Drawing 2D state for render updates =====
|
|
697
648
|
const drawing2D = useViewerStore((s) => s.drawing2D);
|
|
698
649
|
const show3DOverlay = useViewerStore((s) => s.drawing2DDisplayOptions.show3DOverlay);
|
|
650
|
+
const showHiddenLines = useViewerStore((s) => s.drawing2DDisplayOptions.showHiddenLines);
|
|
699
651
|
|
|
700
652
|
// ===== Streaming progress =====
|
|
701
653
|
const progress = useViewerStore((state) => state.progress);
|
|
@@ -865,11 +817,13 @@ export function Viewport({ geometry, coordinateInfo, computedIsolatedIds, modelI
|
|
|
865
817
|
activeToolRef,
|
|
866
818
|
drawing2D,
|
|
867
819
|
show3DOverlay,
|
|
820
|
+
showHiddenLines,
|
|
868
821
|
});
|
|
869
822
|
|
|
870
823
|
return (
|
|
871
824
|
<canvas
|
|
872
825
|
ref={canvasRef}
|
|
826
|
+
data-viewport="main"
|
|
873
827
|
className="w-full h-full block"
|
|
874
828
|
/>
|
|
875
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">
|
|
@@ -11,6 +11,7 @@ import { useEffect, type MutableRefObject } from 'react';
|
|
|
11
11
|
import type { Renderer } from '@ifc-lite/renderer';
|
|
12
12
|
import type { MeshData, CoordinateInfo } from '@ifc-lite/geometry';
|
|
13
13
|
import type { SectionPlane } from '@/store';
|
|
14
|
+
import { goHomeFromStore } from '@/store/homeView';
|
|
14
15
|
import { getEntityBounds } from '../../utils/viewportUtils.js';
|
|
15
16
|
|
|
16
17
|
export interface UseKeyboardControlsParams {
|
|
@@ -132,8 +133,7 @@ export function useKeyboardControls(params: UseKeyboardControlsParams): void {
|
|
|
132
133
|
|
|
133
134
|
// Home view (H) - reset to isometric
|
|
134
135
|
if (e.key === 'h' || e.key === 'H') {
|
|
135
|
-
|
|
136
|
-
calculateScale();
|
|
136
|
+
goHomeFromStore();
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Fit all / Zoom extents (Z)
|
|
@@ -47,6 +47,7 @@ export interface UseRenderUpdatesParams {
|
|
|
47
47
|
// Drawing 2D
|
|
48
48
|
drawing2D: Drawing2D | null;
|
|
49
49
|
show3DOverlay: boolean;
|
|
50
|
+
showHiddenLines: boolean;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
@@ -75,6 +76,7 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
75
76
|
activeToolRef,
|
|
76
77
|
drawing2D,
|
|
77
78
|
show3DOverlay,
|
|
79
|
+
showHiddenLines,
|
|
78
80
|
} = params;
|
|
79
81
|
|
|
80
82
|
// Theme-aware clear color update
|
|
@@ -109,8 +111,13 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
109
111
|
expressId: cp.entityId, // DrawingPolygon uses entityId
|
|
110
112
|
}));
|
|
111
113
|
|
|
112
|
-
//
|
|
113
|
-
const lines: DrawingLine2D[] =
|
|
114
|
+
// Include linework from the generated drawing on the section plane overlay.
|
|
115
|
+
const lines: DrawingLine2D[] = drawing2D.lines
|
|
116
|
+
.filter((line) => showHiddenLines || line.visibility !== 'hidden')
|
|
117
|
+
.map((line) => ({
|
|
118
|
+
line: line.line,
|
|
119
|
+
category: line.category,
|
|
120
|
+
}));
|
|
114
121
|
|
|
115
122
|
// Upload to renderer - will be drawn on the section plane
|
|
116
123
|
// Pass sectionRange to match exactly what render() uses for section plane position
|
|
@@ -142,7 +149,7 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
142
149
|
max: sectionRangeRef.current?.max,
|
|
143
150
|
} : undefined,
|
|
144
151
|
});
|
|
145
|
-
}, [drawing2D, activeTool, sectionPlane, isInitialized, coordinateInfo, show3DOverlay]);
|
|
152
|
+
}, [drawing2D, activeTool, sectionPlane, isInitialized, coordinateInfo, show3DOverlay, showHiddenLines]);
|
|
146
153
|
|
|
147
154
|
// Re-render when visibility, selection, or section plane changes
|
|
148
155
|
useEffect(() => {
|