@ifc-lite/viewer 1.7.0 → 1.8.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 +35 -0
- package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CwcRxist.js} +1 -1
- package/dist/assets/index-7WoQ-qVC.css +1 -0
- package/dist/assets/{index-dgdgiQ9p.js → index-BSANf7-H.js} +20926 -17587
- package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-5LbrYh3R.js} +1 -1
- package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-CgpLtj1h.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -18
- package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
- package/src/components/viewer/EntityContextMenu.tsx +47 -20
- package/src/components/viewer/ExportDialog.tsx +166 -17
- package/src/components/viewer/HierarchyPanel.tsx +3 -1
- package/src/components/viewer/LensPanel.tsx +848 -85
- package/src/components/viewer/MainToolbar.tsx +114 -81
- package/src/components/viewer/Section2DPanel.tsx +269 -29
- package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
- package/src/components/viewer/Viewport.tsx +57 -23
- package/src/components/viewer/ViewportContainer.tsx +2 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
- package/src/components/viewer/hierarchy/types.ts +1 -1
- package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
- package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
- package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
- package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
- package/src/components/viewer/tools/computePolygonArea.ts +72 -0
- package/src/components/viewer/useGeometryStreaming.ts +12 -4
- package/src/hooks/ids/idsExportService.ts +1 -1
- package/src/hooks/useAnnotation2D.ts +551 -0
- package/src/hooks/useDrawingExport.ts +83 -1
- package/src/hooks/useKeyboardShortcuts.ts +113 -14
- package/src/hooks/useLens.ts +39 -55
- package/src/hooks/useLensDiscovery.ts +46 -0
- package/src/hooks/useModelSelection.ts +5 -22
- package/src/index.css +7 -1
- package/src/lib/lens/adapter.ts +127 -1
- package/src/lib/lists/columnToAutoColor.ts +33 -0
- package/src/store/index.ts +14 -1
- package/src/store/resolveEntityRef.ts +44 -0
- package/src/store/slices/drawing2DSlice.ts +321 -0
- package/src/store/slices/lensSlice.ts +46 -4
- package/src/store/slices/pinboardSlice.ts +171 -38
- package/src/store.ts +3 -0
- package/dist/assets/index-yTqs8kgX.css +0 -1
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
Scissors,
|
|
15
15
|
Eye,
|
|
16
16
|
EyeOff,
|
|
17
|
-
|
|
17
|
+
Equal,
|
|
18
18
|
Crosshair,
|
|
19
19
|
Home,
|
|
20
20
|
Maximize2,
|
|
@@ -34,13 +34,11 @@ import {
|
|
|
34
34
|
SquareX,
|
|
35
35
|
Building2,
|
|
36
36
|
Plus,
|
|
37
|
+
Minus,
|
|
37
38
|
MessageSquare,
|
|
38
39
|
ClipboardCheck,
|
|
39
|
-
Pin,
|
|
40
|
-
PinOff,
|
|
41
40
|
Palette,
|
|
42
41
|
Orbit,
|
|
43
|
-
Trash2,
|
|
44
42
|
} from 'lucide-react';
|
|
45
43
|
import { Button } from '@/components/ui/button';
|
|
46
44
|
import { Separator } from '@/components/ui/separator';
|
|
@@ -57,7 +55,8 @@ import {
|
|
|
57
55
|
DropdownMenuSubContent,
|
|
58
56
|
} from '@/components/ui/dropdown-menu';
|
|
59
57
|
import { Progress } from '@/components/ui/progress';
|
|
60
|
-
import { useViewerStore, isIfcxDataStore } from '@/store';
|
|
58
|
+
import { useViewerStore, isIfcxDataStore, stringToEntityRef } from '@/store';
|
|
59
|
+
import type { EntityRef } from '@/store';
|
|
61
60
|
import { useIfc } from '@/hooks/useIfc';
|
|
62
61
|
import { cn } from '@/lib/utils';
|
|
63
62
|
import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
|
|
@@ -158,8 +157,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
158
157
|
const theme = useViewerStore((state) => state.theme);
|
|
159
158
|
const toggleTheme = useViewerStore((state) => state.toggleTheme);
|
|
160
159
|
const selectedEntityId = useViewerStore((state) => state.selectedEntityId);
|
|
161
|
-
const
|
|
162
|
-
const hideEntity = useViewerStore((state) => state.hideEntity);
|
|
160
|
+
const hideEntities = useViewerStore((state) => state.hideEntities);
|
|
163
161
|
const showAll = useViewerStore((state) => state.showAll);
|
|
164
162
|
const clearStoreySelection = useViewerStore((state) => state.clearStoreySelection);
|
|
165
163
|
const error = useViewerStore((state) => state.error);
|
|
@@ -178,13 +176,14 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
178
176
|
const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
|
|
179
177
|
const projectionMode = useViewerStore((state) => state.projectionMode);
|
|
180
178
|
const toggleProjectionMode = useViewerStore((state) => state.toggleProjectionMode);
|
|
181
|
-
//
|
|
179
|
+
// Basket state (= + − isolation basket)
|
|
182
180
|
const pinboardEntities = useViewerStore((state) => state.pinboardEntities);
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
181
|
+
const setBasket = useViewerStore((state) => state.setBasket);
|
|
182
|
+
const addToBasket = useViewerStore((state) => state.addToBasket);
|
|
183
|
+
const removeFromBasket = useViewerStore((state) => state.removeFromBasket);
|
|
184
|
+
const clearBasket = useViewerStore((state) => state.clearBasket);
|
|
185
|
+
// NOTE: selectedEntity and selectedEntitiesSet accessed via getState() in callbacks
|
|
186
|
+
// to avoid re-rendering MainToolbar on every Cmd+Click selection change.
|
|
188
187
|
// Lens state
|
|
189
188
|
const lensPanelVisible = useViewerStore((state) => state.lensPanelVisible);
|
|
190
189
|
const toggleLensPanel = useViewerStore((state) => state.toggleLensPanel);
|
|
@@ -289,25 +288,85 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
289
288
|
e.target.value = '';
|
|
290
289
|
}, [loadFilesSequentially, addIfcxOverlays, ifcDataStore]);
|
|
291
290
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
/** Get current selection as EntityRef[] — uses getState() to avoid reactive subscriptions */
|
|
292
|
+
const getSelectionRefs = useCallback((): EntityRef[] => {
|
|
293
|
+
const state = useViewerStore.getState();
|
|
294
|
+
if (state.selectedEntitiesSet.size > 0) {
|
|
295
|
+
const refs: EntityRef[] = [];
|
|
296
|
+
for (const str of state.selectedEntitiesSet) {
|
|
297
|
+
refs.push(stringToEntityRef(str));
|
|
298
|
+
}
|
|
299
|
+
return refs;
|
|
300
|
+
}
|
|
301
|
+
if (state.selectedEntity) {
|
|
302
|
+
return [state.selectedEntity];
|
|
303
|
+
}
|
|
304
|
+
return [];
|
|
305
|
+
}, []);
|
|
306
|
+
|
|
307
|
+
const hasSelection = selectedEntityId !== null;
|
|
308
|
+
|
|
309
|
+
// Basket state
|
|
310
|
+
const showPinboard = useViewerStore((state) => state.showPinboard);
|
|
311
|
+
|
|
312
|
+
// Clear multi-select state after basket operations so subsequent − targets a single entity
|
|
313
|
+
const clearMultiSelect = useCallback(() => {
|
|
314
|
+
const state = useViewerStore.getState();
|
|
315
|
+
if (state.selectedEntitiesSet.size > 0) {
|
|
316
|
+
useViewerStore.setState({ selectedEntitiesSet: new Set(), selectedEntityIds: new Set() });
|
|
317
|
+
}
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
// Basket operations
|
|
321
|
+
const handleSetBasket = useCallback(() => {
|
|
322
|
+
const state = useViewerStore.getState();
|
|
323
|
+
// If basket already exists and user hasn't explicitly multi-selected,
|
|
324
|
+
// re-apply the basket instead of replacing it with a stale single selection.
|
|
325
|
+
// Only an explicit multi-selection (Ctrl+Click) should replace an existing basket.
|
|
326
|
+
if (state.pinboardEntities.size > 0 && state.selectedEntitiesSet.size === 0) {
|
|
327
|
+
showPinboard();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const refs = getSelectionRefs();
|
|
331
|
+
if (refs.length > 0) {
|
|
332
|
+
setBasket(refs);
|
|
333
|
+
clearMultiSelect();
|
|
334
|
+
}
|
|
335
|
+
}, [getSelectionRefs, setBasket, showPinboard, clearMultiSelect]);
|
|
336
|
+
|
|
337
|
+
const handleAddToBasket = useCallback(() => {
|
|
338
|
+
const refs = getSelectionRefs();
|
|
339
|
+
if (refs.length > 0) {
|
|
340
|
+
addToBasket(refs);
|
|
341
|
+
clearMultiSelect();
|
|
342
|
+
}
|
|
343
|
+
}, [getSelectionRefs, addToBasket, clearMultiSelect]);
|
|
344
|
+
|
|
345
|
+
const handleRemoveFromBasket = useCallback(() => {
|
|
346
|
+
const refs = getSelectionRefs();
|
|
347
|
+
if (refs.length > 0) {
|
|
348
|
+
removeFromBasket(refs);
|
|
349
|
+
clearMultiSelect();
|
|
295
350
|
}
|
|
296
|
-
}, [
|
|
351
|
+
}, [getSelectionRefs, removeFromBasket, clearMultiSelect]);
|
|
297
352
|
|
|
298
353
|
const clearSelection = useViewerStore((state) => state.clearSelection);
|
|
299
354
|
|
|
300
355
|
const handleHide = useCallback(() => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
356
|
+
// Hide ALL selected entities (multi-select or single)
|
|
357
|
+
const state = useViewerStore.getState();
|
|
358
|
+
const ids: number[] = state.selectedEntityIds.size > 0
|
|
359
|
+
? Array.from(state.selectedEntityIds)
|
|
360
|
+
: selectedEntityId !== null ? [selectedEntityId] : [];
|
|
361
|
+
if (ids.length > 0) {
|
|
362
|
+
hideEntities(ids);
|
|
304
363
|
clearSelection();
|
|
305
364
|
}
|
|
306
|
-
}, [selectedEntityId,
|
|
365
|
+
}, [selectedEntityId, hideEntities, clearSelection]);
|
|
307
366
|
|
|
308
367
|
const handleShowAll = useCallback(() => {
|
|
309
|
-
showAll();
|
|
310
|
-
clearStoreySelection(); // Also clear storey filtering
|
|
368
|
+
showAll(); // Clear hiddenEntities + isolatedEntities (basket contents preserved)
|
|
369
|
+
clearStoreySelection(); // Also clear storey filtering
|
|
311
370
|
}, [showAll, clearStoreySelection]);
|
|
312
371
|
|
|
313
372
|
const handleExportGLB = useCallback(() => {
|
|
@@ -675,9 +734,37 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
675
734
|
|
|
676
735
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
677
736
|
|
|
678
|
-
{/* ──
|
|
679
|
-
<
|
|
680
|
-
|
|
737
|
+
{/* ── Basket Isolation (= + −) ── */}
|
|
738
|
+
<Tooltip>
|
|
739
|
+
<TooltipTrigger asChild>
|
|
740
|
+
<Button
|
|
741
|
+
variant={pinboardEntities.size > 0 ? 'default' : 'ghost'}
|
|
742
|
+
size="icon-sm"
|
|
743
|
+
onClick={(e) => {
|
|
744
|
+
(e.currentTarget as HTMLButtonElement).blur();
|
|
745
|
+
handleSetBasket();
|
|
746
|
+
}}
|
|
747
|
+
disabled={!hasSelection && pinboardEntities.size === 0}
|
|
748
|
+
className={cn(
|
|
749
|
+
pinboardEntities.size > 0 && 'bg-primary text-primary-foreground relative',
|
|
750
|
+
)}
|
|
751
|
+
>
|
|
752
|
+
<Equal className="h-4 w-4" />
|
|
753
|
+
{pinboardEntities.size > 0 && (
|
|
754
|
+
<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">
|
|
755
|
+
{pinboardEntities.size}
|
|
756
|
+
</span>
|
|
757
|
+
)}
|
|
758
|
+
</Button>
|
|
759
|
+
</TooltipTrigger>
|
|
760
|
+
<TooltipContent>
|
|
761
|
+
Set Basket — isolate selection <span className="ml-2 text-xs opacity-60">(I)</span>
|
|
762
|
+
</TooltipContent>
|
|
763
|
+
</Tooltip>
|
|
764
|
+
<ActionButton icon={Plus} label="Add to Basket" onClick={handleAddToBasket} shortcut="+" disabled={!hasSelection} />
|
|
765
|
+
<ActionButton icon={Minus} label="Remove from Basket" onClick={handleRemoveFromBasket} shortcut="−" disabled={!hasSelection} />
|
|
766
|
+
|
|
767
|
+
<ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del / Space" disabled={!hasSelection} />
|
|
681
768
|
<ActionButton icon={Eye} label="Show All (Reset Filters)" onClick={handleShowAll} shortcut="A" />
|
|
682
769
|
<ActionButton icon={Maximize2} label="Fit All" onClick={() => cameraCallbacks.fitAll?.()} shortcut="Z" />
|
|
683
770
|
<ActionButton
|
|
@@ -685,7 +772,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
685
772
|
label="Frame Selection"
|
|
686
773
|
onClick={() => cameraCallbacks.frameSelection?.()}
|
|
687
774
|
shortcut="F"
|
|
688
|
-
disabled={!
|
|
775
|
+
disabled={!hasSelection}
|
|
689
776
|
/>
|
|
690
777
|
|
|
691
778
|
<DropdownMenu>
|
|
@@ -730,60 +817,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
730
817
|
</DropdownMenuContent>
|
|
731
818
|
</DropdownMenu>
|
|
732
819
|
|
|
733
|
-
{/* Pinboard dropdown */}
|
|
734
|
-
<DropdownMenu>
|
|
735
|
-
<Tooltip>
|
|
736
|
-
<TooltipTrigger asChild>
|
|
737
|
-
<DropdownMenuTrigger asChild>
|
|
738
|
-
<Button
|
|
739
|
-
variant={pinboardEntities.size > 0 ? 'default' : 'ghost'}
|
|
740
|
-
size="icon-sm"
|
|
741
|
-
className={cn(pinboardEntities.size > 0 && 'bg-primary text-primary-foreground relative')}
|
|
742
|
-
>
|
|
743
|
-
<Pin className="h-4 w-4" />
|
|
744
|
-
{pinboardEntities.size > 0 && (
|
|
745
|
-
<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">
|
|
746
|
-
{pinboardEntities.size}
|
|
747
|
-
</span>
|
|
748
|
-
)}
|
|
749
|
-
</Button>
|
|
750
|
-
</DropdownMenuTrigger>
|
|
751
|
-
</TooltipTrigger>
|
|
752
|
-
<TooltipContent>Pinboard ({pinboardEntities.size})</TooltipContent>
|
|
753
|
-
</Tooltip>
|
|
754
|
-
<DropdownMenuContent>
|
|
755
|
-
<DropdownMenuItem
|
|
756
|
-
onClick={() => { if (selectedEntity) addToPinboard([selectedEntity]); }}
|
|
757
|
-
disabled={!selectedEntity}
|
|
758
|
-
>
|
|
759
|
-
<Pin className="h-4 w-4 mr-2" />
|
|
760
|
-
Pin Selection
|
|
761
|
-
</DropdownMenuItem>
|
|
762
|
-
<DropdownMenuItem
|
|
763
|
-
onClick={() => { if (selectedEntity) removeFromPinboard([selectedEntity]); }}
|
|
764
|
-
disabled={!selectedEntity}
|
|
765
|
-
>
|
|
766
|
-
<PinOff className="h-4 w-4 mr-2" />
|
|
767
|
-
Unpin Selection
|
|
768
|
-
</DropdownMenuItem>
|
|
769
|
-
<DropdownMenuSeparator />
|
|
770
|
-
<DropdownMenuItem
|
|
771
|
-
onClick={() => showPinboard()}
|
|
772
|
-
disabled={pinboardEntities.size === 0}
|
|
773
|
-
>
|
|
774
|
-
<Eye className="h-4 w-4 mr-2" />
|
|
775
|
-
Show Pinboard
|
|
776
|
-
</DropdownMenuItem>
|
|
777
|
-
<DropdownMenuItem
|
|
778
|
-
onClick={() => clearPinboard()}
|
|
779
|
-
disabled={pinboardEntities.size === 0}
|
|
780
|
-
>
|
|
781
|
-
<Trash2 className="h-4 w-4 mr-2" />
|
|
782
|
-
Clear Pinboard
|
|
783
|
-
</DropdownMenuItem>
|
|
784
|
-
</DropdownMenuContent>
|
|
785
|
-
</DropdownMenu>
|
|
786
|
-
|
|
787
820
|
{/* Lens (rule-based filtering) */}
|
|
788
821
|
<Tooltip>
|
|
789
822
|
<TooltipTrigger asChild>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react';
|
|
15
|
-
import { X, Download, Eye, EyeOff, Maximize2, ZoomIn, ZoomOut, Loader2, Printer, GripVertical, MoreHorizontal, RefreshCw, Pin, PinOff, Palette, Ruler, Trash2, FileText, Shapes, Box } from 'lucide-react';
|
|
15
|
+
import { X, Download, Eye, EyeOff, Maximize2, ZoomIn, ZoomOut, Loader2, Printer, GripVertical, MoreHorizontal, RefreshCw, Pin, PinOff, Palette, Ruler, Trash2, FileText, Shapes, Box, PenTool, Hexagon, Type, Cloud, MousePointer2 } from 'lucide-react';
|
|
16
16
|
import { Button } from '@/components/ui/button';
|
|
17
17
|
import {
|
|
18
18
|
DropdownMenu,
|
|
@@ -28,9 +28,11 @@ import { type GeometryResult } from '@ifc-lite/geometry';
|
|
|
28
28
|
import { DrawingSettingsPanel } from './DrawingSettingsPanel';
|
|
29
29
|
import { SheetSetupPanel } from './SheetSetupPanel';
|
|
30
30
|
import { TitleBlockEditor } from './TitleBlockEditor';
|
|
31
|
+
import { TextAnnotationEditor } from './TextAnnotationEditor';
|
|
31
32
|
import { Drawing2DCanvas } from './Drawing2DCanvas';
|
|
32
33
|
import { useDrawingGeneration } from '@/hooks/useDrawingGeneration';
|
|
33
34
|
import { useMeasure2D } from '@/hooks/useMeasure2D';
|
|
35
|
+
import { useAnnotation2D } from '@/hooks/useAnnotation2D';
|
|
34
36
|
import { useViewControls } from '@/hooks/useViewControls';
|
|
35
37
|
import { useDrawingExport } from '@/hooks/useDrawingExport';
|
|
36
38
|
|
|
@@ -98,6 +100,39 @@ export function Section2DPanel({
|
|
|
98
100
|
const measure2DSnapPoint = useViewerStore((s) => s.measure2DSnapPoint);
|
|
99
101
|
const setMeasure2DSnapPoint = useViewerStore((s) => s.setMeasure2DSnapPoint);
|
|
100
102
|
|
|
103
|
+
// Annotation tool state
|
|
104
|
+
const annotation2DActiveTool = useViewerStore((s) => s.annotation2DActiveTool);
|
|
105
|
+
const setAnnotation2DActiveTool = useViewerStore((s) => s.setAnnotation2DActiveTool);
|
|
106
|
+
const annotation2DCursorPos = useViewerStore((s) => s.annotation2DCursorPos);
|
|
107
|
+
const setAnnotation2DCursorPos = useViewerStore((s) => s.setAnnotation2DCursorPos);
|
|
108
|
+
// Polygon area state
|
|
109
|
+
const polygonArea2DPoints = useViewerStore((s) => s.polygonArea2DPoints);
|
|
110
|
+
const polygonArea2DResults = useViewerStore((s) => s.polygonArea2DResults);
|
|
111
|
+
const addPolygonArea2DPoint = useViewerStore((s) => s.addPolygonArea2DPoint);
|
|
112
|
+
const completePolygonArea2D = useViewerStore((s) => s.completePolygonArea2D);
|
|
113
|
+
const cancelPolygonArea2D = useViewerStore((s) => s.cancelPolygonArea2D);
|
|
114
|
+
const clearPolygonArea2DResults = useViewerStore((s) => s.clearPolygonArea2DResults);
|
|
115
|
+
// Text annotation state
|
|
116
|
+
const textAnnotations2D = useViewerStore((s) => s.textAnnotations2D);
|
|
117
|
+
const textAnnotation2DEditing = useViewerStore((s) => s.textAnnotation2DEditing);
|
|
118
|
+
const addTextAnnotation2D = useViewerStore((s) => s.addTextAnnotation2D);
|
|
119
|
+
const updateTextAnnotation2D = useViewerStore((s) => s.updateTextAnnotation2D);
|
|
120
|
+
const removeTextAnnotation2D = useViewerStore((s) => s.removeTextAnnotation2D);
|
|
121
|
+
const setTextAnnotation2DEditing = useViewerStore((s) => s.setTextAnnotation2DEditing);
|
|
122
|
+
// Cloud annotation state
|
|
123
|
+
const cloudAnnotation2DPoints = useViewerStore((s) => s.cloudAnnotation2DPoints);
|
|
124
|
+
const cloudAnnotations2D = useViewerStore((s) => s.cloudAnnotations2D);
|
|
125
|
+
const addCloudAnnotation2DPoint = useViewerStore((s) => s.addCloudAnnotation2DPoint);
|
|
126
|
+
const completeCloudAnnotation2D = useViewerStore((s) => s.completeCloudAnnotation2D);
|
|
127
|
+
const cancelCloudAnnotation2D = useViewerStore((s) => s.cancelCloudAnnotation2D);
|
|
128
|
+
// Selection
|
|
129
|
+
const selectedAnnotation2D = useViewerStore((s) => s.selectedAnnotation2D);
|
|
130
|
+
const setSelectedAnnotation2D = useViewerStore((s) => s.setSelectedAnnotation2D);
|
|
131
|
+
const deleteSelectedAnnotation2D = useViewerStore((s) => s.deleteSelectedAnnotation2D);
|
|
132
|
+
const moveAnnotation2D = useViewerStore((s) => s.moveAnnotation2D);
|
|
133
|
+
// Bulk
|
|
134
|
+
const clearAllAnnotations2D = useViewerStore((s) => s.clearAllAnnotations2D);
|
|
135
|
+
|
|
101
136
|
const sectionPlane = useViewerStore((s) => s.sectionPlane);
|
|
102
137
|
const activeTool = useViewerStore((s) => s.activeTool);
|
|
103
138
|
const models = useViewerStore((s) => s.models);
|
|
@@ -228,7 +263,7 @@ export function Section2DPanel({
|
|
|
228
263
|
isPinned, cachedSheetTransformRef,
|
|
229
264
|
});
|
|
230
265
|
|
|
231
|
-
const
|
|
266
|
+
const measureHandlers = useMeasure2D({
|
|
232
267
|
drawing, viewTransform, setViewTransform, sectionAxis: sectionPlane.axis, containerRef,
|
|
233
268
|
measure2DMode, measure2DStart, measure2DCurrent,
|
|
234
269
|
measure2DShiftLocked, measure2DLockedAxis,
|
|
@@ -236,10 +271,67 @@ export function Section2DPanel({
|
|
|
236
271
|
setMeasure2DSnapPoint, cancelMeasure2D, completeMeasure2D,
|
|
237
272
|
});
|
|
238
273
|
|
|
274
|
+
const annotationHandlers = useAnnotation2D({
|
|
275
|
+
drawing, viewTransform, sectionAxis: sectionPlane.axis, containerRef,
|
|
276
|
+
activeTool: annotation2DActiveTool, setActiveTool: setAnnotation2DActiveTool,
|
|
277
|
+
polygonArea2DPoints, addPolygonArea2DPoint, completePolygonArea2D, cancelPolygonArea2D,
|
|
278
|
+
textAnnotations2D, addTextAnnotation2D, setTextAnnotation2DEditing,
|
|
279
|
+
cloudAnnotation2DPoints, cloudAnnotations2D, addCloudAnnotation2DPoint, completeCloudAnnotation2D, cancelCloudAnnotation2D,
|
|
280
|
+
measure2DResults, polygonArea2DResults,
|
|
281
|
+
selectedAnnotation2D, setSelectedAnnotation2D, deleteSelectedAnnotation2D, moveAnnotation2D,
|
|
282
|
+
setAnnotation2DCursorPos, setMeasure2DSnapPoint,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Unified mouse handlers that dispatch to the right tool
|
|
286
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
287
|
+
if (annotation2DActiveTool === 'measure') {
|
|
288
|
+
measureHandlers.handleMouseDown(e);
|
|
289
|
+
} else if (annotation2DActiveTool === 'none') {
|
|
290
|
+
// Try annotation selection/drag first; if it consumed the click, don't pan
|
|
291
|
+
const consumed = annotationHandlers.handleMouseDown(e);
|
|
292
|
+
if (!consumed) {
|
|
293
|
+
measureHandlers.handleMouseDown(e);
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
annotationHandlers.handleMouseDown(e);
|
|
297
|
+
}
|
|
298
|
+
}, [annotation2DActiveTool, measureHandlers, annotationHandlers]);
|
|
299
|
+
|
|
300
|
+
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
|
301
|
+
// If dragging an annotation, let the annotation handler handle it
|
|
302
|
+
if (annotationHandlers.isDraggingRef.current) {
|
|
303
|
+
annotationHandlers.handleMouseMove(e);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (annotation2DActiveTool === 'measure' || annotation2DActiveTool === 'none') {
|
|
307
|
+
measureHandlers.handleMouseMove(e);
|
|
308
|
+
} else {
|
|
309
|
+
annotationHandlers.handleMouseMove(e);
|
|
310
|
+
}
|
|
311
|
+
}, [annotation2DActiveTool, measureHandlers, annotationHandlers]);
|
|
312
|
+
|
|
313
|
+
const handleMouseUp = useCallback((e: React.MouseEvent) => {
|
|
314
|
+
annotationHandlers.handleMouseUp(e);
|
|
315
|
+
measureHandlers.handleMouseUp();
|
|
316
|
+
}, [measureHandlers, annotationHandlers]);
|
|
317
|
+
|
|
318
|
+
const handleMouseLeave = useCallback(() => {
|
|
319
|
+
measureHandlers.handleMouseLeave();
|
|
320
|
+
}, [measureHandlers]);
|
|
321
|
+
|
|
322
|
+
const handleMouseEnter = useCallback((e: React.MouseEvent) => {
|
|
323
|
+
measureHandlers.handleMouseEnter(e);
|
|
324
|
+
}, [measureHandlers]);
|
|
325
|
+
|
|
326
|
+
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
|
|
327
|
+
annotationHandlers.handleDoubleClick(e);
|
|
328
|
+
}, [annotationHandlers]);
|
|
329
|
+
|
|
239
330
|
const { formatDistance, handleExportSVG, handlePrint } = useDrawingExport({
|
|
240
331
|
drawing, displayOptions, sectionPlane, activePresetId,
|
|
241
332
|
entityColorMap, overridesEnabled, overrideEngine,
|
|
242
|
-
measure2DResults,
|
|
333
|
+
measure2DResults, polygonArea2DResults, textAnnotations2D, cloudAnnotations2D,
|
|
334
|
+
sheetEnabled, activeSheet,
|
|
243
335
|
});
|
|
244
336
|
|
|
245
337
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -271,6 +363,42 @@ export function Section2DPanel({
|
|
|
271
363
|
setIsPinned((prev) => !prev);
|
|
272
364
|
}, []);
|
|
273
365
|
|
|
366
|
+
// Text editor handlers
|
|
367
|
+
const handleTextConfirm = useCallback((id: string, text: string) => {
|
|
368
|
+
updateTextAnnotation2D(id, { text });
|
|
369
|
+
setTextAnnotation2DEditing(null);
|
|
370
|
+
}, [updateTextAnnotation2D, setTextAnnotation2DEditing]);
|
|
371
|
+
|
|
372
|
+
const handleTextCancel = useCallback((id: string) => {
|
|
373
|
+
// If text is empty (just created), remove it
|
|
374
|
+
const annotation = textAnnotations2D.find((a) => a.id === id);
|
|
375
|
+
if (annotation && !annotation.text.trim()) {
|
|
376
|
+
removeTextAnnotation2D(id);
|
|
377
|
+
}
|
|
378
|
+
setTextAnnotation2DEditing(null);
|
|
379
|
+
}, [textAnnotations2D, removeTextAnnotation2D, setTextAnnotation2DEditing]);
|
|
380
|
+
|
|
381
|
+
// Check if any annotations exist
|
|
382
|
+
const hasAnnotations = measure2DResults.length > 0 ||
|
|
383
|
+
polygonArea2DResults.length > 0 ||
|
|
384
|
+
textAnnotations2D.length > 0 ||
|
|
385
|
+
cloudAnnotations2D.length > 0;
|
|
386
|
+
|
|
387
|
+
// Cursor style based on active tool
|
|
388
|
+
const cursorClass = useMemo(() => {
|
|
389
|
+
if (selectedAnnotation2D && annotation2DActiveTool === 'none') return 'cursor-move';
|
|
390
|
+
switch (annotation2DActiveTool) {
|
|
391
|
+
case 'measure':
|
|
392
|
+
case 'polygon-area':
|
|
393
|
+
case 'cloud':
|
|
394
|
+
return 'cursor-crosshair';
|
|
395
|
+
case 'text':
|
|
396
|
+
return 'cursor-text';
|
|
397
|
+
default:
|
|
398
|
+
return 'cursor-grab active:cursor-grabbing';
|
|
399
|
+
}
|
|
400
|
+
}, [annotation2DActiveTool, selectedAnnotation2D]);
|
|
401
|
+
|
|
274
402
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
275
403
|
// RESIZE HANDLING
|
|
276
404
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -400,25 +528,55 @@ export function Section2DPanel({
|
|
|
400
528
|
{displayOptions.useSymbolicRepresentations ? <Shapes className="h-4 w-4" /> : <Box className="h-4 w-4" />}
|
|
401
529
|
</Button>
|
|
402
530
|
|
|
403
|
-
{/*
|
|
404
|
-
<
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
531
|
+
{/* Annotation Tools Dropdown */}
|
|
532
|
+
<DropdownMenu>
|
|
533
|
+
<DropdownMenuTrigger asChild>
|
|
534
|
+
<Button
|
|
535
|
+
variant={annotation2DActiveTool !== 'none' ? 'default' : 'ghost'}
|
|
536
|
+
size="icon-sm"
|
|
537
|
+
title="Annotation tools"
|
|
538
|
+
>
|
|
539
|
+
<PenTool className="h-4 w-4" />
|
|
540
|
+
</Button>
|
|
541
|
+
</DropdownMenuTrigger>
|
|
542
|
+
<DropdownMenuContent align="start">
|
|
543
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool('none')}>
|
|
544
|
+
<MousePointer2 className="h-4 w-4 mr-2" />
|
|
545
|
+
Select / Pan
|
|
546
|
+
{annotation2DActiveTool === 'none' && <span className="ml-auto text-xs text-primary">Active</span>}
|
|
547
|
+
</DropdownMenuItem>
|
|
548
|
+
<DropdownMenuSeparator />
|
|
549
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool(annotation2DActiveTool === 'measure' ? 'none' : 'measure')}>
|
|
550
|
+
<Ruler className="h-4 w-4 mr-2" />
|
|
551
|
+
Distance Measure
|
|
552
|
+
{annotation2DActiveTool === 'measure' && <span className="ml-auto text-xs text-primary">Active</span>}
|
|
553
|
+
</DropdownMenuItem>
|
|
554
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool(annotation2DActiveTool === 'polygon-area' ? 'none' : 'polygon-area')}>
|
|
555
|
+
<Hexagon className="h-4 w-4 mr-2" />
|
|
556
|
+
Area Measure
|
|
557
|
+
{annotation2DActiveTool === 'polygon-area' && <span className="ml-auto text-xs text-primary">Active</span>}
|
|
558
|
+
</DropdownMenuItem>
|
|
559
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool(annotation2DActiveTool === 'text' ? 'none' : 'text')}>
|
|
560
|
+
<Type className="h-4 w-4 mr-2" />
|
|
561
|
+
Text Box
|
|
562
|
+
{annotation2DActiveTool === 'text' && <span className="ml-auto text-xs text-primary">Active</span>}
|
|
563
|
+
</DropdownMenuItem>
|
|
564
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool(annotation2DActiveTool === 'cloud' ? 'none' : 'cloud')}>
|
|
565
|
+
<Cloud className="h-4 w-4 mr-2" />
|
|
566
|
+
Revision Cloud
|
|
567
|
+
{annotation2DActiveTool === 'cloud' && <span className="ml-auto text-xs text-primary">Active</span>}
|
|
568
|
+
</DropdownMenuItem>
|
|
569
|
+
{hasAnnotations && (
|
|
570
|
+
<>
|
|
571
|
+
<DropdownMenuSeparator />
|
|
572
|
+
<DropdownMenuItem onClick={clearAllAnnotations2D}>
|
|
573
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
|
574
|
+
Clear All Annotations
|
|
575
|
+
</DropdownMenuItem>
|
|
576
|
+
</>
|
|
577
|
+
)}
|
|
578
|
+
</DropdownMenuContent>
|
|
579
|
+
</DropdownMenu>
|
|
422
580
|
|
|
423
581
|
{/* Graphic Override Settings */}
|
|
424
582
|
<Button
|
|
@@ -537,14 +695,30 @@ export function Section2DPanel({
|
|
|
537
695
|
{displayOptions.useSymbolicRepresentations ? <Shapes className="h-4 w-4 mr-2" /> : <Box className="h-4 w-4 mr-2" />}
|
|
538
696
|
{displayOptions.useSymbolicRepresentations ? 'Symbolic (Plan)' : 'Section Cut (Body)'}
|
|
539
697
|
</DropdownMenuItem>
|
|
540
|
-
<DropdownMenuItem onClick={
|
|
698
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool('none')}>
|
|
699
|
+
<MousePointer2 className="h-4 w-4 mr-2" />
|
|
700
|
+
Select / Pan {annotation2DActiveTool === 'none' ? '(On)' : ''}
|
|
701
|
+
</DropdownMenuItem>
|
|
702
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool(annotation2DActiveTool === 'measure' ? 'none' : 'measure')}>
|
|
541
703
|
<Ruler className="h-4 w-4 mr-2" />
|
|
542
|
-
Measure {
|
|
704
|
+
Distance Measure {annotation2DActiveTool === 'measure' ? '(On)' : ''}
|
|
705
|
+
</DropdownMenuItem>
|
|
706
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool(annotation2DActiveTool === 'polygon-area' ? 'none' : 'polygon-area')}>
|
|
707
|
+
<Hexagon className="h-4 w-4 mr-2" />
|
|
708
|
+
Area Measure {annotation2DActiveTool === 'polygon-area' ? '(On)' : ''}
|
|
709
|
+
</DropdownMenuItem>
|
|
710
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool(annotation2DActiveTool === 'text' ? 'none' : 'text')}>
|
|
711
|
+
<Type className="h-4 w-4 mr-2" />
|
|
712
|
+
Text Box {annotation2DActiveTool === 'text' ? '(On)' : ''}
|
|
713
|
+
</DropdownMenuItem>
|
|
714
|
+
<DropdownMenuItem onClick={() => setAnnotation2DActiveTool(annotation2DActiveTool === 'cloud' ? 'none' : 'cloud')}>
|
|
715
|
+
<Cloud className="h-4 w-4 mr-2" />
|
|
716
|
+
Revision Cloud {annotation2DActiveTool === 'cloud' ? '(On)' : ''}
|
|
543
717
|
</DropdownMenuItem>
|
|
544
|
-
{
|
|
545
|
-
<DropdownMenuItem onClick={
|
|
718
|
+
{hasAnnotations && (
|
|
719
|
+
<DropdownMenuItem onClick={clearAllAnnotations2D}>
|
|
546
720
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
547
|
-
Clear
|
|
721
|
+
Clear All Annotations
|
|
548
722
|
</DropdownMenuItem>
|
|
549
723
|
)}
|
|
550
724
|
<DropdownMenuSeparator />
|
|
@@ -602,13 +776,13 @@ export function Section2DPanel({
|
|
|
602
776
|
{/* Drawing Canvas */}
|
|
603
777
|
<div
|
|
604
778
|
ref={containerRef}
|
|
605
|
-
className={`relative flex-1 overflow-hidden bg-white dark:bg-zinc-950 rounded-b-lg ${
|
|
606
|
-
}`}
|
|
779
|
+
className={`relative flex-1 overflow-hidden bg-white dark:bg-zinc-950 rounded-b-lg ${cursorClass}`}
|
|
607
780
|
onMouseDown={handleMouseDown}
|
|
608
781
|
onMouseMove={handleMouseMove}
|
|
609
782
|
onMouseUp={handleMouseUp}
|
|
610
783
|
onMouseEnter={handleMouseEnter}
|
|
611
784
|
onMouseLeave={handleMouseLeave}
|
|
785
|
+
onDoubleClick={handleDoubleClick}
|
|
612
786
|
>
|
|
613
787
|
{status === 'generating' && (
|
|
614
788
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-background/80">
|
|
@@ -658,6 +832,15 @@ export function Section2DPanel({
|
|
|
658
832
|
sectionAxis={sectionPlane.axis}
|
|
659
833
|
isPinned={isPinned}
|
|
660
834
|
cachedSheetTransformRef={cachedSheetTransformRef}
|
|
835
|
+
annotation2DActiveTool={annotation2DActiveTool}
|
|
836
|
+
annotation2DCursorPos={annotation2DCursorPos}
|
|
837
|
+
polygonAreaPoints={polygonArea2DPoints}
|
|
838
|
+
polygonAreaResults={polygonArea2DResults}
|
|
839
|
+
textAnnotations={textAnnotations2D}
|
|
840
|
+
textAnnotationEditing={textAnnotation2DEditing}
|
|
841
|
+
cloudAnnotationPoints={cloudAnnotation2DPoints}
|
|
842
|
+
cloudAnnotations={cloudAnnotations2D}
|
|
843
|
+
selectedAnnotation={selectedAnnotation2D}
|
|
661
844
|
/>
|
|
662
845
|
{/* Subtle updating indicator - shows while regenerating without hiding the drawing */}
|
|
663
846
|
{isRegenerating && (
|
|
@@ -669,6 +852,25 @@ export function Section2DPanel({
|
|
|
669
852
|
</>
|
|
670
853
|
)}
|
|
671
854
|
|
|
855
|
+
{/* Text Annotation Editor Overlay */}
|
|
856
|
+
{textAnnotation2DEditing && (() => {
|
|
857
|
+
const editingAnnotation = textAnnotations2D.find((a) => a.id === textAnnotation2DEditing);
|
|
858
|
+
if (!editingAnnotation) return null;
|
|
859
|
+
const scaleX = sectionPlane.axis === 'side' ? -viewTransform.scale : viewTransform.scale;
|
|
860
|
+
const scaleY = sectionPlane.axis === 'down' ? viewTransform.scale : -viewTransform.scale;
|
|
861
|
+
const screenX = editingAnnotation.position.x * scaleX + viewTransform.x;
|
|
862
|
+
const screenY = editingAnnotation.position.y * scaleY + viewTransform.y;
|
|
863
|
+
return (
|
|
864
|
+
<TextAnnotationEditor
|
|
865
|
+
annotation={editingAnnotation}
|
|
866
|
+
screenX={screenX}
|
|
867
|
+
screenY={screenY}
|
|
868
|
+
onConfirm={handleTextConfirm}
|
|
869
|
+
onCancel={handleTextCancel}
|
|
870
|
+
/>
|
|
871
|
+
);
|
|
872
|
+
})()}
|
|
873
|
+
|
|
672
874
|
{/* Measure mode tip - bottom right */}
|
|
673
875
|
{measure2DMode && measure2DStart && (
|
|
674
876
|
<div className="absolute bottom-2 right-2 pointer-events-none z-10">
|
|
@@ -679,6 +881,44 @@ export function Section2DPanel({
|
|
|
679
881
|
</div>
|
|
680
882
|
)}
|
|
681
883
|
|
|
884
|
+
{/* Polygon area tip */}
|
|
885
|
+
{annotation2DActiveTool === 'polygon-area' && (
|
|
886
|
+
<div className="absolute bottom-2 right-2 pointer-events-none z-10">
|
|
887
|
+
<div className="text-[10px] text-black bg-white/80 px-1.5 py-0.5 rounded">
|
|
888
|
+
{polygonArea2DPoints.length === 0 ? 'Click to place first vertex · Hold Shift to constrain' :
|
|
889
|
+
polygonArea2DPoints.length < 3 ? `${polygonArea2DPoints.length} vertices — need at least 3 · Shift = constrain` :
|
|
890
|
+
'Double-click or click first vertex to close · Shift = constrain'}
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
893
|
+
)}
|
|
894
|
+
|
|
895
|
+
{/* Cloud tool tip */}
|
|
896
|
+
{annotation2DActiveTool === 'cloud' && (
|
|
897
|
+
<div className="absolute bottom-2 right-2 pointer-events-none z-10">
|
|
898
|
+
<div className="text-[10px] text-black bg-white/80 px-1.5 py-0.5 rounded">
|
|
899
|
+
{cloudAnnotation2DPoints.length === 0 ? 'Click to place first corner' : 'Click to place second corner · Shift = square'}
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
902
|
+
)}
|
|
903
|
+
|
|
904
|
+
{/* Text tool tip */}
|
|
905
|
+
{annotation2DActiveTool === 'text' && !textAnnotation2DEditing && (
|
|
906
|
+
<div className="absolute bottom-2 right-2 pointer-events-none z-10">
|
|
907
|
+
<div className="text-[10px] text-black bg-white/80 px-1.5 py-0.5 rounded">
|
|
908
|
+
Click to place text box
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
)}
|
|
912
|
+
|
|
913
|
+
{/* Selection tip */}
|
|
914
|
+
{selectedAnnotation2D && annotation2DActiveTool === 'none' && (
|
|
915
|
+
<div className="absolute bottom-2 right-2 pointer-events-none z-10">
|
|
916
|
+
<div className="text-[10px] text-black bg-white/80 px-1.5 py-0.5 rounded">
|
|
917
|
+
{selectedAnnotation2D.type === 'text' ? 'Del = delete · Drag to move · Double-click to edit' : 'Del = delete · Drag to move'} · Esc = deselect
|
|
918
|
+
</div>
|
|
919
|
+
</div>
|
|
920
|
+
)}
|
|
921
|
+
|
|
682
922
|
{status === 'ready' && drawing && drawing.cutPolygons.length === 0 && (!drawing.lines || drawing.lines.length === 0) && (
|
|
683
923
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
684
924
|
<div className="text-center text-muted-foreground">
|