@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,365 @@
|
|
|
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
|
+
* Hook for 2D measurement tool logic
|
|
7
|
+
* Extracts pan/measure mouse handling, snapping, orthogonal constraints,
|
|
8
|
+
* and keyboard/global-mouseup effects from Section2DPanel.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
12
|
+
import type { Drawing2D } from '@ifc-lite/drawing-2d';
|
|
13
|
+
|
|
14
|
+
// ─── Public interfaces ──────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface UseMeasure2DParams {
|
|
17
|
+
drawing: Drawing2D | null;
|
|
18
|
+
viewTransform: { x: number; y: number; scale: number };
|
|
19
|
+
setViewTransform: React.Dispatch<React.SetStateAction<{ x: number; y: number; scale: number }>>;
|
|
20
|
+
sectionAxis: 'down' | 'front' | 'side';
|
|
21
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
22
|
+
// Store state
|
|
23
|
+
measure2DMode: boolean;
|
|
24
|
+
measure2DStart: { x: number; y: number } | null;
|
|
25
|
+
measure2DCurrent: { x: number; y: number } | null;
|
|
26
|
+
measure2DShiftLocked: boolean;
|
|
27
|
+
measure2DLockedAxis: 'x' | 'y' | null;
|
|
28
|
+
setMeasure2DStart: (pt: { x: number; y: number }) => void;
|
|
29
|
+
setMeasure2DCurrent: (pt: { x: number; y: number }) => void;
|
|
30
|
+
setMeasure2DShiftLocked: (locked: boolean, axis?: 'x' | 'y') => void;
|
|
31
|
+
setMeasure2DSnapPoint: (pt: { x: number; y: number } | null) => void;
|
|
32
|
+
cancelMeasure2D: () => void;
|
|
33
|
+
completeMeasure2D: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseMeasure2DResult {
|
|
37
|
+
handleMouseDown: (e: React.MouseEvent) => void;
|
|
38
|
+
handleMouseMove: (e: React.MouseEvent) => void;
|
|
39
|
+
handleMouseUp: () => void;
|
|
40
|
+
handleMouseLeave: () => void;
|
|
41
|
+
handleMouseEnter: (e: React.MouseEvent) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Hook implementation ────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function useMeasure2D({
|
|
47
|
+
drawing,
|
|
48
|
+
viewTransform,
|
|
49
|
+
setViewTransform,
|
|
50
|
+
sectionAxis,
|
|
51
|
+
containerRef,
|
|
52
|
+
measure2DMode,
|
|
53
|
+
measure2DStart,
|
|
54
|
+
measure2DCurrent,
|
|
55
|
+
measure2DShiftLocked,
|
|
56
|
+
measure2DLockedAxis,
|
|
57
|
+
setMeasure2DStart,
|
|
58
|
+
setMeasure2DCurrent,
|
|
59
|
+
setMeasure2DShiftLocked,
|
|
60
|
+
setMeasure2DSnapPoint,
|
|
61
|
+
cancelMeasure2D,
|
|
62
|
+
completeMeasure2D,
|
|
63
|
+
}: UseMeasure2DParams): UseMeasure2DResult {
|
|
64
|
+
// ── Internal refs ───────────────────────────────────────────────────────
|
|
65
|
+
const isPanning = useRef(false);
|
|
66
|
+
const lastPanPoint = useRef({ x: 0, y: 0 });
|
|
67
|
+
const isMouseButtonDown = useRef(false);
|
|
68
|
+
const isMouseInsidePanel = useRef(true);
|
|
69
|
+
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
71
|
+
// 2D MEASURE TOOL HELPER FUNCTIONS
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
// Convert screen coordinates to drawing coordinates
|
|
75
|
+
const screenToDrawing = useCallback((screenX: number, screenY: number): { x: number; y: number } => {
|
|
76
|
+
// Screen coord → drawing coord
|
|
77
|
+
// Apply axis-specific inverse transforms (matching canvas rendering)
|
|
78
|
+
const currentAxis = sectionAxis;
|
|
79
|
+
const flipY = currentAxis !== 'down'; // Only flip Y for front/side views
|
|
80
|
+
const flipX = currentAxis === 'side'; // Flip X for side view
|
|
81
|
+
|
|
82
|
+
// Inverse of: screenX = drawingX * scaleX + transform.x
|
|
83
|
+
// where scaleX = flipX ? -scale : scale
|
|
84
|
+
const scaleX = flipX ? -viewTransform.scale : viewTransform.scale;
|
|
85
|
+
const scaleY = flipY ? -viewTransform.scale : viewTransform.scale;
|
|
86
|
+
|
|
87
|
+
const x = (screenX - viewTransform.x) / scaleX;
|
|
88
|
+
const y = (screenY - viewTransform.y) / scaleY;
|
|
89
|
+
return { x, y };
|
|
90
|
+
}, [viewTransform, sectionAxis]);
|
|
91
|
+
|
|
92
|
+
// Find nearest point on a line segment
|
|
93
|
+
const nearestPointOnSegment = useCallback((
|
|
94
|
+
p: { x: number; y: number },
|
|
95
|
+
a: { x: number; y: number },
|
|
96
|
+
b: { x: number; y: number }
|
|
97
|
+
): { point: { x: number; y: number }; dist: number } => {
|
|
98
|
+
const dx = b.x - a.x;
|
|
99
|
+
const dy = b.y - a.y;
|
|
100
|
+
const lenSq = dx * dx + dy * dy;
|
|
101
|
+
|
|
102
|
+
if (lenSq < 0.0001) {
|
|
103
|
+
// Degenerate segment
|
|
104
|
+
const d = Math.sqrt((p.x - a.x) ** 2 + (p.y - a.y) ** 2);
|
|
105
|
+
return { point: a, dist: d };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parameter t along segment [0,1]
|
|
109
|
+
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq;
|
|
110
|
+
t = Math.max(0, Math.min(1, t));
|
|
111
|
+
|
|
112
|
+
const nearest = { x: a.x + t * dx, y: a.y + t * dy };
|
|
113
|
+
const dist = Math.sqrt((p.x - nearest.x) ** 2 + (p.y - nearest.y) ** 2);
|
|
114
|
+
|
|
115
|
+
return { point: nearest, dist };
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
// Find snap point near cursor (check polygon vertices, edges, and line endpoints)
|
|
119
|
+
const findSnapPoint = useCallback((drawingCoord: { x: number; y: number }): { x: number; y: number } | null => {
|
|
120
|
+
if (!drawing) return null;
|
|
121
|
+
|
|
122
|
+
const snapThreshold = 10 / viewTransform.scale; // 10 screen pixels
|
|
123
|
+
let bestSnap: { x: number; y: number } | null = null;
|
|
124
|
+
let bestDist = snapThreshold;
|
|
125
|
+
|
|
126
|
+
// Priority 1: Check polygon vertices (endpoints are highest priority)
|
|
127
|
+
for (const polygon of drawing.cutPolygons) {
|
|
128
|
+
for (const pt of polygon.polygon.outer) {
|
|
129
|
+
const dist = Math.sqrt((pt.x - drawingCoord.x) ** 2 + (pt.y - drawingCoord.y) ** 2);
|
|
130
|
+
if (dist < bestDist * 0.7) { // Vertices get priority (70% threshold)
|
|
131
|
+
return { x: pt.x, y: pt.y }; // Return immediately for vertex snaps
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const hole of polygon.polygon.holes) {
|
|
135
|
+
for (const pt of hole) {
|
|
136
|
+
const dist = Math.sqrt((pt.x - drawingCoord.x) ** 2 + (pt.y - drawingCoord.y) ** 2);
|
|
137
|
+
if (dist < bestDist * 0.7) {
|
|
138
|
+
return { x: pt.x, y: pt.y };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Priority 2: Check line endpoints
|
|
145
|
+
for (const line of drawing.lines) {
|
|
146
|
+
const { start, end } = line.line;
|
|
147
|
+
for (const pt of [start, end]) {
|
|
148
|
+
const dist = Math.sqrt((pt.x - drawingCoord.x) ** 2 + (pt.y - drawingCoord.y) ** 2);
|
|
149
|
+
if (dist < bestDist * 0.7) {
|
|
150
|
+
return { x: pt.x, y: pt.y };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Priority 3: Check polygon edges
|
|
156
|
+
for (const polygon of drawing.cutPolygons) {
|
|
157
|
+
const outer = polygon.polygon.outer;
|
|
158
|
+
for (let i = 0; i < outer.length; i++) {
|
|
159
|
+
const a = outer[i];
|
|
160
|
+
const b = outer[(i + 1) % outer.length];
|
|
161
|
+
const { point, dist } = nearestPointOnSegment(drawingCoord, a, b);
|
|
162
|
+
if (dist < bestDist) {
|
|
163
|
+
bestDist = dist;
|
|
164
|
+
bestSnap = point;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const hole of polygon.polygon.holes) {
|
|
168
|
+
for (let i = 0; i < hole.length; i++) {
|
|
169
|
+
const a = hole[i];
|
|
170
|
+
const b = hole[(i + 1) % hole.length];
|
|
171
|
+
const { point, dist } = nearestPointOnSegment(drawingCoord, a, b);
|
|
172
|
+
if (dist < bestDist) {
|
|
173
|
+
bestDist = dist;
|
|
174
|
+
bestSnap = point;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Priority 4: Check drawing lines
|
|
181
|
+
for (const line of drawing.lines) {
|
|
182
|
+
const { start, end } = line.line;
|
|
183
|
+
const { point, dist } = nearestPointOnSegment(drawingCoord, start, end);
|
|
184
|
+
if (dist < bestDist) {
|
|
185
|
+
bestDist = dist;
|
|
186
|
+
bestSnap = point;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return bestSnap;
|
|
191
|
+
}, [drawing, viewTransform.scale, nearestPointOnSegment]);
|
|
192
|
+
|
|
193
|
+
// Apply orthogonal constraint if shift is held
|
|
194
|
+
const applyOrthogonalConstraint = useCallback((start: { x: number; y: number }, current: { x: number; y: number }, lockedAxis: 'x' | 'y' | null): { x: number; y: number } => {
|
|
195
|
+
if (!lockedAxis) return current;
|
|
196
|
+
|
|
197
|
+
if (lockedAxis === 'x') {
|
|
198
|
+
return { x: current.x, y: start.y };
|
|
199
|
+
} else {
|
|
200
|
+
return { x: start.x, y: current.y };
|
|
201
|
+
}
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
205
|
+
// EFFECTS
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
207
|
+
|
|
208
|
+
// Keyboard handlers for shift key (orthogonal constraint)
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!measure2DMode) return;
|
|
211
|
+
|
|
212
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
213
|
+
if (e.key === 'Shift' && measure2DStart && measure2DCurrent && !measure2DShiftLocked) {
|
|
214
|
+
// Determine axis based on dominant direction
|
|
215
|
+
const dx = Math.abs(measure2DCurrent.x - measure2DStart.x);
|
|
216
|
+
const dy = Math.abs(measure2DCurrent.y - measure2DStart.y);
|
|
217
|
+
const axis = dx > dy ? 'x' : 'y';
|
|
218
|
+
setMeasure2DShiftLocked(true, axis);
|
|
219
|
+
}
|
|
220
|
+
if (e.key === 'Escape') {
|
|
221
|
+
cancelMeasure2D();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
226
|
+
if (e.key === 'Shift') {
|
|
227
|
+
setMeasure2DShiftLocked(false);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
232
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
233
|
+
|
|
234
|
+
return () => {
|
|
235
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
236
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
237
|
+
};
|
|
238
|
+
}, [measure2DMode, measure2DStart, measure2DCurrent, measure2DShiftLocked, setMeasure2DShiftLocked, cancelMeasure2D]);
|
|
239
|
+
|
|
240
|
+
// Global mouseup handler to cancel measurement if released outside panel
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (!measure2DMode) return;
|
|
243
|
+
|
|
244
|
+
const handleGlobalMouseUp = (e: MouseEvent) => {
|
|
245
|
+
// If mouse button is released and we're outside the panel with a measurement started, cancel it
|
|
246
|
+
if (!isMouseInsidePanel.current && measure2DStart && e.button === 0) {
|
|
247
|
+
cancelMeasure2D();
|
|
248
|
+
}
|
|
249
|
+
isMouseButtonDown.current = false;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
window.addEventListener('mouseup', handleGlobalMouseUp);
|
|
253
|
+
return () => {
|
|
254
|
+
window.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
255
|
+
};
|
|
256
|
+
}, [measure2DMode, measure2DStart, cancelMeasure2D]);
|
|
257
|
+
|
|
258
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
259
|
+
// PAN / MEASURE HANDLERS
|
|
260
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
261
|
+
|
|
262
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
263
|
+
if (e.button !== 0) return;
|
|
264
|
+
|
|
265
|
+
isMouseButtonDown.current = true;
|
|
266
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
267
|
+
if (!rect) return;
|
|
268
|
+
|
|
269
|
+
const screenX = e.clientX - rect.left;
|
|
270
|
+
const screenY = e.clientY - rect.top;
|
|
271
|
+
|
|
272
|
+
if (measure2DMode) {
|
|
273
|
+
// Measure mode: set start point
|
|
274
|
+
const drawingCoord = screenToDrawing(screenX, screenY);
|
|
275
|
+
const snapPoint = findSnapPoint(drawingCoord);
|
|
276
|
+
const startPoint = snapPoint || drawingCoord;
|
|
277
|
+
setMeasure2DStart(startPoint);
|
|
278
|
+
setMeasure2DCurrent(startPoint);
|
|
279
|
+
} else {
|
|
280
|
+
// Pan mode
|
|
281
|
+
isPanning.current = true;
|
|
282
|
+
lastPanPoint.current = { x: e.clientX, y: e.clientY };
|
|
283
|
+
}
|
|
284
|
+
}, [measure2DMode, screenToDrawing, findSnapPoint, setMeasure2DStart, setMeasure2DCurrent]);
|
|
285
|
+
|
|
286
|
+
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
|
287
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
288
|
+
if (!rect) return;
|
|
289
|
+
|
|
290
|
+
const screenX = e.clientX - rect.left;
|
|
291
|
+
const screenY = e.clientY - rect.top;
|
|
292
|
+
|
|
293
|
+
if (measure2DMode) {
|
|
294
|
+
const drawingCoord = screenToDrawing(screenX, screenY);
|
|
295
|
+
|
|
296
|
+
// Find snap point and update
|
|
297
|
+
const snapPoint = findSnapPoint(drawingCoord);
|
|
298
|
+
setMeasure2DSnapPoint(snapPoint);
|
|
299
|
+
|
|
300
|
+
if (measure2DStart) {
|
|
301
|
+
// If measuring, update current point
|
|
302
|
+
let currentPoint = snapPoint || drawingCoord;
|
|
303
|
+
|
|
304
|
+
// Apply orthogonal constraint if shift is held
|
|
305
|
+
if (measure2DShiftLocked && measure2DLockedAxis) {
|
|
306
|
+
currentPoint = applyOrthogonalConstraint(measure2DStart, currentPoint, measure2DLockedAxis);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
setMeasure2DCurrent(currentPoint);
|
|
310
|
+
}
|
|
311
|
+
} else if (isPanning.current) {
|
|
312
|
+
// Pan mode
|
|
313
|
+
const dx = e.clientX - lastPanPoint.current.x;
|
|
314
|
+
const dy = e.clientY - lastPanPoint.current.y;
|
|
315
|
+
lastPanPoint.current = { x: e.clientX, y: e.clientY };
|
|
316
|
+
setViewTransform((prev) => ({
|
|
317
|
+
...prev,
|
|
318
|
+
x: prev.x + dx,
|
|
319
|
+
y: prev.y + dy,
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
}, [measure2DMode, measure2DStart, measure2DShiftLocked, measure2DLockedAxis, screenToDrawing, findSnapPoint, setMeasure2DSnapPoint, setMeasure2DCurrent, applyOrthogonalConstraint]);
|
|
323
|
+
|
|
324
|
+
const handleMouseUp = useCallback(() => {
|
|
325
|
+
isMouseButtonDown.current = false;
|
|
326
|
+
if (measure2DMode && measure2DStart && measure2DCurrent) {
|
|
327
|
+
// Complete the measurement
|
|
328
|
+
completeMeasure2D();
|
|
329
|
+
}
|
|
330
|
+
isPanning.current = false;
|
|
331
|
+
}, [measure2DMode, measure2DStart, measure2DCurrent, completeMeasure2D]);
|
|
332
|
+
|
|
333
|
+
const handleMouseLeave = useCallback(() => {
|
|
334
|
+
isMouseInsidePanel.current = false;
|
|
335
|
+
// Don't cancel if button is still down - user might re-enter
|
|
336
|
+
// Cancel will happen on global mouseup if released outside
|
|
337
|
+
isPanning.current = false;
|
|
338
|
+
}, []);
|
|
339
|
+
|
|
340
|
+
const handleMouseEnter = useCallback((e: React.MouseEvent) => {
|
|
341
|
+
isMouseInsidePanel.current = true;
|
|
342
|
+
// If re-entering with button down and measurement started, resume tracking
|
|
343
|
+
if (isMouseButtonDown.current && measure2DMode && measure2DStart) {
|
|
344
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
345
|
+
if (rect) {
|
|
346
|
+
const screenX = e.clientX - rect.left;
|
|
347
|
+
const screenY = e.clientY - rect.top;
|
|
348
|
+
const drawingCoord = screenToDrawing(screenX, screenY);
|
|
349
|
+
const snapPoint = findSnapPoint(drawingCoord);
|
|
350
|
+
const currentPoint = snapPoint || drawingCoord;
|
|
351
|
+
setMeasure2DCurrent(currentPoint);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}, [measure2DMode, measure2DStart, screenToDrawing, findSnapPoint, setMeasure2DCurrent]);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
handleMouseDown,
|
|
358
|
+
handleMouseMove,
|
|
359
|
+
handleMouseUp,
|
|
360
|
+
handleMouseLeave,
|
|
361
|
+
handleMouseEnter,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export default useMeasure2D;
|
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import type { Drawing2D, DrawingSheet } from '@ifc-lite/drawing-2d';
|
|
7
|
+
|
|
8
|
+
interface UseViewControlsParams {
|
|
9
|
+
drawing: Drawing2D | null;
|
|
10
|
+
sectionPlane: { axis: 'down' | 'front' | 'side'; position: number; flipped: boolean };
|
|
11
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
12
|
+
panelVisible: boolean;
|
|
13
|
+
status: string;
|
|
14
|
+
sheetEnabled: boolean;
|
|
15
|
+
activeSheet: DrawingSheet | null;
|
|
16
|
+
isPinned: boolean;
|
|
17
|
+
cachedSheetTransformRef: React.MutableRefObject<{
|
|
18
|
+
translateX: number;
|
|
19
|
+
translateY: number;
|
|
20
|
+
scaleFactor: number;
|
|
21
|
+
} | null>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface UseViewControlsResult {
|
|
25
|
+
viewTransform: { x: number; y: number; scale: number };
|
|
26
|
+
setViewTransform: React.Dispatch<React.SetStateAction<{ x: number; y: number; scale: number }>>;
|
|
27
|
+
zoomIn: () => void;
|
|
28
|
+
zoomOut: () => void;
|
|
29
|
+
fitToView: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function useViewControls({
|
|
33
|
+
drawing,
|
|
34
|
+
sectionPlane,
|
|
35
|
+
containerRef,
|
|
36
|
+
panelVisible,
|
|
37
|
+
status,
|
|
38
|
+
sheetEnabled,
|
|
39
|
+
activeSheet,
|
|
40
|
+
isPinned,
|
|
41
|
+
cachedSheetTransformRef,
|
|
42
|
+
}: UseViewControlsParams): UseViewControlsResult {
|
|
43
|
+
const [viewTransform, setViewTransform] = useState({ x: 0, y: 0, scale: 1 });
|
|
44
|
+
const [needsFit, setNeedsFit] = useState(true); // Force fit on first open and axis change
|
|
45
|
+
const prevAxisRef = useRef(sectionPlane.axis); // Track axis changes
|
|
46
|
+
|
|
47
|
+
// Wheel zoom handler
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
// Only attach handler when panel is visible
|
|
50
|
+
if (!panelVisible) return;
|
|
51
|
+
|
|
52
|
+
const container = containerRef.current;
|
|
53
|
+
if (!container) {
|
|
54
|
+
// Container not ready yet, try again on next render
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const wheelHandler = (e: WheelEvent) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
e.stopPropagation();
|
|
61
|
+
const delta = e.deltaY > 0 ? 0.9 : 1.1;
|
|
62
|
+
const rect = container.getBoundingClientRect();
|
|
63
|
+
|
|
64
|
+
const x = e.clientX - rect.left;
|
|
65
|
+
const y = e.clientY - rect.top;
|
|
66
|
+
|
|
67
|
+
setViewTransform((prev) => {
|
|
68
|
+
const newScale = Math.max(0.01, prev.scale * delta);
|
|
69
|
+
const scaleRatio = newScale / prev.scale;
|
|
70
|
+
return {
|
|
71
|
+
scale: newScale,
|
|
72
|
+
x: x - (x - prev.x) * scaleRatio,
|
|
73
|
+
y: y - (y - prev.y) * scaleRatio,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
container.addEventListener('wheel', wheelHandler, { passive: false });
|
|
79
|
+
return () => {
|
|
80
|
+
container.removeEventListener('wheel', wheelHandler);
|
|
81
|
+
};
|
|
82
|
+
}, [panelVisible, status]); // Re-run when panel visibility or status changes to ensure container is ready
|
|
83
|
+
|
|
84
|
+
// Zoom controls - unlimited zoom
|
|
85
|
+
const zoomIn = useCallback(() => {
|
|
86
|
+
setViewTransform((prev) => ({ ...prev, scale: prev.scale * 1.2 })); // No upper limit
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const zoomOut = useCallback(() => {
|
|
90
|
+
setViewTransform((prev) => ({ ...prev, scale: Math.max(0.01, prev.scale / 1.2) }));
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const fitToView = useCallback(() => {
|
|
94
|
+
if (!drawing || !containerRef.current) return;
|
|
95
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
96
|
+
|
|
97
|
+
// Sheet mode: fit the entire paper into view
|
|
98
|
+
if (sheetEnabled && activeSheet) {
|
|
99
|
+
const paperWidth = activeSheet.paper.widthMm;
|
|
100
|
+
const paperHeight = activeSheet.paper.heightMm;
|
|
101
|
+
|
|
102
|
+
// Calculate scale to fit paper with padding (10% margin on each side)
|
|
103
|
+
const padding = 0.1;
|
|
104
|
+
const availableWidth = rect.width * (1 - 2 * padding);
|
|
105
|
+
const availableHeight = rect.height * (1 - 2 * padding);
|
|
106
|
+
const scaleX = availableWidth / paperWidth;
|
|
107
|
+
const scaleY = availableHeight / paperHeight;
|
|
108
|
+
const scale = Math.min(scaleX, scaleY);
|
|
109
|
+
|
|
110
|
+
// Center the paper in the view
|
|
111
|
+
setViewTransform({
|
|
112
|
+
scale,
|
|
113
|
+
x: (rect.width - paperWidth * scale) / 2,
|
|
114
|
+
y: (rect.height - paperHeight * scale) / 2,
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Non-sheet mode: fit the drawing bounds
|
|
120
|
+
const { bounds } = drawing;
|
|
121
|
+
const width = bounds.max.x - bounds.min.x;
|
|
122
|
+
const height = bounds.max.y - bounds.min.y;
|
|
123
|
+
|
|
124
|
+
if (width < 0.001 || height < 0.001) return;
|
|
125
|
+
|
|
126
|
+
// Calculate scale to fit with padding (15% margin on each side)
|
|
127
|
+
const padding = 0.15;
|
|
128
|
+
const availableWidth = rect.width * (1 - 2 * padding);
|
|
129
|
+
const availableHeight = rect.height * (1 - 2 * padding);
|
|
130
|
+
const scaleX = availableWidth / width;
|
|
131
|
+
const scaleY = availableHeight / height;
|
|
132
|
+
// No artificial cap - let it zoom to fit the content
|
|
133
|
+
const scale = Math.min(scaleX, scaleY);
|
|
134
|
+
|
|
135
|
+
// Center the drawing in the view with axis-specific transforms
|
|
136
|
+
// Must match the canvas rendering transforms:
|
|
137
|
+
// - 'down' (plan view): no Y flip
|
|
138
|
+
// - 'front'/'side': Y flip
|
|
139
|
+
// - 'side': X flip
|
|
140
|
+
const currentAxis = sectionPlane.axis;
|
|
141
|
+
const flipY = currentAxis !== 'down';
|
|
142
|
+
const flipX = currentAxis === 'side';
|
|
143
|
+
|
|
144
|
+
const centerX = (bounds.min.x + bounds.max.x) / 2;
|
|
145
|
+
const centerY = (bounds.min.y + bounds.max.y) / 2;
|
|
146
|
+
|
|
147
|
+
// Apply transforms matching canvas rendering
|
|
148
|
+
const adjustedCenterX = flipX ? -centerX : centerX;
|
|
149
|
+
const adjustedCenterY = flipY ? -centerY : centerY;
|
|
150
|
+
|
|
151
|
+
setViewTransform({
|
|
152
|
+
scale,
|
|
153
|
+
x: rect.width / 2 - adjustedCenterX * scale,
|
|
154
|
+
y: rect.height / 2 - adjustedCenterY * scale,
|
|
155
|
+
});
|
|
156
|
+
}, [drawing, sheetEnabled, activeSheet, sectionPlane.axis]);
|
|
157
|
+
|
|
158
|
+
// Track axis changes for forced fit-to-view
|
|
159
|
+
const lastFitAxisRef = useRef(sectionPlane.axis);
|
|
160
|
+
|
|
161
|
+
// Set needsFit when axis changes
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (sectionPlane.axis !== prevAxisRef.current) {
|
|
164
|
+
prevAxisRef.current = sectionPlane.axis;
|
|
165
|
+
setNeedsFit(true); // Force fit when axis changes
|
|
166
|
+
cachedSheetTransformRef.current = null; // Clear cached transform for new axis
|
|
167
|
+
}
|
|
168
|
+
}, [sectionPlane.axis]);
|
|
169
|
+
|
|
170
|
+
// Track previous sheet mode to detect toggle
|
|
171
|
+
const prevSheetEnabledRef = useRef(sheetEnabled);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (sheetEnabled !== prevSheetEnabledRef.current) {
|
|
174
|
+
prevSheetEnabledRef.current = sheetEnabled;
|
|
175
|
+
cachedSheetTransformRef.current = null; // Clear cached transform
|
|
176
|
+
// Auto-fit when sheet mode is toggled
|
|
177
|
+
if (status === 'ready' && drawing && containerRef.current) {
|
|
178
|
+
const timeout = setTimeout(() => {
|
|
179
|
+
fitToView();
|
|
180
|
+
}, 50);
|
|
181
|
+
return () => clearTimeout(timeout);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}, [sheetEnabled, status, drawing, fitToView]);
|
|
185
|
+
|
|
186
|
+
// Auto-fit when: (1) needsFit is true (first open or axis change), or (2) not pinned after regenerate
|
|
187
|
+
// ALWAYS fit when axis changed, regardless of pin state
|
|
188
|
+
// Also re-run when panelVisible changes so we fit when panel opens with existing drawing
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (status === 'ready' && drawing && containerRef.current && panelVisible) {
|
|
191
|
+
const axisChanged = lastFitAxisRef.current !== sectionPlane.axis;
|
|
192
|
+
|
|
193
|
+
// Fit if needsFit (first open/axis change) OR if not pinned OR if axis just changed
|
|
194
|
+
if (needsFit || !isPinned || axisChanged) {
|
|
195
|
+
// Small delay to ensure canvas is rendered
|
|
196
|
+
const timeout = setTimeout(() => {
|
|
197
|
+
fitToView();
|
|
198
|
+
lastFitAxisRef.current = sectionPlane.axis;
|
|
199
|
+
if (needsFit) {
|
|
200
|
+
setNeedsFit(false); // Clear the flag after fitting
|
|
201
|
+
}
|
|
202
|
+
}, 50);
|
|
203
|
+
return () => clearTimeout(timeout);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}, [status, drawing, fitToView, isPinned, needsFit, sectionPlane.axis, panelVisible]);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
viewTransform,
|
|
210
|
+
setViewTransform,
|
|
211
|
+
zoomIn,
|
|
212
|
+
zoomOut,
|
|
213
|
+
fitToView,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { useViewControls };
|
|
218
|
+
export default useViewControls;
|