@ifc-lite/viewer 1.19.0 → 1.21.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/.turbo/turbo-build.log +59 -43
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +496 -0
- package/dist/assets/basketViewActivator-Bzw51jhm.js +71 -0
- package/dist/assets/{bcf-DOG9_WPX.js → bcf-4K724hw0.js} +18 -18
- package/dist/assets/decode-worker-t2EGKAxO.js +1708 -0
- package/dist/assets/drawing-2d-Bjy8YPrg.js +257 -0
- package/dist/assets/exporters-u0sz2Upj.js +259119 -0
- package/dist/assets/geometry-controller.worker-NH8pZmrU.js +7 -0
- package/dist/assets/geometry.worker-Bp4rW_R1.js +1 -0
- package/dist/assets/ids-B7AXEv7h.js +4067 -0
- package/dist/assets/ifc-lite-DfZHk36-.js +7 -0
- package/dist/assets/ifc-lite_bg-DlKs5-yM.wasm +0 -0
- package/dist/assets/ifc-lite_bg-PqmRe3Ph.wasm +0 -0
- package/dist/assets/index-CSWgTe1s.css +1 -0
- package/dist/assets/{index-BOi3BuUI.js → index-DVNSvEMh.js} +49877 -28410
- package/dist/assets/laz-perf-Cvr_Lepg.js +1 -0
- package/dist/assets/laz-perf-DnSyzVYH.wasm +0 -0
- package/dist/assets/{native-bridge-CpBeOPQa.js → native-bridge-BiD01jI9.js} +2 -2
- package/dist/assets/parser.worker-Bnbrl6gy.js +182 -0
- package/dist/assets/{sandbox-Baez7n-t.js → sandbox-DPD1ROr0.js} +548 -530
- package/dist/assets/{server-client-BB6cMAXE.js → server-client-DP8fMPY9.js} +1 -1
- package/dist/assets/three-CDRZThFA.js +4057 -0
- package/dist/assets/{wasm-bridge-CAYCUHbE.js → wasm-bridge-CErti6zX.js} +1 -1
- package/dist/assets/workerHelpers-CBbWSJmd.js +36 -0
- package/dist/index.html +10 -9
- package/dist/samples/building-architecture.ifc +453 -0
- package/dist/samples/hello-wall.ifc +1054 -0
- package/dist/samples/infra-bridge.ifc +962 -0
- package/index.html +1 -1
- package/package.json +15 -10
- package/public/samples/building-architecture.ifc +453 -0
- package/public/samples/hello-wall.ifc +1054 -0
- package/public/samples/infra-bridge.ifc +962 -0
- package/src/App.tsx +37 -3
- package/src/components/mcp/HeroScene.tsx +876 -0
- package/src/components/mcp/McpLanding.tsx +1318 -0
- package/src/components/mcp/McpPlayground.tsx +524 -0
- package/src/components/mcp/PlaygroundChat.tsx +1097 -0
- package/src/components/mcp/PlaygroundViewer.tsx +815 -0
- package/src/components/mcp/README.md +171 -0
- package/src/components/mcp/data.ts +659 -0
- package/src/components/mcp/playground-dispatcher.ts +1649 -0
- package/src/components/mcp/playground-files.ts +107 -0
- package/src/components/mcp/playground-uploads.ts +122 -0
- package/src/components/mcp/types.ts +65 -0
- package/src/components/mcp/use-mcp-page.ts +109 -0
- package/src/components/viewer/BasketPresentationDock.tsx +3 -0
- package/src/components/viewer/CesiumOverlay.tsx +165 -120
- package/src/components/viewer/DeviationPanel.tsx +172 -0
- package/src/components/viewer/HierarchyPanel.tsx +29 -3
- package/src/components/viewer/HoverTooltip.tsx +5 -0
- package/src/components/viewer/IDSAuditSummary.tsx +389 -0
- package/src/components/viewer/IDSPanel.tsx +80 -26
- package/src/components/viewer/MainToolbar.tsx +79 -7
- package/src/components/viewer/MergeLayersBanner.tsx +108 -0
- package/src/components/viewer/MobileToolbar.tsx +326 -0
- package/src/components/viewer/PointCloudClasses.tsx +111 -0
- package/src/components/viewer/PointCloudLegend.tsx +119 -0
- package/src/components/viewer/PointCloudPanel.tsx +52 -1
- package/src/components/viewer/PropertiesPanel.tsx +37 -6
- package/src/components/viewer/RectSelectionOverlay.tsx +48 -0
- package/src/components/viewer/StatusBar.tsx +14 -0
- package/src/components/viewer/ViewerLayout.tsx +288 -95
- package/src/components/viewer/Viewport.tsx +86 -18
- package/src/components/viewer/ViewportContainer.tsx +60 -15
- package/src/components/viewer/ViewportOverlays.tsx +41 -26
- package/src/components/viewer/mouseHandlerTypes.ts +22 -0
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +77 -8
- package/src/components/viewer/properties/MaterialCard.tsx +2 -2
- package/src/components/viewer/selectionHandlers.ts +41 -0
- package/src/components/viewer/tools/SectionPanel.tsx +181 -24
- package/src/components/viewer/tools/SectionVisualization.tsx +384 -3
- package/src/components/viewer/useAnimationLoop.ts +22 -0
- package/src/components/viewer/useMouseControls.ts +296 -3
- package/src/components/viewer/usePointCloudSync.ts +8 -1
- package/src/components/viewer/useRenderUpdates.ts +21 -1
- package/src/components/viewer/useTouchControls.ts +100 -41
- package/src/generated/mcp-catalog.json +82 -0
- package/src/hooks/federationLoadGate.test.ts +90 -0
- package/src/hooks/federationLoadGate.ts +127 -0
- package/src/hooks/ids/idsDataAccessor.ts +11 -259
- package/src/hooks/ingest/pointCloudIngest.ts +127 -16
- package/src/hooks/useDrawingGeneration.ts +81 -8
- package/src/hooks/useIDS.ts +90 -10
- package/src/hooks/useIfcFederation.ts +94 -16
- package/src/hooks/useIfcLoader.ts +289 -64
- package/src/hooks/useViewerSelectors.ts +10 -0
- package/src/lib/geo/cesium-bridge.ts +84 -67
- package/src/lib/geo/clamp-anchor.test.ts +80 -0
- package/src/lib/geo/clamp-anchor.ts +57 -0
- package/src/lib/geo/effective-georef.test.ts +79 -1
- package/src/lib/geo/effective-georef.ts +83 -0
- package/src/lib/geo/reproject.ts +26 -13
- package/src/lib/geo/terrain-elevation.ts +166 -0
- package/src/lib/lens/adapter.ts +1 -1
- package/src/lib/llm/context-builder.ts +1 -1
- package/src/lib/perf/memoryAccounting.test.ts +92 -0
- package/src/lib/perf/memoryAccounting.ts +235 -0
- package/src/sdk/adapters/mutation-view.ts +1 -1
- package/src/store/constants.ts +39 -2
- package/src/store/index.ts +6 -1
- package/src/store/slices/cesiumSlice.ts +1 -1
- package/src/store/slices/idsSlice.ts +24 -0
- package/src/store/slices/loadingSlice.ts +12 -0
- package/src/store/slices/pointCloudSlice.ts +72 -1
- package/src/store/slices/sectionSlice.test.ts +590 -1
- package/src/store/slices/sectionSlice.ts +344 -17
- package/src/store/slices/uiSlice.merge-layers.test.ts +217 -0
- package/src/store/slices/uiSlice.ts +60 -2
- package/src/store/types.ts +42 -0
- package/src/store.ts +13 -0
- package/src/utils/acquireFileBuffer.test.ts +231 -0
- package/src/utils/acquireFileBuffer.ts +128 -0
- package/src/utils/ifcConfig.ts +24 -0
- package/src/utils/nativeSpatialDataStore.ts +20 -2
- package/src/utils/spatialHierarchy.test.ts +116 -0
- package/src/utils/spatialHierarchy.ts +23 -0
- package/tailwind.config.js +5 -0
- package/tsconfig.json +1 -0
- package/vite.config.ts +12 -0
- package/dist/assets/basketViewActivator-RZy5c3Td.js +0 -1
- package/dist/assets/decode-worker-Collf_X_.js +0 -1320
- package/dist/assets/drawing-2d-DoxKMqbO.js +0 -257
- package/dist/assets/exporters-BraHBeoi.js +0 -81583
- package/dist/assets/geometry.worker-DQEZB2rB.js +0 -1
- package/dist/assets/ids-DQ5jY0E8.js +0 -1
- package/dist/assets/ifc-lite_bg-4yUkDRD8.wasm +0 -0
- package/dist/assets/index-0XpVr_S5.css +0 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* selection/context-menu interactions to selectionHandlers.ts.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { useEffect, type MutableRefObject, type RefObject } from 'react';
|
|
12
|
+
import { useEffect, useRef, type MutableRefObject, type RefObject } from 'react';
|
|
13
13
|
import type { Renderer, PickResult, SnapTarget } from '@ifc-lite/renderer';
|
|
14
14
|
import type { MeshData } from '@ifc-lite/geometry';
|
|
15
15
|
import type {
|
|
@@ -41,6 +41,12 @@ export interface MouseState {
|
|
|
41
41
|
startX: number;
|
|
42
42
|
startY: number;
|
|
43
43
|
didDrag: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* True while the user is mid-drag in rectangle-select mode (Ctrl/⌘
|
|
46
|
+
* held over the canvas in select tool). Suppresses orbit/pan in
|
|
47
|
+
* the drag handlers and triggers `pickRect` on mouseup.
|
|
48
|
+
*/
|
|
49
|
+
isRectSelecting?: boolean;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export interface UseMouseControlsParams {
|
|
@@ -57,6 +63,10 @@ export interface UseMouseControlsParams {
|
|
|
57
63
|
snapEnabledRef: MutableRefObject<boolean>;
|
|
58
64
|
edgeLockStateRef: MutableRefObject<EdgeLockState>;
|
|
59
65
|
measurementConstraintEdgeRef: MutableRefObject<MeasurementConstraintEdge | null>;
|
|
66
|
+
/** Section tool: when true, the next click picks a face for the clip plane (issue #243). */
|
|
67
|
+
sectionPickModeRef: MutableRefObject<boolean>;
|
|
68
|
+
/** Renderer model bounds; passed to face-pick so the cardinal-fallback `position` % is correct. */
|
|
69
|
+
modelBoundsRef: MutableRefObject<{ min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } } | null>;
|
|
60
70
|
|
|
61
71
|
// Visibility/selection refs
|
|
62
72
|
hiddenEntitiesRef: MutableRefObject<Set<number>>;
|
|
@@ -102,7 +112,20 @@ export interface UseMouseControlsParams {
|
|
|
102
112
|
|
|
103
113
|
// Callbacks
|
|
104
114
|
handlePickForSelection: (pickResult: PickResult | null) => void;
|
|
105
|
-
setHoverState: (state: {
|
|
115
|
+
setHoverState: (state: {
|
|
116
|
+
entityId: number;
|
|
117
|
+
screenX: number;
|
|
118
|
+
screenY: number;
|
|
119
|
+
worldXYZ?: { x: number; y: number; z: number };
|
|
120
|
+
}) => void;
|
|
121
|
+
/**
|
|
122
|
+
* Called during a rectangle-selection drag with the current rect
|
|
123
|
+
* (CSS pixels, canvas-relative). Passed `null` on drag end to clear
|
|
124
|
+
* any visual overlay. The hook handles the actual `pickRect` call
|
|
125
|
+
* + selection update internally; this callback is only for the
|
|
126
|
+
* overlay visual.
|
|
127
|
+
*/
|
|
128
|
+
setRectSelection?: (rect: { x0: number; y0: number; x1: number; y1: number } | null) => void;
|
|
106
129
|
clearHover: () => void;
|
|
107
130
|
openContextMenu: (entityId: number | null, screenX: number, screenY: number) => void;
|
|
108
131
|
startMeasurement: (point: MeasurePoint) => void;
|
|
@@ -122,6 +145,24 @@ export interface UseMouseControlsParams {
|
|
|
122
145
|
calculateScale: () => void;
|
|
123
146
|
getPickOptions: () => { isStreaming: boolean; hiddenIds: Set<number>; isolatedIds: Set<number> | null };
|
|
124
147
|
hasPendingMeasurements: () => boolean;
|
|
148
|
+
/** Section face-pick: set the clip plane through a world-space face (issue #243). */
|
|
149
|
+
setSectionPlaneFromFace: (
|
|
150
|
+
normal: [number, number, number],
|
|
151
|
+
point: [number, number, number],
|
|
152
|
+
bounds?: { min: [number, number, number]; max: [number, number, number] },
|
|
153
|
+
) => void;
|
|
154
|
+
/** Section face-pick: arm/disarm the "next click picks a face" mode. */
|
|
155
|
+
setSectionPickMode: (enabled: boolean) => void;
|
|
156
|
+
/**
|
|
157
|
+
* Section face-pick hover preview (issue #243 follow-up). Set by the
|
|
158
|
+
* dwell handler when the cursor pauses ~200ms over a face; cleared
|
|
159
|
+
* (passed `null`) when the cursor leaves the canvas, moves to a
|
|
160
|
+
* different face, or pick mode is disarmed. Purely visual — does not
|
|
161
|
+
* touch `sectionPlane`.
|
|
162
|
+
*/
|
|
163
|
+
setSectionPickPreview: (
|
|
164
|
+
preview: { normal: [number, number, number]; point: [number, number, number]; faceKey: string } | null,
|
|
165
|
+
) => void;
|
|
125
166
|
|
|
126
167
|
// Constants
|
|
127
168
|
HOVER_SNAP_THROTTLE_MS: number;
|
|
@@ -145,6 +186,8 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
145
186
|
snapEnabledRef,
|
|
146
187
|
edgeLockStateRef,
|
|
147
188
|
measurementConstraintEdgeRef,
|
|
189
|
+
sectionPickModeRef,
|
|
190
|
+
modelBoundsRef,
|
|
148
191
|
hiddenEntitiesRef,
|
|
149
192
|
isolatedEntitiesRef,
|
|
150
193
|
selectedEntityIdRef,
|
|
@@ -186,6 +229,10 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
186
229
|
calculateScale,
|
|
187
230
|
getPickOptions,
|
|
188
231
|
hasPendingMeasurements,
|
|
232
|
+
setSectionPlaneFromFace,
|
|
233
|
+
setSectionPickMode,
|
|
234
|
+
setSectionPickPreview,
|
|
235
|
+
setRectSelection,
|
|
189
236
|
HOVER_SNAP_THROTTLE_MS,
|
|
190
237
|
SLOW_RAYCAST_THRESHOLD_MS,
|
|
191
238
|
hoverThrottleMs,
|
|
@@ -194,6 +241,37 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
194
241
|
RENDER_THROTTLE_MS_HUGE,
|
|
195
242
|
} = params;
|
|
196
243
|
|
|
244
|
+
// ─── Section face-pick hover preview (issue #243 follow-up) ──────────
|
|
245
|
+
// Refs persist across render so the dwell timer + sticky-face state
|
|
246
|
+
// survive the throttled mousemove path. Critical for the anti-jitter
|
|
247
|
+
// contract: cursor wobble within the same triangle/face must NOT
|
|
248
|
+
// restart the dwell or repaint the overlay. See `handleSectionPickHover`
|
|
249
|
+
// in this file for the full UX rules.
|
|
250
|
+
const sectionDwellTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
251
|
+
const sectionLastFaceKeyRef = useRef<string | null>(null);
|
|
252
|
+
const sectionLastCastPosRef = useRef<{ x: number; y: number } | null>(null);
|
|
253
|
+
const sectionLastCastTsRef = useRef<number>(0);
|
|
254
|
+
|
|
255
|
+
// When `sectionPickMode` flips off (Esc, second toggle press, tool
|
|
256
|
+
// change), make sure any in-flight dwell timer is cancelled so it
|
|
257
|
+
// can't call `setSectionPickPreview(...)` after the slice has
|
|
258
|
+
// already been disarmed. The slice's own guard would no-op the
|
|
259
|
+
// call, but it's clearer to stop the timer at the source rather
|
|
260
|
+
// than relying on the late guard.
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
const unsub = useViewerStore.subscribe((s, prev) => {
|
|
263
|
+
if (prev.sectionPickMode && !s.sectionPickMode) {
|
|
264
|
+
if (sectionDwellTimerRef.current) {
|
|
265
|
+
clearTimeout(sectionDwellTimerRef.current);
|
|
266
|
+
sectionDwellTimerRef.current = null;
|
|
267
|
+
}
|
|
268
|
+
sectionLastFaceKeyRef.current = null;
|
|
269
|
+
sectionLastCastPosRef.current = null;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return unsub;
|
|
273
|
+
}, []);
|
|
274
|
+
|
|
197
275
|
useEffect(() => {
|
|
198
276
|
const canvas = canvasRef.current;
|
|
199
277
|
const renderer = rendererRef.current;
|
|
@@ -213,6 +291,8 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
213
291
|
snapEnabledRef,
|
|
214
292
|
edgeLockStateRef,
|
|
215
293
|
measurementConstraintEdgeRef,
|
|
294
|
+
sectionPickModeRef,
|
|
295
|
+
modelBoundsRef,
|
|
216
296
|
hiddenEntitiesRef,
|
|
217
297
|
isolatedEntitiesRef,
|
|
218
298
|
geometryRef,
|
|
@@ -240,10 +320,129 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
240
320
|
openContextMenu,
|
|
241
321
|
hasPendingMeasurements,
|
|
242
322
|
getPickOptions,
|
|
323
|
+
setSectionPlaneFromFace,
|
|
324
|
+
setSectionPickMode,
|
|
325
|
+
setSectionPickPreview,
|
|
243
326
|
HOVER_SNAP_THROTTLE_MS,
|
|
244
327
|
SLOW_RAYCAST_THRESHOLD_MS,
|
|
245
328
|
};
|
|
246
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Section face-pick hover preview (issue #243 follow-up).
|
|
332
|
+
*
|
|
333
|
+
* Anti-jitter contract — these are the rules the dwell handler
|
|
334
|
+
* MUST honour, in order:
|
|
335
|
+
* 1. < 16ms since last raycast → skip (60fps cap).
|
|
336
|
+
* 2. < 2px movement since last raycast → skip (cheap throttle).
|
|
337
|
+
* 3. No hit OR degenerate normal → cancel timer + clear preview.
|
|
338
|
+
* 4. Hit on the SAME face as last cast → no-op (don't restart
|
|
339
|
+
* dwell, don't repaint — this is the critical rule that keeps
|
|
340
|
+
* cursor wobble inside a flat wall from flickering).
|
|
341
|
+
* 5. Hit on a NEW face → cancel old timer + clear preview, start
|
|
342
|
+
* a fresh 200ms dwell.
|
|
343
|
+
* 6. Dwell elapses → camera-orient the normal (matches the click
|
|
344
|
+
* commit policy in `selectionHandlers.ts` so the previewed
|
|
345
|
+
* arrow always points the same direction the actual cut will
|
|
346
|
+
* keep), then publish to the slice.
|
|
347
|
+
*
|
|
348
|
+
* `faceKey` heuristic: we use the closed-form
|
|
349
|
+
* `${expressId}:${meshIndex}:${triangleIndex}` from the renderer's
|
|
350
|
+
* `Intersection`. That uniquely identifies the triangle and is
|
|
351
|
+
* stable under cursor wobble within a single triangle. For two
|
|
352
|
+
* adjacent triangles of the same flat wall the keys differ but the
|
|
353
|
+
* normals are nearly equal — that yields a brief reset of the
|
|
354
|
+
* dwell timer when crossing the diagonal, which is acceptable
|
|
355
|
+
* (matches the "moved to a new triangle" intuition and avoids the
|
|
356
|
+
* complexity of clustering coplanar triangles). The user only
|
|
357
|
+
* waits a fresh 200ms once per crossing; the per-triangle key
|
|
358
|
+
* still suppresses the in-triangle wobble that drove the
|
|
359
|
+
* jitter complaint.
|
|
360
|
+
*/
|
|
361
|
+
const handleSectionPickHover = (e: MouseEvent, x: number, y: number): void => {
|
|
362
|
+
const now = performance.now();
|
|
363
|
+
// 60fps cap — keeps the raycast off the hot path of high-Hz
|
|
364
|
+
// pointer devices. Reading-clock rate doesn't have to align
|
|
365
|
+
// with the display refresh; the dwell timer below paints at
|
|
366
|
+
// 200ms regardless.
|
|
367
|
+
if (now - sectionLastCastTsRef.current < 16) return;
|
|
368
|
+
// 2px deadband — fights spurious mousemove events from drift /
|
|
369
|
+
// touchpad jitter so we don't burn raycasts when the cursor is
|
|
370
|
+
// effectively still.
|
|
371
|
+
const last = sectionLastCastPosRef.current;
|
|
372
|
+
if (last) {
|
|
373
|
+
const dx = e.clientX - last.x;
|
|
374
|
+
const dy = e.clientY - last.y;
|
|
375
|
+
if (dx * dx + dy * dy < 4) return;
|
|
376
|
+
}
|
|
377
|
+
sectionLastCastPosRef.current = { x: e.clientX, y: e.clientY };
|
|
378
|
+
sectionLastCastTsRef.current = now;
|
|
379
|
+
|
|
380
|
+
const hit = renderer.raycastScene(x, y, {
|
|
381
|
+
hiddenIds: hiddenEntitiesRef.current,
|
|
382
|
+
isolatedIds: isolatedEntitiesRef.current,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Reject misses and degenerate normals. The renderer's
|
|
386
|
+
// raycaster *should* always hand back a unit-length normal but
|
|
387
|
+
// BVH meshes occasionally yield tiny-magnitude normals on
|
|
388
|
+
// co-planar triangle pairs; the slice would warn and refuse a
|
|
389
|
+
// commit anyway, so don't waste a preview on it.
|
|
390
|
+
const nLen = hit ? Math.hypot(hit.intersection.normal.x, hit.intersection.normal.y, hit.intersection.normal.z) : 0;
|
|
391
|
+
if (!hit || nLen < 1e-6) {
|
|
392
|
+
if (sectionDwellTimerRef.current) {
|
|
393
|
+
clearTimeout(sectionDwellTimerRef.current);
|
|
394
|
+
sectionDwellTimerRef.current = null;
|
|
395
|
+
}
|
|
396
|
+
sectionLastFaceKeyRef.current = null;
|
|
397
|
+
setSectionPickPreview(null);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const ix = hit.intersection;
|
|
402
|
+
// Triangle-stable face key — see the JSDoc above for the
|
|
403
|
+
// adjacent-triangle behaviour.
|
|
404
|
+
const faceKey = `${ix.expressId}:${ix.meshIndex}:${ix.triangleIndex}`;
|
|
405
|
+
if (faceKey === sectionLastFaceKeyRef.current) {
|
|
406
|
+
// Same face — cursor is just wobbling within the triangle.
|
|
407
|
+
// The preview (if any) is already painted in the right place;
|
|
408
|
+
// the dwell timer (if any) is already counting down for this
|
|
409
|
+
// face. Doing nothing here is the entire point of the sticky
|
|
410
|
+
// faceKey rule.
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
sectionLastFaceKeyRef.current = faceKey;
|
|
414
|
+
|
|
415
|
+
// New face — cancel the previous face's pending dwell + drop
|
|
416
|
+
// any preview still pinned to it so the user doesn't see the
|
|
417
|
+
// overlay linger on the wrong surface during the new face's
|
|
418
|
+
// 200ms wait.
|
|
419
|
+
if (sectionDwellTimerRef.current) clearTimeout(sectionDwellTimerRef.current);
|
|
420
|
+
setSectionPickPreview(null);
|
|
421
|
+
|
|
422
|
+
// Snapshot what we need so the timer closure doesn't capture
|
|
423
|
+
// a hit object that the raycaster will mutate on the next cast.
|
|
424
|
+
const px = ix.point.x, py = ix.point.y, pz = ix.point.z;
|
|
425
|
+
const nx = ix.normal.x / nLen, ny = ix.normal.y / nLen, nz = ix.normal.z / nLen;
|
|
426
|
+
|
|
427
|
+
sectionDwellTimerRef.current = setTimeout(() => {
|
|
428
|
+
sectionDwellTimerRef.current = null;
|
|
429
|
+
// Camera-aware normal flip — mirrors the commit logic in
|
|
430
|
+
// `selectionHandlers.ts` so the previewed arrow direction
|
|
431
|
+
// matches what the click will actually produce. Without this
|
|
432
|
+
// the preview would point one way and the cap (post-click)
|
|
433
|
+
// could end up the other, which the user would read as a
|
|
434
|
+
// bug.
|
|
435
|
+
const cam = renderer.getCamera().getPosition();
|
|
436
|
+
const vx = cam.x - px, vy = cam.y - py, vz = cam.z - pz;
|
|
437
|
+
const sign = (vx * nx + vy * ny + vz * nz) < 0 ? -1 : 1;
|
|
438
|
+
setSectionPickPreview({
|
|
439
|
+
normal: [sign * nx, sign * ny, sign * nz],
|
|
440
|
+
point: [px, py, pz],
|
|
441
|
+
faceKey,
|
|
442
|
+
});
|
|
443
|
+
}, 200);
|
|
444
|
+
};
|
|
445
|
+
|
|
247
446
|
// Mouse controls - respect active tool
|
|
248
447
|
// Uses pointer events + setPointerCapture so pointerup always fires,
|
|
249
448
|
// even when the pointer leaves the canvas (e.g. dragging across panels).
|
|
@@ -258,10 +457,23 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
258
457
|
mouseState.startX = e.clientX;
|
|
259
458
|
mouseState.startY = e.clientY;
|
|
260
459
|
mouseState.didDrag = false;
|
|
460
|
+
mouseState.isRectSelecting = false;
|
|
261
461
|
|
|
262
462
|
// Determine action based on active tool and mouse button
|
|
263
463
|
const tool = activeToolRef.current;
|
|
264
464
|
|
|
465
|
+
// Rectangle-select gesture: Ctrl/⌘ + LMB drag while in the
|
|
466
|
+
// select tool. Suppresses orbit/pan; the rect is finalised
|
|
467
|
+
// and pick happens on mouseup.
|
|
468
|
+
if (tool === 'select' && e.button === 0 && (e.ctrlKey || e.metaKey)) {
|
|
469
|
+
mouseState.isRectSelecting = true;
|
|
470
|
+
const rect = canvas.getBoundingClientRect();
|
|
471
|
+
const cx = e.clientX - rect.left;
|
|
472
|
+
const cy = e.clientY - rect.top;
|
|
473
|
+
setRectSelection?.({ x0: cx, y0: cy, x1: cx, y1: cy });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
265
477
|
// Will this mousedown lead to an orbit drag?
|
|
266
478
|
const isPanGesture = tool === 'pan' || e.button === 1 || e.button === 2 ||
|
|
267
479
|
(tool === 'select' && e.shiftKey);
|
|
@@ -357,6 +569,18 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
357
569
|
const y = e.clientY - rect.top;
|
|
358
570
|
const tool = activeToolRef.current;
|
|
359
571
|
|
|
572
|
+
// Rectangle-select drag: just update the visual; no orbit / pan
|
|
573
|
+
// / pick / hover work happens in this branch.
|
|
574
|
+
if (mouseState.isRectSelecting) {
|
|
575
|
+
setRectSelection?.({
|
|
576
|
+
x0: mouseState.startX - rect.left,
|
|
577
|
+
y0: mouseState.startY - rect.top,
|
|
578
|
+
x1: x,
|
|
579
|
+
y1: y,
|
|
580
|
+
});
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
360
584
|
// Handle measure tool live preview while dragging
|
|
361
585
|
// IMPORTANT: Check tool first, not activeMeasurement, to prevent orbit conflict
|
|
362
586
|
if (tool === 'measure' && mouseState.isDragging && activeMeasurementRef.current) {
|
|
@@ -376,6 +600,17 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
376
600
|
if (handleAddElementHover(ctx, x, y)) return;
|
|
377
601
|
}
|
|
378
602
|
|
|
603
|
+
// Section tool face-pick: dwell-aware hover preview (issue #243
|
|
604
|
+
// follow-up). Runs INSTEAD of the generic tooltip path while
|
|
605
|
+
// pick mode is armed so the overlay stays the only signal under
|
|
606
|
+
// the cursor — the tooltip would just compete visually with the
|
|
607
|
+
// violet quad. See `handleSectionPickHover` for the full
|
|
608
|
+
// anti-jitter rules.
|
|
609
|
+
if (tool === 'section' && !mouseState.isDragging && sectionPickModeRef.current) {
|
|
610
|
+
handleSectionPickHover(e, x, y);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
379
614
|
// Handle orbit/pan for other tools (or measure tool with shift+drag or no active measurement)
|
|
380
615
|
if (mouseState.isDragging && (tool !== 'measure' || !activeMeasurementRef.current)) {
|
|
381
616
|
const dx = e.clientX - mouseState.lastX;
|
|
@@ -421,7 +656,12 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
421
656
|
// Uses visibility filtering so hidden elements don't show hover tooltips
|
|
422
657
|
const pickResult = await renderer.pick(x, y, getPickOptions());
|
|
423
658
|
if (pickResult) {
|
|
424
|
-
setHoverState({
|
|
659
|
+
setHoverState({
|
|
660
|
+
entityId: pickResult.expressId,
|
|
661
|
+
screenX: e.clientX,
|
|
662
|
+
screenY: e.clientY,
|
|
663
|
+
worldXYZ: pickResult.worldXYZ,
|
|
664
|
+
});
|
|
425
665
|
} else {
|
|
426
666
|
clearHover();
|
|
427
667
|
}
|
|
@@ -441,6 +681,39 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
441
681
|
|
|
442
682
|
const tool = activeToolRef.current;
|
|
443
683
|
|
|
684
|
+
// Rectangle-select finalisation: run pickRect against the
|
|
685
|
+
// dragged rect, replace the current selection with the result,
|
|
686
|
+
// then clear the visual.
|
|
687
|
+
if (mouseState.isRectSelecting) {
|
|
688
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
689
|
+
const x0 = mouseState.startX - canvasRect.left;
|
|
690
|
+
const y0 = mouseState.startY - canvasRect.top;
|
|
691
|
+
const x1 = e.clientX - canvasRect.left;
|
|
692
|
+
const y1 = e.clientY - canvasRect.top;
|
|
693
|
+
// Tiny rect (just a click + tiny twitch) → no-op so we don't
|
|
694
|
+
// accidentally clear selection on a missed Ctrl-click.
|
|
695
|
+
const rectSize = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0));
|
|
696
|
+
if (rectSize >= 4) {
|
|
697
|
+
// pickRect can reject on WebGPU validation / device-loss
|
|
698
|
+
// paths — swallow the error so the pointer event doesn't
|
|
699
|
+
// surface an unhandled rejection. Selection stays
|
|
700
|
+
// untouched on failure (better UX than clearing it).
|
|
701
|
+
void renderer
|
|
702
|
+
.pickRect(x0, y0, x1, y1, getPickOptions())
|
|
703
|
+
.then((ids) => {
|
|
704
|
+
useViewerStore.getState().setSelectedEntityIds(Array.from(ids));
|
|
705
|
+
})
|
|
706
|
+
.catch((error) => {
|
|
707
|
+
console.warn('[useMouseControls] Rectangle selection failed:', error);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
setRectSelection?.(null);
|
|
711
|
+
mouseState.isRectSelecting = false;
|
|
712
|
+
mouseState.isDragging = false;
|
|
713
|
+
mouseState.isPanning = false;
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
444
717
|
// Handle measure tool completion
|
|
445
718
|
if (tool === 'measure' && activeMeasurementRef.current) {
|
|
446
719
|
if (handleMeasureUp(ctx, e)) return;
|
|
@@ -456,6 +729,17 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
456
729
|
mouseState.isDragging = false;
|
|
457
730
|
mouseState.isPanning = false;
|
|
458
731
|
camera.stopInertia();
|
|
732
|
+
// Section face-pick preview: cursor left the canvas, so any
|
|
733
|
+
// pending dwell timer would otherwise commit a stale hover
|
|
734
|
+
// when the user returns. Drop the overlay too so we don't leave
|
|
735
|
+
// a violet quad orphaned on the last-seen face after leaving.
|
|
736
|
+
if (sectionDwellTimerRef.current) {
|
|
737
|
+
clearTimeout(sectionDwellTimerRef.current);
|
|
738
|
+
sectionDwellTimerRef.current = null;
|
|
739
|
+
}
|
|
740
|
+
sectionLastFaceKeyRef.current = null;
|
|
741
|
+
sectionLastCastPosRef.current = null;
|
|
742
|
+
setSectionPickPreview(null);
|
|
459
743
|
// Restore cursor based on active tool
|
|
460
744
|
if (tool === 'measure') {
|
|
461
745
|
canvas.style.cursor = 'crosshair';
|
|
@@ -528,6 +812,15 @@ export function useMouseControls(params: UseMouseControlsParams): void {
|
|
|
528
812
|
cancelAnimationFrame(measureRaycastFrameRef.current);
|
|
529
813
|
measureRaycastFrameRef.current = null;
|
|
530
814
|
}
|
|
815
|
+
|
|
816
|
+
// Section face-pick: drop any pending dwell so the timer
|
|
817
|
+
// doesn't fire after unmount and call into a stale renderer.
|
|
818
|
+
if (sectionDwellTimerRef.current) {
|
|
819
|
+
clearTimeout(sectionDwellTimerRef.current);
|
|
820
|
+
sectionDwellTimerRef.current = null;
|
|
821
|
+
}
|
|
822
|
+
sectionLastFaceKeyRef.current = null;
|
|
823
|
+
sectionLastCastPosRef.current = null;
|
|
531
824
|
};
|
|
532
825
|
}, [isInitialized]);
|
|
533
826
|
}
|
|
@@ -37,6 +37,10 @@ export function usePointCloudSync(params: UsePointCloudSyncParams): void {
|
|
|
37
37
|
const roundShape = useViewerStore((s) => s.pointCloudRoundShape);
|
|
38
38
|
const edlEnabled = useViewerStore((s) => s.pointCloudEdlEnabled);
|
|
39
39
|
const edlStrength = useViewerStore((s) => s.pointCloudEdlStrength);
|
|
40
|
+
const classMask = useViewerStore((s) => s.pointCloudClassMask);
|
|
41
|
+
const previewStride = useViewerStore((s) => s.pointCloudPreviewStride);
|
|
42
|
+
const deviationCenter = useViewerStore((s) => s.pointCloudDeviationCenterOffset);
|
|
43
|
+
const deviationHalf = useViewerStore((s) => s.pointCloudDeviationHalfRange);
|
|
40
44
|
const setAssetCount = useViewerStore((s) => s.setPointCloudAssetCount);
|
|
41
45
|
const fittedRef = useRef(false);
|
|
42
46
|
|
|
@@ -82,9 +86,12 @@ export function usePointCloudSync(params: UsePointCloudSyncParams): void {
|
|
|
82
86
|
pointSize,
|
|
83
87
|
worldRadius,
|
|
84
88
|
roundShape,
|
|
89
|
+
classMask,
|
|
90
|
+
previewStride,
|
|
91
|
+
deviationRange: { centerOffset: deviationCenter, halfRange: deviationHalf },
|
|
85
92
|
});
|
|
86
93
|
renderer.requestRender();
|
|
87
|
-
}, [colorMode, fixedColor, sizeMode, pointSize, worldRadius, roundShape, isInitialized, rendererRef]);
|
|
94
|
+
}, [colorMode, fixedColor, sizeMode, pointSize, worldRadius, roundShape, classMask, previewStride, deviationCenter, deviationHalf, isInitialized, rendererRef]);
|
|
88
95
|
|
|
89
96
|
// Push EDL toggle + strength to the renderer.
|
|
90
97
|
useEffect(() => {
|
|
@@ -15,6 +15,7 @@ import type { Renderer, CutPolygon2D, DrawingLine2D, VisualEnhancementOptions }
|
|
|
15
15
|
import type { CoordinateInfo } from '@ifc-lite/geometry';
|
|
16
16
|
import type { Drawing2D } from '@ifc-lite/drawing-2d';
|
|
17
17
|
import type { SectionPlane } from '@/store';
|
|
18
|
+
import { customPlaneCenter } from '@/store';
|
|
18
19
|
import { getThemeClearColor } from '../../utils/viewportUtils.js';
|
|
19
20
|
|
|
20
21
|
export interface UseRenderUpdatesParams {
|
|
@@ -100,13 +101,32 @@ export function useRenderUpdates(params: UseRenderUpdatesParams): void {
|
|
|
100
101
|
category: line.category,
|
|
101
102
|
}));
|
|
102
103
|
|
|
104
|
+
// For face-picked custom planes (issue #243), forward the plane
|
|
105
|
+
// basis so `uploadDrawing` can lift 2D polygons back to 3D using
|
|
106
|
+
// the same axes the cutter projected with — without that the cap
|
|
107
|
+
// silhouette lands off the actual cutting plane (PR #581's bug).
|
|
108
|
+
// The basis origin is `pickedAt` projected onto the LIVE plane
|
|
109
|
+
// (`customPlaneCenter`), not `pickedAt` directly: as the user
|
|
110
|
+
// drags the gizmo only `distance` changes, and pickedAt sits off
|
|
111
|
+
// the live plane — using it here makes the lift drop the normal-
|
|
112
|
+
// component, freezing the cap at the original pick location.
|
|
113
|
+
const custom = sectionPlane.custom;
|
|
114
|
+
const customPlane = custom
|
|
115
|
+
? {
|
|
116
|
+
origin: customPlaneCenter(custom),
|
|
117
|
+
tangent: custom.tangent,
|
|
118
|
+
bitangent: custom.bitangent,
|
|
119
|
+
}
|
|
120
|
+
: undefined;
|
|
121
|
+
|
|
103
122
|
renderer.uploadSection2DOverlay(
|
|
104
123
|
polygons,
|
|
105
124
|
lines,
|
|
106
125
|
sectionPlane.axis,
|
|
107
126
|
sectionPlane.position,
|
|
108
127
|
sectionRangeRef.current ?? undefined,
|
|
109
|
-
sectionPlane.flipped
|
|
128
|
+
sectionPlane.flipped,
|
|
129
|
+
customPlane,
|
|
110
130
|
);
|
|
111
131
|
} else {
|
|
112
132
|
renderer.clearSection2DOverlay();
|