@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
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { setGlobalCanvasRef, setGlobalRendererRef, clearGlobalRefs } from '../../hooks/useBCF.js';
|
|
33
33
|
|
|
34
34
|
import { useMouseControls, type MouseState } from './useMouseControls.js';
|
|
35
|
+
import { RectSelectionOverlay, type RectSelectionRect } from './RectSelectionOverlay.js';
|
|
35
36
|
import { useTouchControls, type TouchState } from './useTouchControls.js';
|
|
36
37
|
import { useKeyboardControls } from './useKeyboardControls.js';
|
|
37
38
|
import { useAnimationLoop } from './useAnimationLoop.js';
|
|
@@ -71,6 +72,7 @@ export function Viewport({
|
|
|
71
72
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
72
73
|
const rendererRef = useRef<Renderer | null>(null);
|
|
73
74
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
75
|
+
const [initError, setInitError] = useState<string | null>(null);
|
|
74
76
|
|
|
75
77
|
const focusViewportForKeyboardShortcuts = useCallback(() => {
|
|
76
78
|
const canvas = canvasRef.current;
|
|
@@ -199,8 +201,17 @@ export function Viewport({
|
|
|
199
201
|
const { hiddenEntities, isolatedEntities: storeIsolatedEntities } = useVisibilityState();
|
|
200
202
|
const isolatedEntities = computedIsolatedIds ?? storeIsolatedEntities ?? null;
|
|
201
203
|
|
|
202
|
-
// Tool state
|
|
203
|
-
|
|
204
|
+
// Tool state — `sectionPickMode` arms a face-pick on the next click for
|
|
205
|
+
// the section tool (issue #243); the action setters are forwarded into
|
|
206
|
+
// the mouse-controls context.
|
|
207
|
+
const {
|
|
208
|
+
activeTool,
|
|
209
|
+
sectionPlane,
|
|
210
|
+
sectionPickMode,
|
|
211
|
+
setSectionPlaneFromFace,
|
|
212
|
+
setSectionPickMode,
|
|
213
|
+
setSectionPickPreview,
|
|
214
|
+
} = useToolState();
|
|
204
215
|
|
|
205
216
|
// Camera state
|
|
206
217
|
const { updateCameraRotationRealtime, updateScaleRealtime, setCameraCallbacks } = useCameraState();
|
|
@@ -282,9 +293,9 @@ export function Viewport({
|
|
|
282
293
|
// Tokyo Night storm: #1a1b26 = rgb(26, 27, 38)
|
|
283
294
|
const clearColorRef = useRef<[number, number, number, number]>([0.102, 0.106, 0.149, 1]);
|
|
284
295
|
const visualEnhancement = useMemo<VisualEnhancementOptions>(() => ({
|
|
285
|
-
enabled: visualEnhancementsEnabled,
|
|
296
|
+
enabled: isMobile ? false : visualEnhancementsEnabled,
|
|
286
297
|
edgeContrast: {
|
|
287
|
-
enabled: edgeContrastEnabled,
|
|
298
|
+
enabled: isMobile ? false : edgeContrastEnabled,
|
|
288
299
|
intensity: edgeContrastIntensity,
|
|
289
300
|
},
|
|
290
301
|
contactShading: {
|
|
@@ -293,7 +304,7 @@ export function Viewport({
|
|
|
293
304
|
radius: contactShadingRadius,
|
|
294
305
|
},
|
|
295
306
|
separationLines: {
|
|
296
|
-
enabled: separationLinesEnabled,
|
|
307
|
+
enabled: isMobile ? false : separationLinesEnabled,
|
|
297
308
|
quality: isMobile ? 'low' : separationLinesQuality,
|
|
298
309
|
intensity: isMobile ? Math.min(0.4, separationLinesIntensity) : separationLinesIntensity,
|
|
299
310
|
radius: isMobile ? 1.0 : separationLinesRadius,
|
|
@@ -349,6 +360,10 @@ export function Viewport({
|
|
|
349
360
|
didMove: false,
|
|
350
361
|
// Track if multi-touch occurred (prevents false tap-select after pinch/zoom)
|
|
351
362
|
multiTouch: false,
|
|
363
|
+
// 2-finger gesture detection
|
|
364
|
+
twoFingerGesture: 'none',
|
|
365
|
+
gestureDistanceAccum: 0,
|
|
366
|
+
gesturePanAccum: 0,
|
|
352
367
|
});
|
|
353
368
|
|
|
354
369
|
// Double-click detection
|
|
@@ -386,7 +401,12 @@ export function Viewport({
|
|
|
386
401
|
const measurementConstraintEdgeRef = useLatestRef(measurementConstraintEdge);
|
|
387
402
|
const sectionPlaneRef = useLatestRef(sectionPlane);
|
|
388
403
|
const sectionRangeRef = useLatestRef(sectionRange);
|
|
404
|
+
const sectionPickModeRef = useLatestRef(sectionPickMode);
|
|
389
405
|
const visualEnhancementRef = useLatestRef(visualEnhancement);
|
|
406
|
+
// Renderer model bounds, kept fresh per-render. The face-pick handler
|
|
407
|
+
// forwards these to the slice so the cardinal-fallback `position` % is
|
|
408
|
+
// computed against the actual model extents at click time.
|
|
409
|
+
const modelBoundsRef = useRef<{ min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } } | null>(null);
|
|
390
410
|
|
|
391
411
|
// Terrain clip Y from Cesium store (read as ref for animation loop)
|
|
392
412
|
const cesiumTerrainClipY = useViewerStore((s) => s.cesiumTerrainClipY);
|
|
@@ -460,9 +480,18 @@ export function Viewport({
|
|
|
460
480
|
}
|
|
461
481
|
}
|
|
462
482
|
|
|
463
|
-
//
|
|
483
|
+
// Leaving the section tool disarms face-pick so it doesn't ambush the
|
|
484
|
+
// user on re-entry to a different tool (issue #243).
|
|
485
|
+
if (activeTool !== 'section' && sectionPickMode) {
|
|
486
|
+
setSectionPickMode(false);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Set cursor based on active tool. Section + pick-armed gets a
|
|
490
|
+
// crosshair to telegraph "click a face".
|
|
464
491
|
if (activeTool === 'measure' || activeTool === 'annotate' || activeTool === 'addElement') {
|
|
465
492
|
canvas.style.cursor = 'crosshair';
|
|
493
|
+
} else if (activeTool === 'section' && sectionPickMode) {
|
|
494
|
+
canvas.style.cursor = 'crosshair';
|
|
466
495
|
} else {
|
|
467
496
|
canvas.style.cursor = 'default';
|
|
468
497
|
}
|
|
@@ -476,7 +505,7 @@ export function Viewport({
|
|
|
476
505
|
state.clearAddElementPending();
|
|
477
506
|
}
|
|
478
507
|
}
|
|
479
|
-
}, [activeTool, activeMeasurement, cancelMeasurement]);
|
|
508
|
+
}, [activeTool, activeMeasurement, cancelMeasurement, sectionPickMode, setSectionPickMode]);
|
|
480
509
|
|
|
481
510
|
// Helper: calculate scale bar value (world-space size for 96px scale bar)
|
|
482
511
|
const calculateScale = () => {
|
|
@@ -526,6 +555,7 @@ export function Viewport({
|
|
|
526
555
|
if (!canvas) return;
|
|
527
556
|
|
|
528
557
|
setIsInitialized(false);
|
|
558
|
+
setInitError(null);
|
|
529
559
|
|
|
530
560
|
let aborted = false;
|
|
531
561
|
let resizeObserver: ResizeObserver | null = null;
|
|
@@ -537,6 +567,9 @@ export function Viewport({
|
|
|
537
567
|
return Math.max(64, Math.floor(size / 64) * 64);
|
|
538
568
|
};
|
|
539
569
|
|
|
570
|
+
// Use CSS pixel dimensions for canvas. The Renderer.render() method manages
|
|
571
|
+
// its own dimension alignment via getBoundingClientRect() — do NOT apply DPR
|
|
572
|
+
// here as it creates a mismatch that causes constant context reconfiguration.
|
|
540
573
|
const rect = canvas.getBoundingClientRect();
|
|
541
574
|
const width = alignToWebGPU(Math.max(1, Math.floor(rect.width)));
|
|
542
575
|
const height = Math.max(1, Math.floor(rect.height));
|
|
@@ -552,7 +585,6 @@ export function Viewport({
|
|
|
552
585
|
|
|
553
586
|
renderer.init().then(() => {
|
|
554
587
|
if (aborted) return;
|
|
555
|
-
|
|
556
588
|
setIsInitialized(true);
|
|
557
589
|
|
|
558
590
|
const camera = renderer.getCamera();
|
|
@@ -664,11 +696,10 @@ export function Viewport({
|
|
|
664
696
|
},
|
|
665
697
|
});
|
|
666
698
|
|
|
667
|
-
// ResizeObserver
|
|
699
|
+
// ResizeObserver — let renderer handle its own dimension alignment
|
|
668
700
|
resizeObserver = new ResizeObserver(() => {
|
|
669
701
|
if (aborted) return;
|
|
670
702
|
const rect = canvas.getBoundingClientRect();
|
|
671
|
-
// Use same WebGPU alignment as initialization
|
|
672
703
|
const w = alignToWebGPU(Math.max(1, Math.floor(rect.width)));
|
|
673
704
|
const h = Math.max(1, Math.floor(rect.height));
|
|
674
705
|
renderer.resize(w, h);
|
|
@@ -678,6 +709,11 @@ export function Viewport({
|
|
|
678
709
|
|
|
679
710
|
// Initial render
|
|
680
711
|
renderCurrent();
|
|
712
|
+
}).catch((err) => {
|
|
713
|
+
if (aborted) return;
|
|
714
|
+
const message = err instanceof Error ? err.message : 'Failed to initialize 3D renderer';
|
|
715
|
+
console.error('[Viewport] Renderer init failed:', message);
|
|
716
|
+
setInitError(message);
|
|
681
717
|
});
|
|
682
718
|
|
|
683
719
|
return () => {
|
|
@@ -717,6 +753,10 @@ export function Viewport({
|
|
|
717
753
|
// The animation loop reads this to skip post-processing during rapid camera movement.
|
|
718
754
|
const isInteractingRef = useRef(false);
|
|
719
755
|
|
|
756
|
+
// Rectangle-select drag state — populated by useMouseControls during
|
|
757
|
+
// a Ctrl/⌘ + LMB drag, consumed by RectSelectionOverlay below.
|
|
758
|
+
const [rectSelection, setRectSelection] = useState<RectSelectionRect | null>(null);
|
|
759
|
+
|
|
720
760
|
// ===== Extracted hooks =====
|
|
721
761
|
useMouseControls({
|
|
722
762
|
canvasRef,
|
|
@@ -728,6 +768,8 @@ export function Viewport({
|
|
|
728
768
|
snapEnabledRef,
|
|
729
769
|
edgeLockStateRef,
|
|
730
770
|
measurementConstraintEdgeRef,
|
|
771
|
+
sectionPickModeRef,
|
|
772
|
+
modelBoundsRef,
|
|
731
773
|
hiddenEntitiesRef,
|
|
732
774
|
isolatedEntitiesRef,
|
|
733
775
|
selectedEntityIdRef,
|
|
@@ -751,6 +793,7 @@ export function Viewport({
|
|
|
751
793
|
handlePickForSelection: (pickResult) => handlePickForSelectionRef.current(pickResult),
|
|
752
794
|
setHoverState,
|
|
753
795
|
clearHover,
|
|
796
|
+
setRectSelection,
|
|
754
797
|
openContextMenu,
|
|
755
798
|
startMeasurement,
|
|
756
799
|
updateMeasurement,
|
|
@@ -769,6 +812,9 @@ export function Viewport({
|
|
|
769
812
|
calculateScale,
|
|
770
813
|
getPickOptions,
|
|
771
814
|
hasPendingMeasurements,
|
|
815
|
+
setSectionPlaneFromFace,
|
|
816
|
+
setSectionPickMode,
|
|
817
|
+
setSectionPickPreview,
|
|
772
818
|
HOVER_SNAP_THROTTLE_MS,
|
|
773
819
|
SLOW_RAYCAST_THRESHOLD_MS,
|
|
774
820
|
hoverThrottleMs,
|
|
@@ -833,6 +879,7 @@ export function Viewport({
|
|
|
833
879
|
clearColorRef,
|
|
834
880
|
sectionPlaneRef,
|
|
835
881
|
sectionRangeRef,
|
|
882
|
+
modelBoundsRef,
|
|
836
883
|
visualEnhancementRef,
|
|
837
884
|
selectedEntityIdsRef,
|
|
838
885
|
coordinateInfoRef,
|
|
@@ -918,13 +965,34 @@ export function Viewport({
|
|
|
918
965
|
: undefined;
|
|
919
966
|
|
|
920
967
|
return (
|
|
921
|
-
<
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
968
|
+
<div className="relative w-full h-full">
|
|
969
|
+
<canvas
|
|
970
|
+
ref={canvasRef}
|
|
971
|
+
data-viewport="main"
|
|
972
|
+
tabIndex={-1}
|
|
973
|
+
className={`w-full h-full block ${cesiumActive ? 'relative z-[1]' : ''}`}
|
|
974
|
+
style={{ touchAction: 'none', ...canvasStyle }}
|
|
975
|
+
onPointerDown={focusViewportForKeyboardShortcuts}
|
|
976
|
+
/>
|
|
977
|
+
{initError && (
|
|
978
|
+
<div className="absolute inset-0 flex items-center justify-center bg-background/90 z-50 p-4">
|
|
979
|
+
<div className="text-center max-w-sm space-y-3">
|
|
980
|
+
<div className="mx-auto w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center">
|
|
981
|
+
<svg className="h-6 w-6 text-destructive" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
982
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
983
|
+
</svg>
|
|
984
|
+
</div>
|
|
985
|
+
<p className="font-semibold text-sm">3D Rendering Failed</p>
|
|
986
|
+
<p className="text-xs text-muted-foreground">{initError}</p>
|
|
987
|
+
<p className="text-xs text-muted-foreground">
|
|
988
|
+
Try using Chrome 113+, Edge 113+, or Safari 18+ with WebGPU support.
|
|
989
|
+
</p>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
)}
|
|
993
|
+
{/* Rectangle-select drag visual. Pointer-events:none so the
|
|
994
|
+
canvas keeps receiving pointer events during the drag. */}
|
|
995
|
+
<RectSelectionOverlay rect={rectSelection} />
|
|
996
|
+
</div>
|
|
929
997
|
);
|
|
930
998
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { useMemo, useRef, useState, useCallback, useEffect, useSyncExternalStore } from 'react';
|
|
6
6
|
import { Viewport } from './Viewport';
|
|
7
7
|
import { ViewportOverlays } from './ViewportOverlays';
|
|
8
|
+
import { MergeLayersBanner } from './MergeLayersBanner';
|
|
8
9
|
import { ToolOverlays } from './ToolOverlays';
|
|
9
10
|
import { AnnotationLayer } from './annotations/AnnotationLayer';
|
|
10
11
|
import { Section2DPanel } from './Section2DPanel';
|
|
@@ -22,7 +23,7 @@ import { cacheFileBlobs, formatFileSize, getCachedFile, getRecentFiles, recordRe
|
|
|
22
23
|
import { isTauri } from '@/lib/platform';
|
|
23
24
|
import { toast } from '@/components/ui/toast';
|
|
24
25
|
import { describeUnsupportedFormat } from '@/hooks/ingest/pointCloudIngest';
|
|
25
|
-
import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3 } from 'lucide-react';
|
|
26
|
+
import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3, Sparkles, ArrowUpRight } from 'lucide-react';
|
|
26
27
|
import type { MeshData, CoordinateInfo, GeometryResult, PointCloudAsset } from '@ifc-lite/geometry';
|
|
27
28
|
import { type IfcDataStore } from '@ifc-lite/parser';
|
|
28
29
|
import { getEffectiveGeoreference } from '@/lib/geo/effective-georef';
|
|
@@ -215,7 +216,11 @@ export function ViewportContainer() {
|
|
|
215
216
|
georefMutations.get(modelId),
|
|
216
217
|
);
|
|
217
218
|
if (effective?.projectedCRS?.name && effective.mapConversion) {
|
|
218
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
...effective,
|
|
221
|
+
sourceModelId: modelId,
|
|
222
|
+
storeyElevations: ds.spatialHierarchy?.storeyElevations,
|
|
223
|
+
};
|
|
219
224
|
}
|
|
220
225
|
}
|
|
221
226
|
|
|
@@ -227,7 +232,11 @@ export function ViewportContainer() {
|
|
|
227
232
|
georefMutations.get('__legacy__'),
|
|
228
233
|
);
|
|
229
234
|
if (effective?.projectedCRS?.name && effective.mapConversion) {
|
|
230
|
-
return {
|
|
235
|
+
return {
|
|
236
|
+
...effective,
|
|
237
|
+
sourceModelId: '__legacy__',
|
|
238
|
+
storeyElevations: ifcDataStore.spatialHierarchy?.storeyElevations,
|
|
239
|
+
};
|
|
231
240
|
}
|
|
232
241
|
}
|
|
233
242
|
|
|
@@ -311,7 +320,7 @@ export function ViewportContainer() {
|
|
|
311
320
|
const allDropped = Array.from(e.dataTransfer.files);
|
|
312
321
|
const supportedFiles = allDropped.filter(
|
|
313
322
|
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|
|
314
|
-
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57')
|
|
323
|
+
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57') || f.name.toLowerCase().endsWith('.pts') || f.name.toLowerCase().endsWith('.xyz')
|
|
315
324
|
);
|
|
316
325
|
|
|
317
326
|
if (supportedFiles.length === 0) {
|
|
@@ -354,7 +363,7 @@ export function ViewportContainer() {
|
|
|
354
363
|
// Filter to supported files (IFC, IFCX, GLB)
|
|
355
364
|
const supportedFiles = Array.from(files).filter(
|
|
356
365
|
f => f.name.endsWith('.ifc') || f.name.endsWith('.ifcx') || f.name.endsWith('.glb')
|
|
357
|
-
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57')
|
|
366
|
+
|| f.name.toLowerCase().endsWith('.las') || f.name.toLowerCase().endsWith('.laz') || f.name.toLowerCase().endsWith('.ply') || f.name.toLowerCase().endsWith('.pcd') || f.name.toLowerCase().endsWith('.e57') || f.name.toLowerCase().endsWith('.pts') || f.name.toLowerCase().endsWith('.xyz')
|
|
358
367
|
);
|
|
359
368
|
|
|
360
369
|
if (supportedFiles.length === 0) return;
|
|
@@ -566,7 +575,7 @@ export function ViewportContainer() {
|
|
|
566
575
|
<input
|
|
567
576
|
ref={fileInputRef}
|
|
568
577
|
type="file"
|
|
569
|
-
accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57"
|
|
578
|
+
accept=".ifc,.ifcx,.glb,.las,.laz,.ply,.pcd,.e57,.pts,.xyz"
|
|
570
579
|
multiple
|
|
571
580
|
onChange={handleFileSelect}
|
|
572
581
|
className="hidden"
|
|
@@ -582,9 +591,9 @@ export function ViewportContainer() {
|
|
|
582
591
|
</div>
|
|
583
592
|
)}
|
|
584
593
|
|
|
585
|
-
{/* WebGPU Not Supported Banner */}
|
|
594
|
+
{/* WebGPU Not Supported Banner — compact on mobile */}
|
|
586
595
|
{!webgpu.checking && !webgpu.supported && (
|
|
587
|
-
<div className="absolute top-0 left-0 right-0 z-40">
|
|
596
|
+
<div className="absolute top-0 left-0 right-0 z-40 max-h-[40vh] overflow-auto">
|
|
588
597
|
{/* Hazard stripes background */}
|
|
589
598
|
<div
|
|
590
599
|
className="absolute inset-0 opacity-10"
|
|
@@ -697,8 +706,8 @@ export function ViewportContainer() {
|
|
|
697
706
|
</div>
|
|
698
707
|
)}
|
|
699
708
|
|
|
700
|
-
{/* Empty state content */}
|
|
701
|
-
<div className="absolute inset-0 flex flex-col items-center justify-center p-8 z-10">
|
|
709
|
+
{/* Empty state content — mobile-optimized padding and scrollable */}
|
|
710
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center p-4 md:p-8 z-10 overflow-auto">
|
|
702
711
|
|
|
703
712
|
{/* Main Card */}
|
|
704
713
|
<div className="max-w-md w-full bg-white dark:bg-[#16161e] border border-zinc-300 dark:border-[#3b4261] p-8 flex flex-col items-center transition-transform hover:-translate-y-1 duration-200 shadow-lg">
|
|
@@ -735,10 +744,18 @@ export function ViewportContainer() {
|
|
|
735
744
|
IFClite
|
|
736
745
|
</h2>
|
|
737
746
|
<p className="text-zinc-500 dark:text-[#565f89] font-mono text-sm text-center mb-8 border-b border-zinc-200 dark:border-[#3b4261] pb-4 w-full">
|
|
738
|
-
|
|
747
|
+
IFC toolkit for the open web
|
|
739
748
|
</p>
|
|
740
749
|
|
|
741
|
-
{/*
|
|
750
|
+
{/*
|
|
751
|
+
Two-track action area: a primary "open file" track and a
|
|
752
|
+
secondary "drive with LLM" track sit in mirrored slots — same
|
|
753
|
+
width, same vertical rhythm, each followed by its own caption
|
|
754
|
+
line. Reads as one balanced composition instead of a primary
|
|
755
|
+
CTA + a tacked-on link, while keeping the file-open path
|
|
756
|
+
visually dominant via the filled-on-hover treatment.
|
|
757
|
+
*/}
|
|
758
|
+
{/* Track 1 — open / drag */}
|
|
742
759
|
<button
|
|
743
760
|
onClick={async () => {
|
|
744
761
|
if (!webgpu.supported) {
|
|
@@ -774,10 +791,33 @@ export function ViewportContainer() {
|
|
|
774
791
|
<span>{webgpu.checking ? 'Checking WebGPU...' : webgpu.supported ? 'Open .ifc file' : 'WebGPU Required'}</span>
|
|
775
792
|
</button>
|
|
776
793
|
|
|
777
|
-
<p className="mt-
|
|
794
|
+
<p className="mt-2.5 text-[11px] font-mono text-center text-zinc-400 dark:text-[#565f89]">
|
|
778
795
|
{webgpu.supported ? 'or drag & drop anywhere' : 'file upload disabled'}
|
|
779
796
|
</p>
|
|
780
797
|
|
|
798
|
+
{/* Subtle "or" rule — anchors the symmetry between the two tracks */}
|
|
799
|
+
<div className="mt-5 mb-5 w-full flex items-center gap-3 text-[10px] font-mono uppercase tracking-[0.22em] text-zinc-400 dark:text-[#565f89]">
|
|
800
|
+
<span className="h-px flex-1 bg-zinc-200 dark:bg-[#3b4261]" />
|
|
801
|
+
<span>or</span>
|
|
802
|
+
<span className="h-px flex-1 bg-zinc-200 dark:bg-[#3b4261]" />
|
|
803
|
+
</div>
|
|
804
|
+
|
|
805
|
+
{/* Track 2 — agent / MCP. Compact inline pill, self-centred so
|
|
806
|
+
it reads as a meta-link sibling to the primary file-open
|
|
807
|
+
CTA, not a competing full-width button. */}
|
|
808
|
+
<a
|
|
809
|
+
href="/mcp"
|
|
810
|
+
className="group inline-flex self-center items-center gap-1.5 px-3 py-1.5 font-mono text-[11px] border border-dashed border-zinc-300 dark:border-[#3b4261] text-zinc-500 dark:text-[#7a82a5] hover:border-primary hover:text-primary transition-all cursor-pointer"
|
|
811
|
+
>
|
|
812
|
+
<Sparkles className="h-3 w-3 transition-transform group-hover:-translate-y-0.5" />
|
|
813
|
+
<span>Drive with any LLM</span>
|
|
814
|
+
<ArrowUpRight className="h-2.5 w-2.5 opacity-60 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
|
815
|
+
</a>
|
|
816
|
+
|
|
817
|
+
<p className="mt-1.5 text-[10px] font-mono text-center text-zinc-400 dark:text-[#565f89]">
|
|
818
|
+
via MCP · install or try the playground
|
|
819
|
+
</p>
|
|
820
|
+
|
|
781
821
|
{recentFiles.length > 0 && (
|
|
782
822
|
<div className="mt-6 w-full border-t border-zinc-200 dark:border-[#3b4261] pt-4">
|
|
783
823
|
<div className="mb-3 flex items-center gap-2 text-xs font-mono uppercase tracking-[0.2em] text-zinc-400 dark:text-[#565f89]">
|
|
@@ -810,8 +850,8 @@ export function ViewportContainer() {
|
|
|
810
850
|
)}
|
|
811
851
|
</div>
|
|
812
852
|
|
|
813
|
-
{/* Feature Grid */}
|
|
814
|
-
<div className="mt-16 grid grid-cols-1 md:grid-cols-3 gap-6 max-w-3xl w-full">
|
|
853
|
+
{/* Feature Grid — hidden on mobile to save viewport space */}
|
|
854
|
+
<div className="mt-16 hidden md:grid grid-cols-1 md:grid-cols-3 gap-6 max-w-3xl w-full">
|
|
815
855
|
{[
|
|
816
856
|
{ icon: MousePointer, label: "Select", desc: "Inspect elements", accentClass: 'text-blue-500 dark:text-[#7aa2f7]' },
|
|
817
857
|
{ icon: Layers, label: "Filter", desc: "Isolate storeys", accentClass: 'text-purple-500 dark:text-[#bb9af7]' },
|
|
@@ -877,6 +917,7 @@ export function ViewportContainer() {
|
|
|
877
917
|
coordinateInfo={georef.coordinateInfo}
|
|
878
918
|
geometryResult={mergedGeometryResult}
|
|
879
919
|
lengthUnitScale={georef.lengthUnitScale}
|
|
920
|
+
storeyElevations={georef.storeyElevations}
|
|
880
921
|
/>
|
|
881
922
|
)}
|
|
882
923
|
<Viewport
|
|
@@ -893,6 +934,10 @@ export function ViewportContainer() {
|
|
|
893
934
|
<AnnotationLayer />
|
|
894
935
|
{bcfOverlayVisible && <BCFOverlay />}
|
|
895
936
|
<ViewportOverlays />
|
|
937
|
+
{/* Issue #540: non-modal "reload to apply" banner anchored to the
|
|
938
|
+
top of the canvas. Only renders when the user has flipped the
|
|
939
|
+
merge-layers toggle while a model is in scope. */}
|
|
940
|
+
<MergeLayersBanner />
|
|
896
941
|
<ToolOverlays />
|
|
897
942
|
<BasketPresentationDock />
|
|
898
943
|
<Section2DPanel
|
|
@@ -34,6 +34,7 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
34
34
|
const isolatedEntities = useViewerStore((s) => s.isolatedEntities);
|
|
35
35
|
const basketPresentationVisible = useViewerStore((s) => s.basketPresentationVisible);
|
|
36
36
|
const cameraCallbacks = useViewerStore((s) => s.cameraCallbacks);
|
|
37
|
+
const isMobile = useViewerStore((s) => s.isMobile);
|
|
37
38
|
const setOnCameraRotationChange = useViewerStore((s) => s.setOnCameraRotationChange);
|
|
38
39
|
const setOnScaleChange = useViewerStore((s) => s.setOnScaleChange);
|
|
39
40
|
const { ifcDataStore, geometryResult } = useIfc();
|
|
@@ -161,11 +162,19 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
161
162
|
onClose={toggleCesium}
|
|
162
163
|
/>
|
|
163
164
|
) : (
|
|
164
|
-
<div
|
|
165
|
+
<div
|
|
166
|
+
className={cn(
|
|
167
|
+
'absolute flex flex-col gap-1 bg-background/90 backdrop-blur-sm border p-1',
|
|
168
|
+
// Mobile: bottom-left at ~15% up from lower edge — thumb-reachable on
|
|
169
|
+
// portrait phones and well clear of the URL bar. Tight radii + flat
|
|
170
|
+
// background match the codebase's brutalist panel-chrome vocabulary.
|
|
171
|
+
isMobile ? 'left-4 bottom-[15%] rounded-md' : 'bottom-4 right-4 rounded-lg shadow-sm',
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
165
174
|
<Tooltip>
|
|
166
175
|
<TooltipTrigger asChild>
|
|
167
|
-
<Button variant="ghost" size="icon-sm" onClick={handleHome}>
|
|
168
|
-
<Home className=
|
|
176
|
+
<Button variant="ghost" size="icon-sm" className={cn(isMobile && 'min-h-[44px] min-w-[44px]')} onClick={handleHome}>
|
|
177
|
+
<Home className={cn(isMobile ? 'h-5 w-5' : 'h-4 w-4')} />
|
|
169
178
|
</Button>
|
|
170
179
|
</TooltipTrigger>
|
|
171
180
|
<TooltipContent side="left">Home (H)</TooltipContent>
|
|
@@ -173,8 +182,8 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
173
182
|
|
|
174
183
|
<Tooltip>
|
|
175
184
|
<TooltipTrigger asChild>
|
|
176
|
-
<Button variant="ghost" size="icon-sm" onClick={handleZoomIn}>
|
|
177
|
-
<ZoomIn className=
|
|
185
|
+
<Button variant="ghost" size="icon-sm" className={cn(isMobile && 'min-h-[44px] min-w-[44px]')} onClick={handleZoomIn}>
|
|
186
|
+
<ZoomIn className={cn(isMobile ? 'h-5 w-5' : 'h-4 w-4')} />
|
|
178
187
|
</Button>
|
|
179
188
|
</TooltipTrigger>
|
|
180
189
|
<TooltipContent side="left">Zoom In (+)</TooltipContent>
|
|
@@ -182,8 +191,8 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
182
191
|
|
|
183
192
|
<Tooltip>
|
|
184
193
|
<TooltipTrigger asChild>
|
|
185
|
-
<Button variant="ghost" size="icon-sm" onClick={handleZoomOut}>
|
|
186
|
-
<ZoomOut className=
|
|
194
|
+
<Button variant="ghost" size="icon-sm" className={cn(isMobile && 'min-h-[44px] min-w-[44px]')} onClick={handleZoomOut}>
|
|
195
|
+
<ZoomOut className={cn(isMobile ? 'h-5 w-5' : 'h-4 w-4')} />
|
|
187
196
|
</Button>
|
|
188
197
|
</TooltipTrigger>
|
|
189
198
|
<TooltipContent side="left">Zoom Out (-)</TooltipContent>
|
|
@@ -191,17 +200,17 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
191
200
|
</div>
|
|
192
201
|
)}
|
|
193
202
|
|
|
194
|
-
{/* Context Info
|
|
203
|
+
{/* Context Info — Storey names. Top-center on mobile (URL bar steals the bottom). */}
|
|
195
204
|
{storeyNames && storeyNames.length > 0 && (
|
|
196
205
|
<div className={cn(
|
|
197
206
|
'absolute left-1/2 -translate-x-1/2 px-4 py-2 bg-background/80 backdrop-blur-sm rounded-full border shadow-sm',
|
|
198
|
-
basketPresentationVisible ? 'bottom-28' : 'bottom-4',
|
|
207
|
+
isMobile ? 'top-4' : basketPresentationVisible ? 'bottom-28' : 'bottom-4',
|
|
199
208
|
)}>
|
|
200
209
|
<div className="flex items-center gap-2 text-sm">
|
|
201
210
|
<Layers className="h-4 w-4 text-primary" />
|
|
202
211
|
<span className="font-medium">
|
|
203
|
-
{storeyNames.length === 1
|
|
204
|
-
? storeyNames[0]
|
|
212
|
+
{storeyNames.length === 1
|
|
213
|
+
? storeyNames[0]
|
|
205
214
|
: `${storeyNames.length} storeys`}
|
|
206
215
|
</span>
|
|
207
216
|
</div>
|
|
@@ -221,20 +230,22 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
221
230
|
</div>
|
|
222
231
|
)}
|
|
223
232
|
|
|
224
|
-
{/* Axis Helper
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
233
|
+
{/* Axis Helper + Scale Bar — desktop only; mobile keeps the viewport unobstructed */}
|
|
234
|
+
{!isMobile && (
|
|
235
|
+
<>
|
|
236
|
+
<div className="absolute bottom-16 left-4">
|
|
237
|
+
<AxisHelper
|
|
238
|
+
ref={axisHelperRef}
|
|
239
|
+
rotationX={initialRotationX}
|
|
240
|
+
rotationY={initialRotationY}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="absolute bottom-4 left-4 flex flex-col items-start gap-1">
|
|
244
|
+
<div className="h-1 w-24 bg-foreground/80 rounded-full" />
|
|
245
|
+
<span className="text-xs text-foreground/80">{formatScale(scale)}</span>
|
|
246
|
+
</div>
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
238
249
|
</>
|
|
239
250
|
);
|
|
240
251
|
}
|
|
@@ -323,5 +334,9 @@ function CesiumSettingsOverlay({
|
|
|
323
334
|
*/
|
|
324
335
|
function PointCloudPanelMount() {
|
|
325
336
|
const count = useViewerStore((s) => s.pointCloudAssetCount);
|
|
326
|
-
|
|
337
|
+
// Triangle total comes from the merged geometry result. The panel
|
|
338
|
+
// gates the BIM↔scan deviation compute button on triangleCount > 0
|
|
339
|
+
// so the user can't trigger an empty-BVH compute pass.
|
|
340
|
+
const triangleCount = useViewerStore((s) => s.geometryResult?.totalTriangles ?? 0);
|
|
341
|
+
return <PointCloudPanel assetCount={count} triangleCount={triangleCount} />;
|
|
327
342
|
}
|
|
@@ -56,6 +56,10 @@ export interface MouseHandlerContext {
|
|
|
56
56
|
snapEnabledRef: MutableRefObject<boolean>;
|
|
57
57
|
edgeLockStateRef: MutableRefObject<EdgeLockState>;
|
|
58
58
|
measurementConstraintEdgeRef: MutableRefObject<MeasurementConstraintEdge | null>;
|
|
59
|
+
/** Section tool: when true, the next click picks a face for the clip plane (issue #243). */
|
|
60
|
+
sectionPickModeRef?: MutableRefObject<boolean>;
|
|
61
|
+
/** Renderer model bounds at click time — passed to `setSectionPlaneFromFace` so the cardinal-fallback `position` percentage is correct. */
|
|
62
|
+
modelBoundsRef?: MutableRefObject<{ min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } } | null>;
|
|
59
63
|
|
|
60
64
|
// Visibility refs
|
|
61
65
|
hiddenEntitiesRef: MutableRefObject<Set<number>>;
|
|
@@ -101,6 +105,24 @@ export interface MouseHandlerContext {
|
|
|
101
105
|
openContextMenu: (entityId: number | null, screenX: number, screenY: number) => void;
|
|
102
106
|
hasPendingMeasurements: () => boolean;
|
|
103
107
|
getPickOptions: () => { isStreaming: boolean; hiddenIds: Set<number>; isolatedIds: Set<number> | null };
|
|
108
|
+
/** Section face-pick: set the clip plane through a world-space face (issue #243). */
|
|
109
|
+
setSectionPlaneFromFace?: (
|
|
110
|
+
normal: [number, number, number],
|
|
111
|
+
point: [number, number, number],
|
|
112
|
+
bounds?: { min: [number, number, number]; max: [number, number, number] },
|
|
113
|
+
) => void;
|
|
114
|
+
/** Section face-pick: arm/disarm the "next click picks a face" mode. */
|
|
115
|
+
setSectionPickMode?: (enabled: boolean) => void;
|
|
116
|
+
/**
|
|
117
|
+
* Section face-pick: set the live hover-preview overlay (issue #243
|
|
118
|
+
* follow-up). Called by the dwell-aware hover handler in
|
|
119
|
+
* `useMouseControls.ts` when the cursor pauses ~200ms over a surface,
|
|
120
|
+
* and with `null` when the preview should hide (cursor leaves the
|
|
121
|
+
* canvas, moves to a different face, or pick mode is disarmed).
|
|
122
|
+
*/
|
|
123
|
+
setSectionPickPreview?: (
|
|
124
|
+
preview: { normal: [number, number, number]; point: [number, number, number]; faceKey: string } | null,
|
|
125
|
+
) => void;
|
|
104
126
|
|
|
105
127
|
// Constants
|
|
106
128
|
HOVER_SNAP_THROTTLE_MS: number;
|