@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,644 @@
|
|
|
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
|
+
* SVG measurement overlay visualizations (lines, labels, snap indicators)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useMemo } from 'react';
|
|
10
|
+
import type { Measurement, SnapVisualization } from '@/store';
|
|
11
|
+
import type { MeasurementConstraintEdge } from '@/store/types';
|
|
12
|
+
import { SnapType, type SnapTarget } from '@ifc-lite/renderer';
|
|
13
|
+
import { formatDistance } from './formatDistance';
|
|
14
|
+
|
|
15
|
+
export interface MeasurementOverlaysProps {
|
|
16
|
+
measurements: Measurement[];
|
|
17
|
+
pending: { screenX: number; screenY: number } | null;
|
|
18
|
+
activeMeasurement: { start: { screenX: number; screenY: number; x: number; y: number; z: number }; current: { screenX: number; screenY: number }; distance: number } | null;
|
|
19
|
+
snapTarget: SnapTarget | null;
|
|
20
|
+
snapVisualization: SnapVisualization | null;
|
|
21
|
+
hoverPosition?: { x: number; y: number } | null;
|
|
22
|
+
projectToScreen?: (worldPos: { x: number; y: number; z: number }) => { x: number; y: number } | null;
|
|
23
|
+
constraintEdge?: MeasurementConstraintEdge | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const MeasurementOverlays = React.memo(function MeasurementOverlays({ measurements, pending, activeMeasurement, snapTarget, snapVisualization, hoverPosition, projectToScreen, constraintEdge }: MeasurementOverlaysProps) {
|
|
27
|
+
// Determine snap indicator position
|
|
28
|
+
// Priority: activeMeasurement.current > snapTarget projected position > hoverPosition (fallback)
|
|
29
|
+
const snapIndicatorPos = useMemo(() => {
|
|
30
|
+
// During active measurement, use the measurement's current position
|
|
31
|
+
if (activeMeasurement) {
|
|
32
|
+
return { x: activeMeasurement.current.screenX, y: activeMeasurement.current.screenY };
|
|
33
|
+
}
|
|
34
|
+
// During hover, project the snap target's world position to screen
|
|
35
|
+
// This ensures the indicator is at the actual snap point, not the cursor
|
|
36
|
+
if (snapTarget && projectToScreen) {
|
|
37
|
+
const projected = projectToScreen(snapTarget.position);
|
|
38
|
+
if (projected) {
|
|
39
|
+
return projected;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Fallback to hover position (cursor position)
|
|
43
|
+
return hoverPosition ?? null;
|
|
44
|
+
}, [
|
|
45
|
+
activeMeasurement?.current?.screenX,
|
|
46
|
+
activeMeasurement?.current?.screenY,
|
|
47
|
+
snapTarget?.position?.x,
|
|
48
|
+
snapTarget?.position?.y,
|
|
49
|
+
snapTarget?.position?.z,
|
|
50
|
+
projectToScreen,
|
|
51
|
+
hoverPosition?.x,
|
|
52
|
+
hoverPosition?.y,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{/* SVG filter definitions for glow effect */}
|
|
58
|
+
<svg className="absolute w-0 h-0 pointer-events-none" style={{ pointerEvents: 'none' }}>
|
|
59
|
+
<defs>
|
|
60
|
+
<filter id="glow">
|
|
61
|
+
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
|
62
|
+
<feMerge>
|
|
63
|
+
<feMergeNode in="coloredBlur"/>
|
|
64
|
+
<feMergeNode in="SourceGraphic"/>
|
|
65
|
+
</feMerge>
|
|
66
|
+
</filter>
|
|
67
|
+
<filter id="snap-glow">
|
|
68
|
+
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
|
|
69
|
+
<feMerge>
|
|
70
|
+
<feMergeNode in="coloredBlur"/>
|
|
71
|
+
<feMergeNode in="SourceGraphic"/>
|
|
72
|
+
</feMerge>
|
|
73
|
+
</filter>
|
|
74
|
+
</defs>
|
|
75
|
+
</svg>
|
|
76
|
+
|
|
77
|
+
{/* Completed measurements */}
|
|
78
|
+
{measurements.map((m) => (
|
|
79
|
+
<div key={m.id} className="pointer-events-none">
|
|
80
|
+
{/* Line connecting start and end */}
|
|
81
|
+
<svg
|
|
82
|
+
className="absolute inset-0 pointer-events-none z-20"
|
|
83
|
+
style={{ overflow: 'visible', pointerEvents: 'none' }}
|
|
84
|
+
>
|
|
85
|
+
<line
|
|
86
|
+
x1={m.start.screenX}
|
|
87
|
+
y1={m.start.screenY}
|
|
88
|
+
x2={m.end.screenX}
|
|
89
|
+
y2={m.end.screenY}
|
|
90
|
+
stroke="hsl(var(--primary))"
|
|
91
|
+
strokeWidth="2"
|
|
92
|
+
strokeDasharray="6,3"
|
|
93
|
+
filter="url(#glow)"
|
|
94
|
+
/>
|
|
95
|
+
{/* Start point */}
|
|
96
|
+
<circle
|
|
97
|
+
cx={m.start.screenX}
|
|
98
|
+
cy={m.start.screenY}
|
|
99
|
+
r="5"
|
|
100
|
+
fill="white"
|
|
101
|
+
stroke="hsl(var(--primary))"
|
|
102
|
+
strokeWidth="2"
|
|
103
|
+
/>
|
|
104
|
+
{/* End point */}
|
|
105
|
+
<circle
|
|
106
|
+
cx={m.end.screenX}
|
|
107
|
+
cy={m.end.screenY}
|
|
108
|
+
r="5"
|
|
109
|
+
fill="white"
|
|
110
|
+
stroke="hsl(var(--primary))"
|
|
111
|
+
strokeWidth="2"
|
|
112
|
+
/>
|
|
113
|
+
</svg>
|
|
114
|
+
|
|
115
|
+
{/* Distance label at midpoint - brutalist style */}
|
|
116
|
+
<div
|
|
117
|
+
className="absolute pointer-events-none z-20 bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 px-2 py-1 font-mono text-xs font-bold -translate-x-1/2 -translate-y-1/2 border-2 border-zinc-900 dark:border-zinc-100 shadow-[2px_2px_0px_0px_rgba(0,0,0,0.3)]"
|
|
118
|
+
style={{
|
|
119
|
+
left: (m.start.screenX + m.end.screenX) / 2,
|
|
120
|
+
top: (m.start.screenY + m.end.screenY) / 2,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{formatDistance(m.distance)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
))}
|
|
127
|
+
|
|
128
|
+
{/* Active measurement (live preview while dragging) */}
|
|
129
|
+
{activeMeasurement && (
|
|
130
|
+
<div className="pointer-events-none">
|
|
131
|
+
<svg
|
|
132
|
+
className="absolute inset-0 pointer-events-none z-20"
|
|
133
|
+
style={{ overflow: 'visible', pointerEvents: 'none' }}
|
|
134
|
+
>
|
|
135
|
+
{/* Animated dashed line (marching ants effect) */}
|
|
136
|
+
<line
|
|
137
|
+
x1={activeMeasurement.start.screenX}
|
|
138
|
+
y1={activeMeasurement.start.screenY}
|
|
139
|
+
x2={activeMeasurement.current.screenX}
|
|
140
|
+
y2={activeMeasurement.current.screenY}
|
|
141
|
+
stroke="hsl(var(--primary))"
|
|
142
|
+
strokeWidth="2"
|
|
143
|
+
strokeDasharray="6,3"
|
|
144
|
+
strokeOpacity="0.7"
|
|
145
|
+
filter="url(#glow)"
|
|
146
|
+
/>
|
|
147
|
+
{/* Start point */}
|
|
148
|
+
<circle
|
|
149
|
+
cx={activeMeasurement.start.screenX}
|
|
150
|
+
cy={activeMeasurement.start.screenY}
|
|
151
|
+
r="6"
|
|
152
|
+
fill="white"
|
|
153
|
+
stroke="hsl(var(--primary))"
|
|
154
|
+
strokeWidth="2"
|
|
155
|
+
filter="url(#glow)"
|
|
156
|
+
/>
|
|
157
|
+
{/* Current point (slightly larger, pulsing) */}
|
|
158
|
+
<circle
|
|
159
|
+
cx={activeMeasurement.current.screenX}
|
|
160
|
+
cy={activeMeasurement.current.screenY}
|
|
161
|
+
r="7"
|
|
162
|
+
fill="white"
|
|
163
|
+
stroke="hsl(var(--primary))"
|
|
164
|
+
strokeWidth="2"
|
|
165
|
+
filter="url(#glow)"
|
|
166
|
+
className="animate-pulse"
|
|
167
|
+
/>
|
|
168
|
+
</svg>
|
|
169
|
+
|
|
170
|
+
{/* Live distance label - brutalist style */}
|
|
171
|
+
<div
|
|
172
|
+
className="absolute pointer-events-none z-20 bg-primary text-primary-foreground px-2.5 py-1 font-mono text-sm font-bold -translate-x-1/2 -translate-y-1/2 border-2 border-primary shadow-[3px_3px_0px_0px_rgba(0,0,0,0.2)]"
|
|
173
|
+
style={{
|
|
174
|
+
left: (activeMeasurement.start.screenX + activeMeasurement.current.screenX) / 2,
|
|
175
|
+
top: (activeMeasurement.start.screenY + activeMeasurement.current.screenY) / 2,
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{formatDistance(activeMeasurement.distance)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* Orthogonal constraint axes visualization */}
|
|
184
|
+
{activeMeasurement && constraintEdge?.activeAxis && projectToScreen && (() => {
|
|
185
|
+
const startWorld = activeMeasurement.start;
|
|
186
|
+
const startScreen = { x: startWorld.screenX, y: startWorld.screenY };
|
|
187
|
+
|
|
188
|
+
// Project axis endpoints to screen space
|
|
189
|
+
const axisLength = 2.0; // 2 meters in world space
|
|
190
|
+
|
|
191
|
+
const { axis1, axis2, axis3 } = constraintEdge.axes;
|
|
192
|
+
const colors = constraintEdge.colors;
|
|
193
|
+
|
|
194
|
+
// Calculate endpoints along each axis (positive and negative)
|
|
195
|
+
const axis1End = projectToScreen({
|
|
196
|
+
x: startWorld.x + axis1.x * axisLength,
|
|
197
|
+
y: startWorld.y + axis1.y * axisLength,
|
|
198
|
+
z: startWorld.z + axis1.z * axisLength,
|
|
199
|
+
});
|
|
200
|
+
const axis1Neg = projectToScreen({
|
|
201
|
+
x: startWorld.x - axis1.x * axisLength,
|
|
202
|
+
y: startWorld.y - axis1.y * axisLength,
|
|
203
|
+
z: startWorld.z - axis1.z * axisLength,
|
|
204
|
+
});
|
|
205
|
+
const axis2End = projectToScreen({
|
|
206
|
+
x: startWorld.x + axis2.x * axisLength,
|
|
207
|
+
y: startWorld.y + axis2.y * axisLength,
|
|
208
|
+
z: startWorld.z + axis2.z * axisLength,
|
|
209
|
+
});
|
|
210
|
+
const axis2Neg = projectToScreen({
|
|
211
|
+
x: startWorld.x - axis2.x * axisLength,
|
|
212
|
+
y: startWorld.y - axis2.y * axisLength,
|
|
213
|
+
z: startWorld.z - axis2.z * axisLength,
|
|
214
|
+
});
|
|
215
|
+
const axis3End = projectToScreen({
|
|
216
|
+
x: startWorld.x + axis3.x * axisLength,
|
|
217
|
+
y: startWorld.y + axis3.y * axisLength,
|
|
218
|
+
z: startWorld.z + axis3.z * axisLength,
|
|
219
|
+
});
|
|
220
|
+
const axis3Neg = projectToScreen({
|
|
221
|
+
x: startWorld.x - axis3.x * axisLength,
|
|
222
|
+
y: startWorld.y - axis3.y * axisLength,
|
|
223
|
+
z: startWorld.z - axis3.z * axisLength,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!axis1End || !axis1Neg || !axis2End || !axis2Neg || !axis3End || !axis3Neg) return null;
|
|
227
|
+
|
|
228
|
+
const activeAxis = constraintEdge.activeAxis;
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<svg
|
|
232
|
+
className="absolute inset-0 pointer-events-none z-25"
|
|
233
|
+
style={{ overflow: 'visible', pointerEvents: 'none' }}
|
|
234
|
+
>
|
|
235
|
+
{/* Axis 1 */}
|
|
236
|
+
<line
|
|
237
|
+
x1={axis1Neg.x}
|
|
238
|
+
y1={axis1Neg.y}
|
|
239
|
+
x2={axis1End.x}
|
|
240
|
+
y2={axis1End.y}
|
|
241
|
+
stroke={colors.axis1}
|
|
242
|
+
strokeWidth={activeAxis === 'axis1' ? 3 : 1.5}
|
|
243
|
+
strokeOpacity={activeAxis === 'axis1' ? 0.9 : 0.3}
|
|
244
|
+
strokeDasharray={activeAxis === 'axis1' ? 'none' : '4,4'}
|
|
245
|
+
strokeLinecap="round"
|
|
246
|
+
/>
|
|
247
|
+
{/* Axis 2 */}
|
|
248
|
+
<line
|
|
249
|
+
x1={axis2Neg.x}
|
|
250
|
+
y1={axis2Neg.y}
|
|
251
|
+
x2={axis2End.x}
|
|
252
|
+
y2={axis2End.y}
|
|
253
|
+
stroke={colors.axis2}
|
|
254
|
+
strokeWidth={activeAxis === 'axis2' ? 3 : 1.5}
|
|
255
|
+
strokeOpacity={activeAxis === 'axis2' ? 0.9 : 0.3}
|
|
256
|
+
strokeDasharray={activeAxis === 'axis2' ? 'none' : '4,4'}
|
|
257
|
+
strokeLinecap="round"
|
|
258
|
+
/>
|
|
259
|
+
{/* Axis 3 */}
|
|
260
|
+
<line
|
|
261
|
+
x1={axis3Neg.x}
|
|
262
|
+
y1={axis3Neg.y}
|
|
263
|
+
x2={axis3End.x}
|
|
264
|
+
y2={axis3End.y}
|
|
265
|
+
stroke={colors.axis3}
|
|
266
|
+
strokeWidth={activeAxis === 'axis3' ? 3 : 1.5}
|
|
267
|
+
strokeOpacity={activeAxis === 'axis3' ? 0.9 : 0.3}
|
|
268
|
+
strokeDasharray={activeAxis === 'axis3' ? 'none' : '4,4'}
|
|
269
|
+
strokeLinecap="round"
|
|
270
|
+
/>
|
|
271
|
+
{/* Center origin dot */}
|
|
272
|
+
<circle
|
|
273
|
+
cx={startScreen.x}
|
|
274
|
+
cy={startScreen.y}
|
|
275
|
+
r="4"
|
|
276
|
+
fill="white"
|
|
277
|
+
stroke={colors[activeAxis]}
|
|
278
|
+
strokeWidth="2"
|
|
279
|
+
/>
|
|
280
|
+
</svg>
|
|
281
|
+
);
|
|
282
|
+
})()}
|
|
283
|
+
|
|
284
|
+
{/* Edge highlight - draw full edge in 3D-projected screen space */}
|
|
285
|
+
{snapVisualization?.edgeLine3D && projectToScreen && (() => {
|
|
286
|
+
const start = projectToScreen(snapVisualization.edgeLine3D.v0);
|
|
287
|
+
const end = projectToScreen(snapVisualization.edgeLine3D.v1);
|
|
288
|
+
if (!start || !end) return null;
|
|
289
|
+
|
|
290
|
+
// Corner position (at v0 or v1)
|
|
291
|
+
const cornerPos = snapVisualization.cornerRings
|
|
292
|
+
? (snapVisualization.cornerRings.atStart ? start : end)
|
|
293
|
+
: null;
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<svg
|
|
297
|
+
className="absolute inset-0 pointer-events-none z-30"
|
|
298
|
+
style={{ overflow: 'visible', pointerEvents: 'none' }}
|
|
299
|
+
>
|
|
300
|
+
{/* Edge line with snap color (orange for edges) */}
|
|
301
|
+
<line
|
|
302
|
+
x1={start.x}
|
|
303
|
+
y1={start.y}
|
|
304
|
+
x2={end.x}
|
|
305
|
+
y2={end.y}
|
|
306
|
+
stroke="#FF9800"
|
|
307
|
+
strokeWidth="4"
|
|
308
|
+
strokeOpacity="0.9"
|
|
309
|
+
strokeLinecap="round"
|
|
310
|
+
filter="url(#snap-glow)"
|
|
311
|
+
/>
|
|
312
|
+
{/* Outer glow line for better visibility */}
|
|
313
|
+
<line
|
|
314
|
+
x1={start.x}
|
|
315
|
+
y1={start.y}
|
|
316
|
+
x2={end.x}
|
|
317
|
+
y2={end.y}
|
|
318
|
+
stroke="#FF9800"
|
|
319
|
+
strokeWidth="8"
|
|
320
|
+
strokeOpacity="0.3"
|
|
321
|
+
strokeLinecap="round"
|
|
322
|
+
/>
|
|
323
|
+
{/* Edge endpoints */}
|
|
324
|
+
<circle cx={start.x} cy={start.y} r="4" fill="#FF9800" fillOpacity="0.6" />
|
|
325
|
+
<circle cx={end.x} cy={end.y} r="4" fill="#FF9800" fillOpacity="0.6" />
|
|
326
|
+
|
|
327
|
+
{/* Corner rings - shows strong attraction at corners */}
|
|
328
|
+
{cornerPos && snapVisualization.cornerRings && (
|
|
329
|
+
<>
|
|
330
|
+
{/* Outer pulsing ring */}
|
|
331
|
+
<circle
|
|
332
|
+
cx={cornerPos.x}
|
|
333
|
+
cy={cornerPos.y}
|
|
334
|
+
r="18"
|
|
335
|
+
fill="none"
|
|
336
|
+
stroke="#FFEB3B"
|
|
337
|
+
strokeWidth="2"
|
|
338
|
+
strokeOpacity="0.4"
|
|
339
|
+
className="animate-pulse"
|
|
340
|
+
/>
|
|
341
|
+
{/* Middle ring */}
|
|
342
|
+
<circle
|
|
343
|
+
cx={cornerPos.x}
|
|
344
|
+
cy={cornerPos.y}
|
|
345
|
+
r="12"
|
|
346
|
+
fill="none"
|
|
347
|
+
stroke="#FFEB3B"
|
|
348
|
+
strokeWidth="2"
|
|
349
|
+
strokeOpacity="0.6"
|
|
350
|
+
/>
|
|
351
|
+
{/* Inner ring */}
|
|
352
|
+
<circle
|
|
353
|
+
cx={cornerPos.x}
|
|
354
|
+
cy={cornerPos.y}
|
|
355
|
+
r="6"
|
|
356
|
+
fill="#FFEB3B"
|
|
357
|
+
fillOpacity="0.8"
|
|
358
|
+
stroke="white"
|
|
359
|
+
strokeWidth="1"
|
|
360
|
+
/>
|
|
361
|
+
{/* Center dot */}
|
|
362
|
+
<circle
|
|
363
|
+
cx={cornerPos.x}
|
|
364
|
+
cy={cornerPos.y}
|
|
365
|
+
r="2"
|
|
366
|
+
fill="white"
|
|
367
|
+
/>
|
|
368
|
+
{/* Valence indicators (small dots around corner) */}
|
|
369
|
+
{snapVisualization.cornerRings.valence >= 3 && (
|
|
370
|
+
<>
|
|
371
|
+
<circle cx={cornerPos.x - 10} cy={cornerPos.y} r="2" fill="#FFEB3B" fillOpacity="0.7" />
|
|
372
|
+
<circle cx={cornerPos.x + 10} cy={cornerPos.y} r="2" fill="#FFEB3B" fillOpacity="0.7" />
|
|
373
|
+
<circle cx={cornerPos.x} cy={cornerPos.y - 10} r="2" fill="#FFEB3B" fillOpacity="0.7" />
|
|
374
|
+
</>
|
|
375
|
+
)}
|
|
376
|
+
</>
|
|
377
|
+
)}
|
|
378
|
+
</svg>
|
|
379
|
+
);
|
|
380
|
+
})()}
|
|
381
|
+
|
|
382
|
+
{/* Plane indicator - subtle grid/cross for face snaps */}
|
|
383
|
+
{snapVisualization?.planeIndicator && (
|
|
384
|
+
<svg
|
|
385
|
+
className="absolute inset-0 pointer-events-none z-25"
|
|
386
|
+
style={{ overflow: 'visible', pointerEvents: 'none' }}
|
|
387
|
+
>
|
|
388
|
+
{/* Cross indicator */}
|
|
389
|
+
<line
|
|
390
|
+
x1={snapVisualization.planeIndicator.x - 20}
|
|
391
|
+
y1={snapVisualization.planeIndicator.y}
|
|
392
|
+
x2={snapVisualization.planeIndicator.x + 20}
|
|
393
|
+
y2={snapVisualization.planeIndicator.y}
|
|
394
|
+
stroke="hsl(var(--primary))"
|
|
395
|
+
strokeWidth="2"
|
|
396
|
+
strokeOpacity="0.4"
|
|
397
|
+
/>
|
|
398
|
+
<line
|
|
399
|
+
x1={snapVisualization.planeIndicator.x}
|
|
400
|
+
y1={snapVisualization.planeIndicator.y - 20}
|
|
401
|
+
x2={snapVisualization.planeIndicator.x}
|
|
402
|
+
y2={snapVisualization.planeIndicator.y + 20}
|
|
403
|
+
stroke="hsl(var(--primary))"
|
|
404
|
+
strokeWidth="2"
|
|
405
|
+
strokeOpacity="0.4"
|
|
406
|
+
/>
|
|
407
|
+
{/* Small circle at center */}
|
|
408
|
+
<circle
|
|
409
|
+
cx={snapVisualization.planeIndicator.x}
|
|
410
|
+
cy={snapVisualization.planeIndicator.y}
|
|
411
|
+
r="4"
|
|
412
|
+
fill="hsl(var(--primary))"
|
|
413
|
+
fillOpacity="0.6"
|
|
414
|
+
/>
|
|
415
|
+
</svg>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
{/* Snap indicator */}
|
|
419
|
+
{snapTarget && snapIndicatorPos && (
|
|
420
|
+
<SnapIndicator
|
|
421
|
+
screenX={snapIndicatorPos.x}
|
|
422
|
+
screenY={snapIndicatorPos.y}
|
|
423
|
+
snapType={snapTarget.type}
|
|
424
|
+
/>
|
|
425
|
+
)}
|
|
426
|
+
|
|
427
|
+
{/* Pending point (legacy - keep for backward compatibility) */}
|
|
428
|
+
{pending && !activeMeasurement && (
|
|
429
|
+
<svg
|
|
430
|
+
className="absolute inset-0 pointer-events-none z-20"
|
|
431
|
+
style={{ overflow: 'visible', pointerEvents: 'none' }}
|
|
432
|
+
>
|
|
433
|
+
<circle
|
|
434
|
+
cx={pending.screenX}
|
|
435
|
+
cy={pending.screenY}
|
|
436
|
+
r="5"
|
|
437
|
+
fill="none"
|
|
438
|
+
stroke="hsl(var(--primary))"
|
|
439
|
+
strokeWidth="1.5"
|
|
440
|
+
/>
|
|
441
|
+
<circle
|
|
442
|
+
cx={pending.screenX}
|
|
443
|
+
cy={pending.screenY}
|
|
444
|
+
r="2.5"
|
|
445
|
+
fill="hsl(var(--primary))"
|
|
446
|
+
/>
|
|
447
|
+
</svg>
|
|
448
|
+
)}
|
|
449
|
+
</>
|
|
450
|
+
);
|
|
451
|
+
}, (prevProps, nextProps) => {
|
|
452
|
+
// Custom comparison to prevent unnecessary re-renders
|
|
453
|
+
// Return true if props are equal (skip re-render), false if different (re-render)
|
|
454
|
+
|
|
455
|
+
// Compare measurements - check both IDs AND screen coordinates
|
|
456
|
+
if (prevProps.measurements.length !== nextProps.measurements.length) return false;
|
|
457
|
+
for (let i = 0; i < prevProps.measurements.length; i++) {
|
|
458
|
+
const prev = prevProps.measurements[i];
|
|
459
|
+
const next = nextProps.measurements[i];
|
|
460
|
+
if (!next || prev.id !== next.id) return false;
|
|
461
|
+
// Check screen coordinates for zoom/camera changes
|
|
462
|
+
if (prev.start.screenX !== next.start.screenX || prev.start.screenY !== next.start.screenY) return false;
|
|
463
|
+
if (prev.end.screenX !== next.end.screenX || prev.end.screenY !== next.end.screenY) return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Compare activeMeasurement - check if it exists and if position changed
|
|
467
|
+
if (!!prevProps.activeMeasurement !== !!nextProps.activeMeasurement) return false;
|
|
468
|
+
if (prevProps.activeMeasurement && nextProps.activeMeasurement) {
|
|
469
|
+
if (
|
|
470
|
+
prevProps.activeMeasurement.current.screenX !== nextProps.activeMeasurement.current.screenX ||
|
|
471
|
+
prevProps.activeMeasurement.current.screenY !== nextProps.activeMeasurement.current.screenY ||
|
|
472
|
+
prevProps.activeMeasurement.start.screenX !== nextProps.activeMeasurement.start.screenX ||
|
|
473
|
+
prevProps.activeMeasurement.start.screenY !== nextProps.activeMeasurement.start.screenY
|
|
474
|
+
) return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Compare snapTarget - check type and position
|
|
478
|
+
if (!!prevProps.snapTarget !== !!nextProps.snapTarget) return false;
|
|
479
|
+
if (prevProps.snapTarget && nextProps.snapTarget) {
|
|
480
|
+
if (
|
|
481
|
+
prevProps.snapTarget.type !== nextProps.snapTarget.type ||
|
|
482
|
+
prevProps.snapTarget.position.x !== nextProps.snapTarget.position.x ||
|
|
483
|
+
prevProps.snapTarget.position.y !== nextProps.snapTarget.position.y ||
|
|
484
|
+
prevProps.snapTarget.position.z !== nextProps.snapTarget.position.z
|
|
485
|
+
) return false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Compare snapVisualization
|
|
489
|
+
if (!!prevProps.snapVisualization !== !!nextProps.snapVisualization) return false;
|
|
490
|
+
if (prevProps.snapVisualization && nextProps.snapVisualization) {
|
|
491
|
+
// Compare edgeLine3D (3D world coordinates)
|
|
492
|
+
const prevEdge = prevProps.snapVisualization.edgeLine3D;
|
|
493
|
+
const nextEdge = nextProps.snapVisualization.edgeLine3D;
|
|
494
|
+
if (!!prevEdge !== !!nextEdge) return false;
|
|
495
|
+
if (prevEdge && nextEdge) {
|
|
496
|
+
if (
|
|
497
|
+
prevEdge.v0.x !== nextEdge.v0.x ||
|
|
498
|
+
prevEdge.v0.y !== nextEdge.v0.y ||
|
|
499
|
+
prevEdge.v0.z !== nextEdge.v0.z ||
|
|
500
|
+
prevEdge.v1.x !== nextEdge.v1.x ||
|
|
501
|
+
prevEdge.v1.y !== nextEdge.v1.y ||
|
|
502
|
+
prevEdge.v1.z !== nextEdge.v1.z
|
|
503
|
+
) return false;
|
|
504
|
+
}
|
|
505
|
+
// Compare slidingDot (t parameter only)
|
|
506
|
+
const prevDot = prevProps.snapVisualization.slidingDot;
|
|
507
|
+
const nextDot = nextProps.snapVisualization.slidingDot;
|
|
508
|
+
if (!!prevDot !== !!nextDot) return false;
|
|
509
|
+
if (prevDot && nextDot) {
|
|
510
|
+
if (prevDot.t !== nextDot.t) return false;
|
|
511
|
+
}
|
|
512
|
+
// Compare cornerRings (atStart + valence)
|
|
513
|
+
const prevCorner = prevProps.snapVisualization.cornerRings;
|
|
514
|
+
const nextCorner = nextProps.snapVisualization.cornerRings;
|
|
515
|
+
if (!!prevCorner !== !!nextCorner) return false;
|
|
516
|
+
if (prevCorner && nextCorner) {
|
|
517
|
+
if (
|
|
518
|
+
prevCorner.atStart !== nextCorner.atStart ||
|
|
519
|
+
prevCorner.valence !== nextCorner.valence
|
|
520
|
+
) return false;
|
|
521
|
+
}
|
|
522
|
+
const prevPlane = prevProps.snapVisualization.planeIndicator;
|
|
523
|
+
const nextPlane = nextProps.snapVisualization.planeIndicator;
|
|
524
|
+
if (!!prevPlane !== !!nextPlane) return false;
|
|
525
|
+
if (prevPlane && nextPlane) {
|
|
526
|
+
if (
|
|
527
|
+
prevPlane.x !== nextPlane.x ||
|
|
528
|
+
prevPlane.y !== nextPlane.y
|
|
529
|
+
) return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Compare projectToScreen (always re-render if it changes as we need it for projection)
|
|
534
|
+
if (prevProps.projectToScreen !== nextProps.projectToScreen) return false;
|
|
535
|
+
|
|
536
|
+
// Compare hoverPosition
|
|
537
|
+
if (prevProps.hoverPosition?.x !== nextProps.hoverPosition?.x ||
|
|
538
|
+
prevProps.hoverPosition?.y !== nextProps.hoverPosition?.y) return false;
|
|
539
|
+
|
|
540
|
+
// Compare pending
|
|
541
|
+
if (prevProps.pending?.screenX !== nextProps.pending?.screenX ||
|
|
542
|
+
prevProps.pending?.screenY !== nextProps.pending?.screenY) return false;
|
|
543
|
+
|
|
544
|
+
// Compare constraintEdge
|
|
545
|
+
if (!!prevProps.constraintEdge !== !!nextProps.constraintEdge) return false;
|
|
546
|
+
if (prevProps.constraintEdge && nextProps.constraintEdge) {
|
|
547
|
+
if (prevProps.constraintEdge.activeAxis !== nextProps.constraintEdge.activeAxis) return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return true; // All props are equal, skip re-render
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
interface SnapIndicatorProps {
|
|
554
|
+
screenX: number;
|
|
555
|
+
screenY: number;
|
|
556
|
+
snapType: SnapType;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function SnapIndicator({ screenX, screenY, snapType }: SnapIndicatorProps) {
|
|
560
|
+
// Distinct colors for each snap type - no labels needed, shapes are self-explanatory
|
|
561
|
+
const snapColors = {
|
|
562
|
+
[SnapType.VERTEX]: '#FFEB3B', // Yellow - circle = point
|
|
563
|
+
[SnapType.EDGE]: '#FF9800', // Orange - line = edge
|
|
564
|
+
[SnapType.FACE]: '#03A9F4', // Light Blue - square = face
|
|
565
|
+
[SnapType.FACE_CENTER]: '#00BCD4', // Cyan - square with dot = center
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const color = snapColors[snapType];
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<svg
|
|
572
|
+
className="absolute inset-0 pointer-events-none z-25"
|
|
573
|
+
style={{ overflow: 'visible', pointerEvents: 'none' }}
|
|
574
|
+
>
|
|
575
|
+
{/* Outer glow ring - subtle pulsing indicator */}
|
|
576
|
+
<circle
|
|
577
|
+
cx={screenX}
|
|
578
|
+
cy={screenY}
|
|
579
|
+
r="10"
|
|
580
|
+
fill="none"
|
|
581
|
+
stroke={color}
|
|
582
|
+
strokeWidth="1.5"
|
|
583
|
+
strokeOpacity="0.4"
|
|
584
|
+
filter="url(#snap-glow)"
|
|
585
|
+
/>
|
|
586
|
+
|
|
587
|
+
{/* Vertex: filled circle (point) */}
|
|
588
|
+
{snapType === SnapType.VERTEX && (
|
|
589
|
+
<>
|
|
590
|
+
<circle cx={screenX} cy={screenY} r="5" fill={color} opacity="0.3" />
|
|
591
|
+
<circle cx={screenX} cy={screenY} r="2.5" fill={color} />
|
|
592
|
+
</>
|
|
593
|
+
)}
|
|
594
|
+
|
|
595
|
+
{/* Edge: horizontal line with center dot */}
|
|
596
|
+
{snapType === SnapType.EDGE && (
|
|
597
|
+
<>
|
|
598
|
+
<line
|
|
599
|
+
x1={screenX - 8}
|
|
600
|
+
y1={screenY}
|
|
601
|
+
x2={screenX + 8}
|
|
602
|
+
y2={screenY}
|
|
603
|
+
stroke={color}
|
|
604
|
+
strokeWidth="2"
|
|
605
|
+
strokeLinecap="round"
|
|
606
|
+
/>
|
|
607
|
+
<circle cx={screenX} cy={screenY} r="2" fill={color} />
|
|
608
|
+
</>
|
|
609
|
+
)}
|
|
610
|
+
|
|
611
|
+
{/* Face: square outline */}
|
|
612
|
+
{snapType === SnapType.FACE && (
|
|
613
|
+
<>
|
|
614
|
+
<rect
|
|
615
|
+
x={screenX - 5}
|
|
616
|
+
y={screenY - 5}
|
|
617
|
+
width="10"
|
|
618
|
+
height="10"
|
|
619
|
+
fill={color}
|
|
620
|
+
fillOpacity="0.2"
|
|
621
|
+
stroke={color}
|
|
622
|
+
strokeWidth="1.5"
|
|
623
|
+
/>
|
|
624
|
+
</>
|
|
625
|
+
)}
|
|
626
|
+
|
|
627
|
+
{/* Face Center: square with center dot */}
|
|
628
|
+
{snapType === SnapType.FACE_CENTER && (
|
|
629
|
+
<>
|
|
630
|
+
<rect
|
|
631
|
+
x={screenX - 5}
|
|
632
|
+
y={screenY - 5}
|
|
633
|
+
width="10"
|
|
634
|
+
height="10"
|
|
635
|
+
fill="none"
|
|
636
|
+
stroke={color}
|
|
637
|
+
strokeWidth="1.5"
|
|
638
|
+
/>
|
|
639
|
+
<circle cx={screenX} cy={screenY} r="2" fill={color} />
|
|
640
|
+
</>
|
|
641
|
+
)}
|
|
642
|
+
</svg>
|
|
643
|
+
);
|
|
644
|
+
}
|