@ifc-lite/viewer 1.6.0 → 1.7.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 +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-v3mcCUPN.css +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Section plane controls panel
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useCallback, useState } from 'react';
|
|
10
|
+
import { X, Slice, ChevronDown, FileImage } from 'lucide-react';
|
|
11
|
+
import { Button } from '@/components/ui/button';
|
|
12
|
+
import { useViewerStore } from '@/store';
|
|
13
|
+
import { AXIS_INFO } from './sectionConstants';
|
|
14
|
+
import { SectionPlaneVisualization } from './SectionVisualization';
|
|
15
|
+
|
|
16
|
+
export function SectionOverlay() {
|
|
17
|
+
const sectionPlane = useViewerStore((s) => s.sectionPlane);
|
|
18
|
+
const setSectionPlaneAxis = useViewerStore((s) => s.setSectionPlaneAxis);
|
|
19
|
+
const setSectionPlanePosition = useViewerStore((s) => s.setSectionPlanePosition);
|
|
20
|
+
const toggleSectionPlane = useViewerStore((s) => s.toggleSectionPlane);
|
|
21
|
+
const setActiveTool = useViewerStore((s) => s.setActiveTool);
|
|
22
|
+
const setDrawingPanelVisible = useViewerStore((s) => s.setDrawing2DPanelVisible);
|
|
23
|
+
const drawingPanelVisible = useViewerStore((s) => s.drawing2DPanelVisible);
|
|
24
|
+
const clearDrawing = useViewerStore((s) => s.clearDrawing2D);
|
|
25
|
+
const [isPanelCollapsed, setIsPanelCollapsed] = useState(true);
|
|
26
|
+
|
|
27
|
+
const handleClose = useCallback(() => {
|
|
28
|
+
setActiveTool('select');
|
|
29
|
+
}, [setActiveTool]);
|
|
30
|
+
|
|
31
|
+
const handleAxisChange = useCallback((axis: 'down' | 'front' | 'side') => {
|
|
32
|
+
setSectionPlaneAxis(axis);
|
|
33
|
+
}, [setSectionPlaneAxis]);
|
|
34
|
+
|
|
35
|
+
const handlePositionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
36
|
+
const value = Number(e.target.value);
|
|
37
|
+
if (!Number.isNaN(value)) {
|
|
38
|
+
setSectionPlanePosition(value);
|
|
39
|
+
}
|
|
40
|
+
}, [setSectionPlanePosition]);
|
|
41
|
+
|
|
42
|
+
const togglePanel = useCallback(() => {
|
|
43
|
+
setIsPanelCollapsed(prev => !prev);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const handleView2D = useCallback(() => {
|
|
47
|
+
// Clear existing drawing to force regeneration with current settings
|
|
48
|
+
clearDrawing();
|
|
49
|
+
setDrawingPanelVisible(true);
|
|
50
|
+
}, [clearDrawing, setDrawingPanelVisible]);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
{/* Compact Section Tool Panel - matches Measure tool style */}
|
|
55
|
+
<div className="pointer-events-auto absolute top-4 left-1/2 -translate-x-1/2 bg-background/95 backdrop-blur-sm rounded-lg border shadow-lg z-30">
|
|
56
|
+
{/* Header - always visible */}
|
|
57
|
+
<div className="flex items-center justify-between gap-2 p-2">
|
|
58
|
+
<button
|
|
59
|
+
onClick={togglePanel}
|
|
60
|
+
className="flex items-center gap-2 hover:bg-accent/50 rounded px-2 py-1 transition-colors"
|
|
61
|
+
>
|
|
62
|
+
<Slice className="h-4 w-4 text-primary" />
|
|
63
|
+
<span className="font-medium text-sm">Section</span>
|
|
64
|
+
{sectionPlane.enabled && (
|
|
65
|
+
<span className="text-xs text-primary font-mono">
|
|
66
|
+
{AXIS_INFO[sectionPlane.axis].label} <span className="inline-block w-12 text-right tabular-nums">{sectionPlane.position.toFixed(1)}%</span>
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
<ChevronDown className={`h-3 w-3 transition-transform ${isPanelCollapsed ? '-rotate-90' : ''}`} />
|
|
70
|
+
</button>
|
|
71
|
+
<div className="flex items-center gap-1">
|
|
72
|
+
{/* Only show 2D button when panel is closed */}
|
|
73
|
+
{!drawingPanelVisible && (
|
|
74
|
+
<Button variant="ghost" size="icon-sm" onClick={handleView2D} title="Open 2D Drawing Panel">
|
|
75
|
+
<FileImage className="h-3 w-3" />
|
|
76
|
+
</Button>
|
|
77
|
+
)}
|
|
78
|
+
<Button variant="ghost" size="icon-sm" onClick={handleClose} title="Close">
|
|
79
|
+
<X className="h-3 w-3" />
|
|
80
|
+
</Button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Expandable content */}
|
|
85
|
+
{!isPanelCollapsed && (
|
|
86
|
+
<div className="border-t px-3 pb-3 min-w-64">
|
|
87
|
+
{/* Direction Selection */}
|
|
88
|
+
<div className="mt-3">
|
|
89
|
+
<label className="text-xs text-muted-foreground mb-2 block">Direction</label>
|
|
90
|
+
<div className="flex gap-1">
|
|
91
|
+
{(['down', 'front', 'side'] as const).map((axis) => (
|
|
92
|
+
<Button
|
|
93
|
+
key={axis}
|
|
94
|
+
variant={sectionPlane.axis === axis ? 'default' : 'outline'}
|
|
95
|
+
size="sm"
|
|
96
|
+
className="flex-1 flex-col h-auto py-1.5"
|
|
97
|
+
onClick={() => handleAxisChange(axis)}
|
|
98
|
+
>
|
|
99
|
+
<span className="text-xs font-medium">{AXIS_INFO[axis].label}</span>
|
|
100
|
+
</Button>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Position Slider */}
|
|
106
|
+
<div className="mt-3">
|
|
107
|
+
<div className="flex items-center justify-between mb-1">
|
|
108
|
+
<label className="text-xs text-muted-foreground">Position</label>
|
|
109
|
+
<input
|
|
110
|
+
type="number"
|
|
111
|
+
min="0"
|
|
112
|
+
max="100"
|
|
113
|
+
step="0.1"
|
|
114
|
+
value={sectionPlane.position}
|
|
115
|
+
onChange={handlePositionChange}
|
|
116
|
+
className="w-16 text-xs font-mono bg-muted px-1.5 py-0.5 rounded border-none text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<input
|
|
120
|
+
type="range"
|
|
121
|
+
min="0"
|
|
122
|
+
max="100"
|
|
123
|
+
step="0.1"
|
|
124
|
+
value={sectionPlane.position}
|
|
125
|
+
onChange={handlePositionChange}
|
|
126
|
+
className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-primary"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Show 2D panel button - only when panel is closed */}
|
|
131
|
+
{!drawingPanelVisible && (
|
|
132
|
+
<div className="mt-3 pt-3 border-t">
|
|
133
|
+
<Button
|
|
134
|
+
variant="outline"
|
|
135
|
+
size="sm"
|
|
136
|
+
className="w-full"
|
|
137
|
+
onClick={handleView2D}
|
|
138
|
+
>
|
|
139
|
+
<FileImage className="h-4 w-4 mr-2" />
|
|
140
|
+
Open 2D Drawing
|
|
141
|
+
</Button>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Instruction hint - brutalist style matching Measure tool */}
|
|
149
|
+
<div
|
|
150
|
+
className="pointer-events-auto absolute bottom-16 left-1/2 -translate-x-1/2 z-30 bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 px-3 py-1.5 border-2 border-zinc-900 dark:border-zinc-100 transition-shadow duration-150"
|
|
151
|
+
style={{
|
|
152
|
+
boxShadow: sectionPlane.enabled
|
|
153
|
+
? '4px 4px 0px 0px #03A9F4' // Light blue shadow when active
|
|
154
|
+
: '3px 3px 0px 0px rgba(0,0,0,0.3)'
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<span className="font-mono text-xs uppercase tracking-wide">
|
|
158
|
+
{sectionPlane.enabled
|
|
159
|
+
? `Cutting ${AXIS_INFO[sectionPlane.axis].label.toLowerCase()} at ${sectionPlane.position.toFixed(1)}%`
|
|
160
|
+
: 'Preview mode'}
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Enable toggle - brutalist style matching Measure tool */}
|
|
165
|
+
<div className="pointer-events-auto absolute bottom-4 left-1/2 -translate-x-1/2 z-30">
|
|
166
|
+
<button
|
|
167
|
+
onClick={toggleSectionPlane}
|
|
168
|
+
className={`px-2 py-1 font-mono text-[10px] uppercase tracking-wider border-2 transition-colors ${
|
|
169
|
+
sectionPlane.enabled
|
|
170
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
171
|
+
: 'bg-zinc-100 dark:bg-zinc-900 text-zinc-500 border-zinc-300 dark:border-zinc-700'
|
|
172
|
+
}`}
|
|
173
|
+
title="Toggle section plane"
|
|
174
|
+
>
|
|
175
|
+
{sectionPlane.enabled ? 'Cutting' : 'Preview'}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Section plane visualization overlay */}
|
|
180
|
+
<SectionPlaneVisualization axis={sectionPlane.axis} enabled={sectionPlane.enabled} />
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Section plane visual indicator/gizmo
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AXIS_INFO } from './sectionConstants';
|
|
10
|
+
|
|
11
|
+
interface SectionPlaneVisualizationProps {
|
|
12
|
+
axis: 'down' | 'front' | 'side';
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Section plane visual indicator component
|
|
17
|
+
export function SectionPlaneVisualization({ axis, enabled }: SectionPlaneVisualizationProps) {
|
|
18
|
+
// Get the axis color
|
|
19
|
+
const axisColors = {
|
|
20
|
+
down: '#03A9F4', // Light blue for horizontal cuts
|
|
21
|
+
front: '#4CAF50', // Green for front cuts
|
|
22
|
+
side: '#FF9800', // Orange for side cuts
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const color = axisColors[axis];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
className="absolute inset-0 pointer-events-none z-20"
|
|
30
|
+
style={{ overflow: 'visible', pointerEvents: 'none' }}
|
|
31
|
+
>
|
|
32
|
+
<defs>
|
|
33
|
+
<filter id="section-glow">
|
|
34
|
+
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
|
35
|
+
<feMerge>
|
|
36
|
+
<feMergeNode in="coloredBlur"/>
|
|
37
|
+
<feMergeNode in="SourceGraphic"/>
|
|
38
|
+
</feMerge>
|
|
39
|
+
</filter>
|
|
40
|
+
{/* Animated dash pattern */}
|
|
41
|
+
<pattern id="section-pattern" patternUnits="userSpaceOnUse" width="10" height="10">
|
|
42
|
+
<line x1="0" y1="0" x2="10" y2="10" stroke={color} strokeWidth="1" strokeOpacity="0.5"/>
|
|
43
|
+
</pattern>
|
|
44
|
+
</defs>
|
|
45
|
+
|
|
46
|
+
{/* Axis indicator in corner */}
|
|
47
|
+
<g transform="translate(24, 24)">
|
|
48
|
+
<circle cx="20" cy="20" r="18" fill={color} fillOpacity={enabled ? 0.2 : 0.1} stroke={color} strokeWidth={enabled ? 3 : 2} filter="url(#section-glow)"/>
|
|
49
|
+
<text
|
|
50
|
+
x="20"
|
|
51
|
+
y="20"
|
|
52
|
+
textAnchor="middle"
|
|
53
|
+
dominantBaseline="central"
|
|
54
|
+
fill={color}
|
|
55
|
+
fontFamily="monospace"
|
|
56
|
+
fontSize="11"
|
|
57
|
+
fontWeight="bold"
|
|
58
|
+
>
|
|
59
|
+
{AXIS_INFO[axis].label.toUpperCase()}
|
|
60
|
+
</text>
|
|
61
|
+
{/* Active indicator */}
|
|
62
|
+
{enabled && (
|
|
63
|
+
<text
|
|
64
|
+
x="20"
|
|
65
|
+
y="32"
|
|
66
|
+
textAnchor="middle"
|
|
67
|
+
fill={color}
|
|
68
|
+
fontFamily="monospace"
|
|
69
|
+
fontSize="7"
|
|
70
|
+
fontWeight="bold"
|
|
71
|
+
>
|
|
72
|
+
CUT
|
|
73
|
+
</text>
|
|
74
|
+
)}
|
|
75
|
+
</g>
|
|
76
|
+
</svg>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format a distance in meters to a human-readable string with appropriate units
|
|
7
|
+
*/
|
|
8
|
+
export function formatDistance(meters: number): string {
|
|
9
|
+
if (meters < 0.01) {
|
|
10
|
+
return `${(meters * 1000).toFixed(1)} mm`;
|
|
11
|
+
} else if (meters < 1) {
|
|
12
|
+
return `${(meters * 100).toFixed(1)} cm`;
|
|
13
|
+
} else if (meters < 1000) {
|
|
14
|
+
return `${meters.toFixed(3)} m`;
|
|
15
|
+
} else {
|
|
16
|
+
return `${(meters / 1000).toFixed(2)} km`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared constants for section tool components
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Axis display info for semantic names
|
|
10
|
+
export const AXIS_INFO = {
|
|
11
|
+
down: { label: 'Down', description: 'Horizontal cut (floor plan view)', icon: '\u2193' },
|
|
12
|
+
front: { label: 'Front', description: 'Vertical cut (elevation view)', icon: '\u2192' },
|
|
13
|
+
side: { label: 'Side', description: 'Vertical cut (side elevation)', icon: '\u2299' },
|
|
14
|
+
} as const;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Animation loop hook for the 3D viewport
|
|
7
|
+
* Handles requestAnimationFrame loop, camera update, ViewCube sync
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useEffect, type MutableRefObject, type RefObject } from 'react';
|
|
11
|
+
import type { Renderer } from '@ifc-lite/renderer';
|
|
12
|
+
import type { SectionPlane } from '@/store';
|
|
13
|
+
|
|
14
|
+
export interface UseAnimationLoopParams {
|
|
15
|
+
canvasRef: RefObject<HTMLCanvasElement | null>;
|
|
16
|
+
rendererRef: MutableRefObject<Renderer | null>;
|
|
17
|
+
isInitialized: boolean;
|
|
18
|
+
animationFrameRef: MutableRefObject<number | null>;
|
|
19
|
+
lastFrameTimeRef: MutableRefObject<number>;
|
|
20
|
+
mouseIsDraggingRef: MutableRefObject<boolean>;
|
|
21
|
+
activeToolRef: MutableRefObject<string>;
|
|
22
|
+
hiddenEntitiesRef: MutableRefObject<Set<number>>;
|
|
23
|
+
isolatedEntitiesRef: MutableRefObject<Set<number> | null>;
|
|
24
|
+
selectedEntityIdRef: MutableRefObject<number | null>;
|
|
25
|
+
selectedModelIndexRef: MutableRefObject<number | undefined>;
|
|
26
|
+
clearColorRef: MutableRefObject<[number, number, number, number]>;
|
|
27
|
+
sectionPlaneRef: MutableRefObject<SectionPlane>;
|
|
28
|
+
sectionRangeRef: MutableRefObject<{ min: number; max: number } | null>;
|
|
29
|
+
lastCameraStateRef: MutableRefObject<{
|
|
30
|
+
position: { x: number; y: number; z: number };
|
|
31
|
+
rotation: { azimuth: number; elevation: number };
|
|
32
|
+
distance: number;
|
|
33
|
+
canvasWidth: number;
|
|
34
|
+
canvasHeight: number;
|
|
35
|
+
} | null>;
|
|
36
|
+
updateCameraRotationRealtime: (rotation: { azimuth: number; elevation: number }) => void;
|
|
37
|
+
calculateScale: () => void;
|
|
38
|
+
updateMeasurementScreenCoords: (projector: (worldPos: { x: number; y: number; z: number }) => { x: number; y: number } | null) => void;
|
|
39
|
+
hasPendingMeasurements: () => boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useAnimationLoop(params: UseAnimationLoopParams): void {
|
|
43
|
+
const {
|
|
44
|
+
canvasRef,
|
|
45
|
+
rendererRef,
|
|
46
|
+
isInitialized,
|
|
47
|
+
animationFrameRef,
|
|
48
|
+
lastFrameTimeRef,
|
|
49
|
+
mouseIsDraggingRef,
|
|
50
|
+
activeToolRef,
|
|
51
|
+
hiddenEntitiesRef,
|
|
52
|
+
isolatedEntitiesRef,
|
|
53
|
+
selectedEntityIdRef,
|
|
54
|
+
selectedModelIndexRef,
|
|
55
|
+
clearColorRef,
|
|
56
|
+
sectionPlaneRef,
|
|
57
|
+
sectionRangeRef,
|
|
58
|
+
lastCameraStateRef,
|
|
59
|
+
updateCameraRotationRealtime,
|
|
60
|
+
calculateScale,
|
|
61
|
+
updateMeasurementScreenCoords,
|
|
62
|
+
hasPendingMeasurements,
|
|
63
|
+
} = params;
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const renderer = rendererRef.current;
|
|
67
|
+
const canvas = canvasRef.current;
|
|
68
|
+
if (!renderer || !canvas || !isInitialized) return;
|
|
69
|
+
|
|
70
|
+
const camera = renderer.getCamera();
|
|
71
|
+
let aborted = false;
|
|
72
|
+
|
|
73
|
+
// Animation loop - update ViewCube in real-time
|
|
74
|
+
let lastRotationUpdate = 0;
|
|
75
|
+
let lastScaleUpdate = 0;
|
|
76
|
+
const animate = (currentTime: number) => {
|
|
77
|
+
if (aborted) return;
|
|
78
|
+
|
|
79
|
+
const deltaTime = currentTime - lastFrameTimeRef.current;
|
|
80
|
+
lastFrameTimeRef.current = currentTime;
|
|
81
|
+
|
|
82
|
+
const isAnimating = camera.update(deltaTime);
|
|
83
|
+
if (isAnimating) {
|
|
84
|
+
renderer.render({
|
|
85
|
+
hiddenIds: hiddenEntitiesRef.current,
|
|
86
|
+
isolatedIds: isolatedEntitiesRef.current,
|
|
87
|
+
selectedId: selectedEntityIdRef.current,
|
|
88
|
+
selectedModelIndex: selectedModelIndexRef.current,
|
|
89
|
+
clearColor: clearColorRef.current,
|
|
90
|
+
sectionPlane: activeToolRef.current === 'section' ? {
|
|
91
|
+
...sectionPlaneRef.current,
|
|
92
|
+
min: sectionRangeRef.current?.min,
|
|
93
|
+
max: sectionRangeRef.current?.max,
|
|
94
|
+
} : undefined,
|
|
95
|
+
});
|
|
96
|
+
// Update ViewCube during camera animation (e.g., preset view transitions)
|
|
97
|
+
updateCameraRotationRealtime(camera.getRotation());
|
|
98
|
+
calculateScale();
|
|
99
|
+
} else if (!mouseIsDraggingRef.current && currentTime - lastRotationUpdate > 500) {
|
|
100
|
+
// Update camera rotation for ViewCube when not dragging (throttled to every 500ms when idle)
|
|
101
|
+
updateCameraRotationRealtime(camera.getRotation());
|
|
102
|
+
lastRotationUpdate = currentTime;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Update scale bar (throttled to every 500ms - scale rarely needs frequent updates)
|
|
106
|
+
if (currentTime - lastScaleUpdate > 500) {
|
|
107
|
+
calculateScale();
|
|
108
|
+
lastScaleUpdate = currentTime;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Update measurement screen coordinates only when:
|
|
112
|
+
// 1. Measure tool is active (not in other modes)
|
|
113
|
+
// 2. Measurements exist
|
|
114
|
+
// 3. Camera actually changed
|
|
115
|
+
// This prevents unnecessary store updates and re-renders when not measuring
|
|
116
|
+
if (activeToolRef.current === 'measure') {
|
|
117
|
+
if (hasPendingMeasurements()) {
|
|
118
|
+
const cameraPos = camera.getPosition();
|
|
119
|
+
const cameraRot = camera.getRotation();
|
|
120
|
+
const cameraDist = camera.getDistance();
|
|
121
|
+
const currentCameraState = {
|
|
122
|
+
position: cameraPos,
|
|
123
|
+
rotation: cameraRot,
|
|
124
|
+
distance: cameraDist,
|
|
125
|
+
canvasWidth: canvas.width,
|
|
126
|
+
canvasHeight: canvas.height,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Check if camera state changed
|
|
130
|
+
const lastState = lastCameraStateRef.current;
|
|
131
|
+
const cameraChanged =
|
|
132
|
+
!lastState ||
|
|
133
|
+
lastState.position.x !== currentCameraState.position.x ||
|
|
134
|
+
lastState.position.y !== currentCameraState.position.y ||
|
|
135
|
+
lastState.position.z !== currentCameraState.position.z ||
|
|
136
|
+
lastState.rotation.azimuth !== currentCameraState.rotation.azimuth ||
|
|
137
|
+
lastState.rotation.elevation !== currentCameraState.rotation.elevation ||
|
|
138
|
+
lastState.distance !== currentCameraState.distance ||
|
|
139
|
+
lastState.canvasWidth !== currentCameraState.canvasWidth ||
|
|
140
|
+
lastState.canvasHeight !== currentCameraState.canvasHeight;
|
|
141
|
+
|
|
142
|
+
if (cameraChanged) {
|
|
143
|
+
lastCameraStateRef.current = currentCameraState;
|
|
144
|
+
updateMeasurementScreenCoords((worldPos) => {
|
|
145
|
+
return camera.projectToScreen(worldPos, canvas.width, canvas.height);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
152
|
+
};
|
|
153
|
+
lastFrameTimeRef.current = performance.now();
|
|
154
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
aborted = true;
|
|
158
|
+
if (animationFrameRef.current !== null) {
|
|
159
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
160
|
+
animationFrameRef.current = null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}, [isInitialized]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default useAnimationLoop;
|