@pascal-app/viewer 1.0.7 → 1.1.1
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/README.md +209 -19
- package/dist/components/viewer/drone-controller.d.ts.map +1 -1
- package/dist/components/viewer/drone-controller.js +1 -5
- package/dist/components/viewer/first-person/build-collider-world.d.ts +14 -0
- package/dist/components/viewer/first-person/build-collider-world.d.ts.map +1 -0
- package/dist/components/viewer/first-person/build-collider-world.js +426 -0
- package/dist/components/viewer/first-person/ground-holes.d.ts +22 -0
- package/dist/components/viewer/first-person/ground-holes.d.ts.map +1 -0
- package/dist/components/viewer/first-person/ground-holes.js +116 -0
- package/dist/components/viewer/first-person/mechanism-targets.d.ts +27 -0
- package/dist/components/viewer/first-person/mechanism-targets.d.ts.map +1 -0
- package/dist/components/viewer/first-person/mechanism-targets.js +79 -0
- package/dist/components/viewer/first-person/terrain-collider.d.ts +32 -0
- package/dist/components/viewer/first-person/terrain-collider.d.ts.map +1 -0
- package/dist/components/viewer/first-person/terrain-collider.js +175 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene.d.ts +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +2 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +8 -4
- package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
- package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +22 -1
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
- package/dist/components/viewer/use-daylight-rooms.js +75 -33
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/room-context.d.ts.map +1 -1
- package/dist/lib/daylight/room-context.js +3 -2
- package/dist/lib/daylight/room-map.d.ts +149 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +813 -0
- package/dist/lib/daylight/room-scene.d.ts +4 -2
- package/dist/lib/daylight/room-scene.d.ts.map +1 -1
- package/dist/lib/daylight/room-scene.js +81 -40
- package/dist/lib/daylight/rooms.d.ts +71 -32
- package/dist/lib/daylight/rooms.d.ts.map +1 -1
- package/dist/lib/daylight/rooms.js +371 -286
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- package/dist/lib/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +7 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +14 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +4 -3
- package/dist/lib/typing-target.d.ts +2 -0
- package/dist/lib/typing-target.d.ts.map +1 -0
- package/dist/lib/typing-target.js +7 -0
- package/dist/lib/viewer-config.d.ts +27 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +23 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/sfx/audio-settings.d.ts +81 -0
- package/dist/sfx/audio-settings.d.ts.map +1 -0
- package/dist/sfx/audio-settings.js +166 -0
- package/dist/sfx/index.d.ts +6 -0
- package/dist/sfx/index.d.ts.map +1 -0
- package/dist/sfx/index.js +5 -0
- package/dist/sfx/interaction-sounds.d.ts +7 -0
- package/dist/sfx/interaction-sounds.d.ts.map +1 -0
- package/dist/sfx/interaction-sounds.js +39 -0
- package/dist/sfx/player.d.ts +31 -0
- package/dist/sfx/player.d.ts.map +1 -0
- package/dist/sfx/player.js +255 -0
- package/dist/sfx/sounds.d.ts +60 -0
- package/dist/sfx/sounds.d.ts.map +1 -0
- package/dist/sfx/sounds.js +25 -0
- package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
- package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
- package/dist/sfx/use-viewer-ui-sounds.js +42 -0
- package/dist/store/use-first-person-hud.d.ts +13 -0
- package/dist/store/use-first-person-hud.d.ts.map +1 -0
- package/dist/store/use-first-person-hud.js +21 -0
- package/dist/store/use-viewer.d.ts +2 -56
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +3 -1
- package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +134 -78
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +56 -10
- package/dist/ui/components/action-button.d.ts +14 -0
- package/dist/ui/components/action-button.d.ts.map +1 -0
- package/dist/ui/components/action-button.js +21 -0
- package/dist/ui/components/action-menu-placement.d.ts +7 -0
- package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
- package/dist/ui/components/action-menu-placement.js +10 -0
- package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
- package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
- package/dist/ui/components/floorplan-compass-button.js +22 -0
- package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
- package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
- package/dist/ui/components/follow-pascal-toggle.js +21 -0
- package/dist/ui/components/localized-html.d.ts +9 -0
- package/dist/ui/components/localized-html.d.ts.map +1 -0
- package/dist/ui/components/localized-html.js +16 -0
- package/dist/ui/components/render-settings-menu.d.ts +9 -0
- package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
- package/dist/ui/components/render-settings-menu.js +145 -0
- package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
- package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
- package/dist/ui/components/rooms/room-highlight.js +56 -0
- package/dist/ui/components/rooms/room-labels.d.ts +278 -0
- package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
- package/dist/ui/components/rooms/room-labels.js +1295 -0
- package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
- package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
- package/dist/ui/components/rooms/viewer-rooms.js +123 -0
- package/dist/ui/components/scene-loader.d.ts +18 -0
- package/dist/ui/components/scene-loader.d.ts.map +1 -0
- package/dist/ui/components/scene-loader.js +29 -0
- package/dist/ui/components/sky-control.d.ts +37 -0
- package/dist/ui/components/sky-control.d.ts.map +1 -0
- package/dist/ui/components/sky-control.js +382 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
- package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
- package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
- package/dist/ui/components/viewer-controls-bar.js +130 -0
- package/dist/ui/components/viewer-hover-label.d.ts +14 -0
- package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
- package/dist/ui/components/viewer-hover-label.js +28 -0
- package/dist/ui/components/viewer-scene-header.d.ts +34 -0
- package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
- package/dist/ui/components/viewer-scene-header.js +92 -0
- package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
- package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
- package/dist/ui/components/viewer-selection-manager.js +116 -0
- package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
- package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage-modes.js +42 -0
- package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
- package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage-switcher.js +16 -0
- package/dist/ui/components/viewer-stage.d.ts +25 -0
- package/dist/ui/components/viewer-stage.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage.js +107 -0
- package/dist/ui/components/walkthrough-hud.d.ts +29 -0
- package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
- package/dist/ui/components/walkthrough-hud.js +55 -0
- package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
- package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
- package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
- package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
- package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
- package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview.js +700 -0
- package/dist/ui/hooks/use-mobile.d.ts +2 -0
- package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
- package/dist/ui/hooks/use-mobile.js +14 -0
- package/dist/ui/index.d.ts +49 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +48 -0
- package/dist/ui/lib/below-grade.d.ts +38 -0
- package/dist/ui/lib/below-grade.d.ts.map +1 -0
- package/dist/ui/lib/below-grade.js +102 -0
- package/dist/ui/lib/camera-pose.d.ts +19 -0
- package/dist/ui/lib/camera-pose.d.ts.map +1 -0
- package/dist/ui/lib/camera-pose.js +113 -0
- package/dist/ui/lib/cn.d.ts +3 -0
- package/dist/ui/lib/cn.d.ts.map +1 -0
- package/dist/ui/lib/cn.js +5 -0
- package/dist/ui/lib/level-name.d.ts +10 -0
- package/dist/ui/lib/level-name.d.ts.map +1 -0
- package/dist/ui/lib/level-name.js +33 -0
- package/dist/ui/lib/node-display-label.d.ts +8 -0
- package/dist/ui/lib/node-display-label.d.ts.map +1 -0
- package/dist/ui/lib/node-display-label.js +36 -0
- package/dist/ui/lib/room-area.d.ts +3 -0
- package/dist/ui/lib/room-area.d.ts.map +1 -0
- package/dist/ui/lib/room-area.js +8 -0
- package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
- package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
- package/dist/ui/lib/room-assembly-overlay.js +222 -0
- package/dist/ui/lib/room-name-messages.d.ts +28 -0
- package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
- package/dist/ui/lib/room-name-messages.js +112 -0
- package/dist/ui/lib/room-selection.d.ts +58 -0
- package/dist/ui/lib/room-selection.d.ts.map +1 -0
- package/dist/ui/lib/room-selection.js +297 -0
- package/dist/ui/lib/scene-theme-name.d.ts +4 -0
- package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
- package/dist/ui/lib/scene-theme-name.js +22 -0
- package/dist/ui/lib/selection-modifiers.d.ts +20 -0
- package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
- package/dist/ui/lib/selection-modifiers.js +15 -0
- package/dist/ui/lib/viewer-selection.d.ts +132 -0
- package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
- package/dist/ui/lib/viewer-selection.js +462 -0
- package/dist/ui/lib/zone-content.d.ts +6 -0
- package/dist/ui/lib/zone-content.d.ts.map +1 -0
- package/dist/ui/lib/zone-content.js +96 -0
- package/dist/ui/primitives/button.d.ts +11 -0
- package/dist/ui/primitives/button.d.ts.map +1 -0
- package/dist/ui/primitives/button.js +35 -0
- package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
- package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
- package/dist/ui/primitives/dropdown-menu.js +53 -0
- package/dist/ui/primitives/popover.d.ts +8 -0
- package/dist/ui/primitives/popover.d.ts.map +1 -0
- package/dist/ui/primitives/popover.js +17 -0
- package/dist/ui/primitives/switch.d.ts +5 -0
- package/dist/ui/primitives/switch.d.ts.map +1 -0
- package/dist/ui/primitives/switch.js +8 -0
- package/dist/ui/primitives/tooltip.d.ts +8 -0
- package/dist/ui/primitives/tooltip.d.ts.map +1 -0
- package/dist/ui/primitives/tooltip.js +17 -0
- package/dist/ui/store/camera-pose-store.d.ts +17 -0
- package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
- package/dist/ui/store/camera-pose-store.js +15 -0
- package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
- package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
- package/dist/ui/store/navigation-sync-pose-store.js +16 -0
- package/dist/ui/store/use-follow-pascal.d.ts +30 -0
- package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
- package/dist/ui/store/use-follow-pascal.js +28 -0
- package/dist/ui/theme.css +339 -0
- package/package.json +34 -7
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { buildFloorplanContext, FLOORPLAN_VIEW_ROTATION_DEG, floorplanLayerRank, floorplanLocalToWorldPoint, isNodeKindEnabled, nodeRegistry, useLocale, usePascalI18n, useScene, worldToFloorplanLocalPoint, } from '@pascal-app/core';
|
|
4
|
+
import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema';
|
|
5
|
+
import { Maximize2, Minus, Plus } from 'lucide-react';
|
|
6
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
7
|
+
import { createPortal } from 'react-dom';
|
|
8
|
+
import useViewer from '../../store/use-viewer';
|
|
9
|
+
import { FloorplanCompassButton } from '../components/floorplan-compass-button';
|
|
10
|
+
import { localizedLevelName } from '../lib/level-name';
|
|
11
|
+
import { publishNavigationSyncPose, subscribeNavigationSyncPose, } from '../store/navigation-sync-pose-store';
|
|
12
|
+
import { subscribeFloorplanCameraNavigation, useFloorplanCameraSyncBridge, } from './floorplan-camera-sync';
|
|
13
|
+
import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer';
|
|
14
|
+
import { createFloorplanNavigationSyncScheduler, setFloorplanCompassRotation, } from './floorplan-navigation-sync';
|
|
15
|
+
import { getFloorplanBounds, padFloorplanBounds, panFloorplanViewBox, scaleFloorplanViewBox, scaleFloorplanViewBoxBetweenClients, } from './floorplan-preview-geometry';
|
|
16
|
+
import { cameraAzimuthFromFloorplanRotation, floorplanRotationFromCameraAzimuth, floorplanViewBoxFromNavigationPose, nearestEquivalentDegrees, rotateFloorplanPoint, visibleFloorplanViewWidth, } from './floorplan-preview-navigation';
|
|
17
|
+
import { isVisibleInFloorplan } from './floorplan-preview-visibility';
|
|
18
|
+
const READ_ONLY_PALETTE = {
|
|
19
|
+
selectedStroke: '#4f46e5',
|
|
20
|
+
selectedFill: '#e0e7ff',
|
|
21
|
+
selectedHatch: '#818cf8',
|
|
22
|
+
wallHoverStroke: '#64748b',
|
|
23
|
+
endpointHandleFill: '#ffffff',
|
|
24
|
+
endpointHandleStroke: '#f97316',
|
|
25
|
+
endpointHandleHoverStroke: '#fb923c',
|
|
26
|
+
endpointHandleActiveFill: '#fed7aa',
|
|
27
|
+
endpointHandleActiveStroke: '#ea580c',
|
|
28
|
+
curveHandleFill: '#ffffff',
|
|
29
|
+
curveHandleStroke: '#0d9488',
|
|
30
|
+
curveHandleHoverStroke: '#14b8a6',
|
|
31
|
+
measurementStroke: '#475569',
|
|
32
|
+
measurementLabelBackground: '#ffffff',
|
|
33
|
+
measurementLabelText: '#0f172a',
|
|
34
|
+
};
|
|
35
|
+
const EMPTY_PREVIEW_NODES = {};
|
|
36
|
+
const EMPTY_INSTALLED_PLUGINS = [];
|
|
37
|
+
const useClientLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
38
|
+
function FloorplanCameraSyncMount() {
|
|
39
|
+
useFloorplanCameraSyncBridge();
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function boundsToViewBox(bounds) {
|
|
43
|
+
return {
|
|
44
|
+
x: bounds.minX,
|
|
45
|
+
y: bounds.minY,
|
|
46
|
+
width: Math.max(bounds.maxX - bounds.minX, 1),
|
|
47
|
+
height: Math.max(bounds.maxY - bounds.minY, 1),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function collectLevelTree(root, nodes) {
|
|
51
|
+
const result = [root];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const queue = [...(root.children ?? [])];
|
|
54
|
+
let index = 0;
|
|
55
|
+
while (index < queue.length) {
|
|
56
|
+
const id = queue[index++];
|
|
57
|
+
if (!id || seen.has(id))
|
|
58
|
+
continue;
|
|
59
|
+
seen.add(id);
|
|
60
|
+
const node = nodes[id];
|
|
61
|
+
if (!node)
|
|
62
|
+
continue;
|
|
63
|
+
result.push(node);
|
|
64
|
+
queue.push(...(node.children ?? []));
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
export function normalizeFloorplanPreviewNodes(nodes) {
|
|
69
|
+
const normalized = {};
|
|
70
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
71
|
+
const builtin = AnyNodeSchema.safeParse(node);
|
|
72
|
+
if (builtin.success) {
|
|
73
|
+
normalized[builtin.data.id] = builtin.data;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const type = node && typeof node === 'object' && !Array.isArray(node)
|
|
77
|
+
? node.type
|
|
78
|
+
: null;
|
|
79
|
+
const registered = typeof type === 'string' ? nodeRegistry.get(type)?.schema.safeParse(node) : null;
|
|
80
|
+
if (registered?.success) {
|
|
81
|
+
const parsed = registered.data;
|
|
82
|
+
normalized[parsed.id] = parsed;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.warn(`[floorplan-preview] Skipping invalid node ${id}`, builtin.error.issues);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
function buildFloorplanGeometries(nodes, installedPlugins, level, unit, metricNotation) {
|
|
91
|
+
const building = level.parentId ? nodes[level.parentId] : undefined;
|
|
92
|
+
const levelTree = collectLevelTree(level, nodes);
|
|
93
|
+
const entries = levelTree.map((node) => ({ node }));
|
|
94
|
+
const entryIds = new Set(entries.map((entry) => entry.node.id));
|
|
95
|
+
if (building) {
|
|
96
|
+
for (const candidate of Object.values(nodes)) {
|
|
97
|
+
const definition = nodeRegistry.get(candidate.type);
|
|
98
|
+
if (definition?.floorplanScope === 'building' &&
|
|
99
|
+
candidate.parentId === building.id &&
|
|
100
|
+
!entryIds.has(candidate.id)) {
|
|
101
|
+
entries.push({
|
|
102
|
+
node: candidate,
|
|
103
|
+
contextOverrides: { children: [], siblings: [], parent: level },
|
|
104
|
+
});
|
|
105
|
+
entryIds.add(candidate.id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const candidate of Object.values(nodes)) {
|
|
110
|
+
const definition = nodeRegistry.get(candidate.type);
|
|
111
|
+
const linkedLevelIds = definition?.floorplanLinkedLevelIds;
|
|
112
|
+
if (definition?.floorplan &&
|
|
113
|
+
linkedLevelIds?.(candidate).includes(level.id) &&
|
|
114
|
+
!entryIds.has(candidate.id)) {
|
|
115
|
+
const childIds = candidate.children;
|
|
116
|
+
const children = Array.isArray(childIds)
|
|
117
|
+
? childIds.map((id) => nodes[id]).filter((child) => child !== undefined)
|
|
118
|
+
: [];
|
|
119
|
+
entries.push({
|
|
120
|
+
node: candidate,
|
|
121
|
+
contextOverrides: { children, siblings: [], parent: level },
|
|
122
|
+
});
|
|
123
|
+
entryIds.add(candidate.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const renderable = entries
|
|
127
|
+
.filter((entry) => isVisibleInFloorplan(entry.node, nodes))
|
|
128
|
+
.filter((entry) => isNodeKindEnabled(entry.node.type, installedPlugins))
|
|
129
|
+
.filter((entry) => nodeRegistry.get(entry.node.type)?.floorplan)
|
|
130
|
+
.sort((a, b) => floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type));
|
|
131
|
+
const byType = new Map();
|
|
132
|
+
for (const node of levelTree) {
|
|
133
|
+
if (!isNodeKindEnabled(node.type, installedPlugins))
|
|
134
|
+
continue;
|
|
135
|
+
if (!nodeRegistry.get(node.type)?.computeFloorplanLevelData)
|
|
136
|
+
continue;
|
|
137
|
+
const siblings = byType.get(node.type) ?? [];
|
|
138
|
+
siblings.push(node);
|
|
139
|
+
byType.set(node.type, siblings);
|
|
140
|
+
}
|
|
141
|
+
const levelData = new Map();
|
|
142
|
+
for (const [type, siblings] of byType) {
|
|
143
|
+
const definition = nodeRegistry.get(type);
|
|
144
|
+
if (definition?.computeFloorplanLevelData) {
|
|
145
|
+
levelData.set(type, definition.computeFloorplanLevelData({ siblings, nodes }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const geometries = [];
|
|
149
|
+
for (const { node, contextOverrides } of renderable) {
|
|
150
|
+
const definition = nodeRegistry.get(node.type);
|
|
151
|
+
if (!definition?.floorplan)
|
|
152
|
+
continue;
|
|
153
|
+
const baseContext = buildFloorplanContext(node, nodes, {
|
|
154
|
+
automaticDimensions: false,
|
|
155
|
+
selected: false,
|
|
156
|
+
unit,
|
|
157
|
+
metricNotation,
|
|
158
|
+
purpose: 'document',
|
|
159
|
+
highlighted: false,
|
|
160
|
+
hovered: false,
|
|
161
|
+
moving: false,
|
|
162
|
+
palette: READ_ONLY_PALETTE,
|
|
163
|
+
}, levelData.get(node.type));
|
|
164
|
+
const context = contextOverrides ? { ...baseContext, ...contextOverrides } : baseContext;
|
|
165
|
+
try {
|
|
166
|
+
const geometry = definition.floorplan(node, context);
|
|
167
|
+
if (geometry) {
|
|
168
|
+
geometries.push({
|
|
169
|
+
id: node.id,
|
|
170
|
+
geometry,
|
|
171
|
+
includeInInitialFit: definition.category !== 'furnish',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error(`[floorplan-preview] Failed to render ${node.type}:${node.id}`, error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return geometries;
|
|
180
|
+
}
|
|
181
|
+
export function FloorplanPreview({ className, compassHost, levelId, navigationVisible = true, onLevelChange, scene, showCompass = true, showLevelSelector = true, synchronizeNavigation = false, }) {
|
|
182
|
+
const i18n = usePascalI18n();
|
|
183
|
+
const locale = useLocale();
|
|
184
|
+
const storeNodes = useScene((state) => (scene ? EMPTY_PREVIEW_NODES : state.nodes));
|
|
185
|
+
const storeInstalledPlugins = useScene((state) => scene ? EMPTY_INSTALLED_PLUGINS : state.installedPlugins);
|
|
186
|
+
const unit = useViewer((state) => state.unit);
|
|
187
|
+
const metricNotation = useViewer((state) => state.metricNotation);
|
|
188
|
+
const externalNodes = useMemo(() => (scene?.nodes ? normalizeFloorplanPreviewNodes(scene.nodes) : null), [scene?.nodes]);
|
|
189
|
+
const nodes = externalNodes ?? storeNodes;
|
|
190
|
+
const installedPlugins = scene ? scene.installedPlugins : storeInstalledPlugins;
|
|
191
|
+
const levels = useMemo(() => Object.values(nodes)
|
|
192
|
+
.filter((node) => node.type === 'level')
|
|
193
|
+
.sort((a, b) => (a.level ?? 0) - (b.level ?? 0)), [nodes]);
|
|
194
|
+
const [internalLevelId, setInternalLevelId] = useState(null);
|
|
195
|
+
const activeLevelId = (levelId && levels.some((level) => level.id === levelId) ? levelId : null) ??
|
|
196
|
+
(internalLevelId && levels.some((level) => level.id === internalLevelId)
|
|
197
|
+
? internalLevelId
|
|
198
|
+
: null) ??
|
|
199
|
+
levels[0]?.id ??
|
|
200
|
+
null;
|
|
201
|
+
const activeLevel = activeLevelId ? nodes[activeLevelId] : undefined;
|
|
202
|
+
const activeBuilding = activeLevel?.parentId
|
|
203
|
+
? nodes[activeLevel.parentId]
|
|
204
|
+
: undefined;
|
|
205
|
+
const buildingPosition = useMemo(() => (activeBuilding?.type === 'building' ? activeBuilding.position : [0, 0, 0]), [activeBuilding]);
|
|
206
|
+
const buildingRotationY = activeBuilding?.type === 'building' ? activeBuilding.rotation[1] : 0;
|
|
207
|
+
const buildingRotationDeg = (buildingRotationY * 180) / Math.PI;
|
|
208
|
+
const renderEntries = useMemo(() => activeLevel
|
|
209
|
+
? buildFloorplanGeometries(nodes, installedPlugins, activeLevel, unit, metricNotation)
|
|
210
|
+
: [], [activeLevel, installedPlugins, metricNotation, nodes, unit]);
|
|
211
|
+
const framingEntries = useMemo(() => {
|
|
212
|
+
const structural = renderEntries.filter((entry) => entry.includeInInitialFit);
|
|
213
|
+
return structural.length > 0 ? structural : renderEntries;
|
|
214
|
+
}, [renderEntries]);
|
|
215
|
+
const fitKey = `${activeLevelId ?? 'none'}:${framingEntries.map((entry) => entry.id).join('|')}`;
|
|
216
|
+
const geometricFitBounds = useMemo(() => getFloorplanBounds(framingEntries.map((entry) => entry.geometry)), [framingEntries]);
|
|
217
|
+
const [measuredFit, setMeasuredFit] = useState(null);
|
|
218
|
+
const fittedViewBox = useMemo(() => {
|
|
219
|
+
const bounds = measuredFit?.key === fitKey ? measuredFit.bounds : geometricFitBounds;
|
|
220
|
+
return bounds
|
|
221
|
+
? boundsToViewBox(padFloorplanBounds(bounds))
|
|
222
|
+
: { x: -5, y: -5, width: 10, height: 10 };
|
|
223
|
+
}, [fitKey, geometricFitBounds, measuredFit]);
|
|
224
|
+
const [viewBox, setViewBox] = useState(fittedViewBox);
|
|
225
|
+
const viewBoxRef = useRef(viewBox);
|
|
226
|
+
const interactionRef = useRef(null);
|
|
227
|
+
const svgRef = useRef(null);
|
|
228
|
+
const gridRef = useRef(null);
|
|
229
|
+
const sceneRef = useRef(null);
|
|
230
|
+
const compassNeedleRef = useRef(null);
|
|
231
|
+
const fitElementRefs = useRef(new Map());
|
|
232
|
+
const pointersRef = useRef(new Map());
|
|
233
|
+
const dragRef = useRef(null);
|
|
234
|
+
const pinchRef = useRef(null);
|
|
235
|
+
const [isPanning, setIsPanning] = useState(false);
|
|
236
|
+
const [isRotating, setIsRotating] = useState(false);
|
|
237
|
+
const [viewportSize, setViewportSize] = useState({ width: 1000, height: 1000 });
|
|
238
|
+
const [rotationDeg, setRotationDeg] = useState(0);
|
|
239
|
+
const rotationDegRef = useRef(rotationDeg);
|
|
240
|
+
const latestNavigationPoseRef = useRef(null);
|
|
241
|
+
const gridPatternId = useId().replaceAll(':', '');
|
|
242
|
+
const updateViewBox = useCallback((update) => {
|
|
243
|
+
setViewBox((current) => {
|
|
244
|
+
const next = typeof update === 'function' ? update(current) : update;
|
|
245
|
+
viewBoxRef.current = next;
|
|
246
|
+
return next;
|
|
247
|
+
});
|
|
248
|
+
}, []);
|
|
249
|
+
const presentViewBox = useCallback((next) => {
|
|
250
|
+
viewBoxRef.current = next;
|
|
251
|
+
const value = `${next.x} ${next.y} ${next.width} ${next.height}`;
|
|
252
|
+
svgRef.current?.setAttribute('viewBox', value);
|
|
253
|
+
const grid = gridRef.current;
|
|
254
|
+
if (grid) {
|
|
255
|
+
grid.setAttribute('x', String(next.x));
|
|
256
|
+
grid.setAttribute('y', String(next.y));
|
|
257
|
+
grid.setAttribute('width', String(next.width));
|
|
258
|
+
grid.setAttribute('height', String(next.height));
|
|
259
|
+
}
|
|
260
|
+
}, []);
|
|
261
|
+
const presentRotation = useCallback((nextRotationDeg) => {
|
|
262
|
+
rotationDegRef.current = nextRotationDeg;
|
|
263
|
+
const sceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg;
|
|
264
|
+
const sceneElement = sceneRef.current;
|
|
265
|
+
if (sceneElement) {
|
|
266
|
+
if (sceneRotationDeg === 0)
|
|
267
|
+
sceneElement.removeAttribute('transform');
|
|
268
|
+
else
|
|
269
|
+
sceneElement.setAttribute('transform', `rotate(${sceneRotationDeg})`);
|
|
270
|
+
}
|
|
271
|
+
setFloorplanCompassRotation(compassNeedleRef.current, nextRotationDeg);
|
|
272
|
+
}, [buildingRotationDeg]);
|
|
273
|
+
const commitPresentation = useCallback((nextViewBox, nextRotationDeg) => {
|
|
274
|
+
presentViewBox(nextViewBox);
|
|
275
|
+
presentRotation(nextRotationDeg);
|
|
276
|
+
setViewBox(nextViewBox);
|
|
277
|
+
setRotationDeg(nextRotationDeg);
|
|
278
|
+
}, [presentRotation, presentViewBox]);
|
|
279
|
+
const publishNavigation = useCallback((nextViewBox, nextRotationDeg) => {
|
|
280
|
+
if (!synchronizeNavigation)
|
|
281
|
+
return;
|
|
282
|
+
const sceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg;
|
|
283
|
+
const displayedCenter = {
|
|
284
|
+
x: nextViewBox.x + nextViewBox.width / 2,
|
|
285
|
+
y: nextViewBox.y + nextViewBox.height / 2,
|
|
286
|
+
};
|
|
287
|
+
const localCenter = rotateFloorplanPoint(displayedCenter, -sceneRotationDeg);
|
|
288
|
+
const worldCenter = floorplanLocalToWorldPoint(localCenter, buildingPosition, buildingRotationY);
|
|
289
|
+
publishNavigationSyncPose({
|
|
290
|
+
source: '2d',
|
|
291
|
+
target: [
|
|
292
|
+
worldCenter.x,
|
|
293
|
+
latestNavigationPoseRef.current?.target[1] ?? buildingPosition[1],
|
|
294
|
+
worldCenter.z,
|
|
295
|
+
],
|
|
296
|
+
azimuth: cameraAzimuthFromFloorplanRotation(nextRotationDeg),
|
|
297
|
+
viewWidth: visibleFloorplanViewWidth(nextViewBox, viewportSize),
|
|
298
|
+
});
|
|
299
|
+
}, [buildingPosition, buildingRotationDeg, buildingRotationY, synchronizeNavigation, viewportSize]);
|
|
300
|
+
const applyNavigationPresentationRef = useRef(() => { });
|
|
301
|
+
const commitNavigationPresentationRef = useRef(() => { });
|
|
302
|
+
const navigationSchedulerRef = useRef(null);
|
|
303
|
+
if (!navigationSchedulerRef.current) {
|
|
304
|
+
navigationSchedulerRef.current = createFloorplanNavigationSyncScheduler({
|
|
305
|
+
applyPresentation: (pose) => applyNavigationPresentationRef.current(pose),
|
|
306
|
+
commit: (pose) => commitNavigationPresentationRef.current(pose),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
applyNavigationPresentationRef.current = (pose) => {
|
|
310
|
+
latestNavigationPoseRef.current = pose;
|
|
311
|
+
const nextRotationDeg = floorplanRotationFromCameraAzimuth(pose.azimuth, rotationDegRef.current);
|
|
312
|
+
presentRotation(nextRotationDeg);
|
|
313
|
+
if (!navigationVisible)
|
|
314
|
+
return;
|
|
315
|
+
const localCenter = worldToFloorplanLocalPoint(pose.target[0], pose.target[2], buildingPosition, buildingRotationY);
|
|
316
|
+
presentViewBox(floorplanViewBoxFromNavigationPose(pose, localCenter, FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg, viewportSize));
|
|
317
|
+
};
|
|
318
|
+
commitNavigationPresentationRef.current = (_pose) => {
|
|
319
|
+
if (!navigationVisible)
|
|
320
|
+
return;
|
|
321
|
+
setViewBox(viewBoxRef.current);
|
|
322
|
+
setRotationDeg(rotationDegRef.current);
|
|
323
|
+
};
|
|
324
|
+
useClientLayoutEffect(() => {
|
|
325
|
+
let bounds = null;
|
|
326
|
+
for (const entry of framingEntries) {
|
|
327
|
+
const element = fitElementRefs.current.get(entry.id);
|
|
328
|
+
if (!element || typeof element.getBBox !== 'function')
|
|
329
|
+
continue;
|
|
330
|
+
let box;
|
|
331
|
+
try {
|
|
332
|
+
box = element.getBBox();
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (![box.x, box.y, box.width, box.height].every(Number.isFinite))
|
|
338
|
+
continue;
|
|
339
|
+
if (box.width === 0 && box.height === 0)
|
|
340
|
+
continue;
|
|
341
|
+
const next = {
|
|
342
|
+
minX: box.x,
|
|
343
|
+
minY: box.y,
|
|
344
|
+
maxX: box.x + box.width,
|
|
345
|
+
maxY: box.y + box.height,
|
|
346
|
+
};
|
|
347
|
+
bounds = bounds
|
|
348
|
+
? {
|
|
349
|
+
minX: Math.min(bounds.minX, next.minX),
|
|
350
|
+
minY: Math.min(bounds.minY, next.minY),
|
|
351
|
+
maxX: Math.max(bounds.maxX, next.maxX),
|
|
352
|
+
maxY: Math.max(bounds.maxY, next.maxY),
|
|
353
|
+
}
|
|
354
|
+
: next;
|
|
355
|
+
}
|
|
356
|
+
if (bounds)
|
|
357
|
+
setMeasuredFit({ key: fitKey, bounds });
|
|
358
|
+
}, [fitKey, framingEntries]);
|
|
359
|
+
useClientLayoutEffect(() => {
|
|
360
|
+
if (synchronizeNavigation && latestNavigationPoseRef.current)
|
|
361
|
+
return;
|
|
362
|
+
updateViewBox(fittedViewBox);
|
|
363
|
+
pointersRef.current.clear();
|
|
364
|
+
dragRef.current = null;
|
|
365
|
+
pinchRef.current = null;
|
|
366
|
+
setIsPanning(false);
|
|
367
|
+
setIsRotating(false);
|
|
368
|
+
}, [fittedViewBox, synchronizeNavigation, updateViewBox]);
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const svg = svgRef.current;
|
|
371
|
+
if (!svg)
|
|
372
|
+
return;
|
|
373
|
+
const updateSize = () => {
|
|
374
|
+
const rect = svg.getBoundingClientRect();
|
|
375
|
+
setViewportSize({ width: Math.max(rect.width, 1), height: Math.max(rect.height, 1) });
|
|
376
|
+
};
|
|
377
|
+
updateSize();
|
|
378
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
379
|
+
window.addEventListener('resize', updateSize);
|
|
380
|
+
return () => window.removeEventListener('resize', updateSize);
|
|
381
|
+
}
|
|
382
|
+
const observer = new ResizeObserver(updateSize);
|
|
383
|
+
observer.observe(svg);
|
|
384
|
+
return () => observer.disconnect();
|
|
385
|
+
}, []);
|
|
386
|
+
useClientLayoutEffect(() => {
|
|
387
|
+
presentRotation(rotationDegRef.current);
|
|
388
|
+
}, [presentRotation]);
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
if (!synchronizeNavigation)
|
|
391
|
+
return;
|
|
392
|
+
const scheduler = navigationSchedulerRef.current;
|
|
393
|
+
if (!scheduler)
|
|
394
|
+
return;
|
|
395
|
+
const receivePose = (pose) => {
|
|
396
|
+
latestNavigationPoseRef.current = pose;
|
|
397
|
+
if (navigationVisible)
|
|
398
|
+
scheduler.update(pose);
|
|
399
|
+
else
|
|
400
|
+
applyNavigationPresentationRef.current(pose);
|
|
401
|
+
};
|
|
402
|
+
const unsubscribeCamera = subscribeFloorplanCameraNavigation(receivePose);
|
|
403
|
+
const unsubscribeStored = subscribeNavigationSyncPose((pose) => {
|
|
404
|
+
if (pose.source === '2d' && !navigationVisible)
|
|
405
|
+
receivePose(pose);
|
|
406
|
+
});
|
|
407
|
+
return () => {
|
|
408
|
+
unsubscribeCamera();
|
|
409
|
+
unsubscribeStored();
|
|
410
|
+
scheduler.discard();
|
|
411
|
+
};
|
|
412
|
+
}, [navigationVisible, synchronizeNavigation]);
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
if (!(synchronizeNavigation && navigationVisible && latestNavigationPoseRef.current))
|
|
415
|
+
return;
|
|
416
|
+
navigationSchedulerRef.current?.update(latestNavigationPoseRef.current);
|
|
417
|
+
}, [navigationVisible, synchronizeNavigation]);
|
|
418
|
+
const chooseLevel = useCallback((nextLevelId) => {
|
|
419
|
+
setInternalLevelId(nextLevelId);
|
|
420
|
+
onLevelChange?.(nextLevelId);
|
|
421
|
+
}, [onLevelChange]);
|
|
422
|
+
const updateLocalViewBox = useCallback((update, commit = true) => {
|
|
423
|
+
const current = viewBoxRef.current;
|
|
424
|
+
const next = typeof update === 'function' ? update(current) : update;
|
|
425
|
+
presentViewBox(next);
|
|
426
|
+
if (commit)
|
|
427
|
+
setViewBox(next);
|
|
428
|
+
publishNavigation(next, rotationDegRef.current);
|
|
429
|
+
return next;
|
|
430
|
+
}, [presentViewBox, publishNavigation]);
|
|
431
|
+
const zoom = useCallback((factor, anchorX = 0.5, anchorY = 0.5) => {
|
|
432
|
+
updateLocalViewBox((current) => scaleFloorplanViewBox(current, factor, anchorX, anchorY));
|
|
433
|
+
}, [updateLocalViewBox]);
|
|
434
|
+
const onWheel = useCallback((event) => {
|
|
435
|
+
event.preventDefault();
|
|
436
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
437
|
+
updateLocalViewBox((current) => scaleFloorplanViewBoxBetweenClients(current, event.deltaY > 0 ? 1.12 : 0.88, rect, [event.clientX, event.clientY], [event.clientX, event.clientY]));
|
|
438
|
+
}, [updateLocalViewBox]);
|
|
439
|
+
const onPointerDown = useCallback((event) => {
|
|
440
|
+
if (event.pointerType === 'mouse' && event.button !== 0 && event.button !== 2)
|
|
441
|
+
return;
|
|
442
|
+
event.preventDefault();
|
|
443
|
+
interactionRef.current?.focus();
|
|
444
|
+
try {
|
|
445
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// Pointer capture is an enhancement; the gesture still works while events stay over the SVG.
|
|
449
|
+
}
|
|
450
|
+
const point = { x: event.clientX, y: event.clientY };
|
|
451
|
+
pointersRef.current.set(event.pointerId, point);
|
|
452
|
+
const pointers = Array.from(pointersRef.current.entries());
|
|
453
|
+
if (pointers.length === 1) {
|
|
454
|
+
dragRef.current = {
|
|
455
|
+
mode: event.pointerType === 'mouse' && event.button === 2 ? 'rotate' : 'pan',
|
|
456
|
+
pointerId: event.pointerId,
|
|
457
|
+
point,
|
|
458
|
+
rotationDeg: rotationDegRef.current,
|
|
459
|
+
viewBox: viewBoxRef.current,
|
|
460
|
+
};
|
|
461
|
+
pinchRef.current = null;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const [first, second] = pointers;
|
|
465
|
+
if (!first || !second)
|
|
466
|
+
return;
|
|
467
|
+
const dx = second[1].x - first[1].x;
|
|
468
|
+
const dy = second[1].y - first[1].y;
|
|
469
|
+
pinchRef.current = {
|
|
470
|
+
pointerIds: [first[0], second[0]],
|
|
471
|
+
midpoint: { x: (first[1].x + second[1].x) / 2, y: (first[1].y + second[1].y) / 2 },
|
|
472
|
+
distance: Math.max(Math.hypot(dx, dy), 1),
|
|
473
|
+
viewBox: viewBoxRef.current,
|
|
474
|
+
};
|
|
475
|
+
dragRef.current = null;
|
|
476
|
+
}
|
|
477
|
+
setIsPanning(true);
|
|
478
|
+
setIsRotating(dragRef.current?.mode === 'rotate');
|
|
479
|
+
}, []);
|
|
480
|
+
const onPointerMove = useCallback((event) => {
|
|
481
|
+
if (!pointersRef.current.has(event.pointerId))
|
|
482
|
+
return;
|
|
483
|
+
pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
484
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
485
|
+
const pinch = pinchRef.current;
|
|
486
|
+
if (pinch) {
|
|
487
|
+
const first = pointersRef.current.get(pinch.pointerIds[0]);
|
|
488
|
+
const second = pointersRef.current.get(pinch.pointerIds[1]);
|
|
489
|
+
if (!(first && second))
|
|
490
|
+
return;
|
|
491
|
+
const dx = second.x - first.x;
|
|
492
|
+
const dy = second.y - first.y;
|
|
493
|
+
const midpoint = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
|
|
494
|
+
updateLocalViewBox(scaleFloorplanViewBoxBetweenClients(pinch.viewBox, pinch.distance / Math.max(Math.hypot(dx, dy), 1), rect, [pinch.midpoint.x, pinch.midpoint.y], [midpoint.x, midpoint.y]), false);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const drag = dragRef.current;
|
|
498
|
+
if (!drag || drag.pointerId !== event.pointerId)
|
|
499
|
+
return;
|
|
500
|
+
if (drag.mode === 'rotate') {
|
|
501
|
+
const nextRotationDeg = drag.rotationDeg + (event.clientX - drag.point.x) * 0.35;
|
|
502
|
+
const initialSceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + drag.rotationDeg - buildingRotationDeg;
|
|
503
|
+
const nextSceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg;
|
|
504
|
+
const displayedCenter = {
|
|
505
|
+
x: drag.viewBox.x + drag.viewBox.width / 2,
|
|
506
|
+
y: drag.viewBox.y + drag.viewBox.height / 2,
|
|
507
|
+
};
|
|
508
|
+
const localCenter = rotateFloorplanPoint(displayedCenter, -initialSceneRotationDeg);
|
|
509
|
+
const nextCenter = rotateFloorplanPoint(localCenter, nextSceneRotationDeg);
|
|
510
|
+
const nextViewBox = {
|
|
511
|
+
...drag.viewBox,
|
|
512
|
+
x: nextCenter.x - drag.viewBox.width / 2,
|
|
513
|
+
y: nextCenter.y - drag.viewBox.height / 2,
|
|
514
|
+
};
|
|
515
|
+
presentRotation(nextRotationDeg);
|
|
516
|
+
presentViewBox(nextViewBox);
|
|
517
|
+
publishNavigation(nextViewBox, nextRotationDeg);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
updateLocalViewBox(panFloorplanViewBox(drag.viewBox, rect, [drag.point.x, drag.point.y], [event.clientX, event.clientY]), false);
|
|
521
|
+
}, [buildingRotationDeg, presentRotation, presentViewBox, publishNavigation, updateLocalViewBox]);
|
|
522
|
+
const onPointerUp = useCallback((event) => {
|
|
523
|
+
pointersRef.current.delete(event.pointerId);
|
|
524
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
525
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
526
|
+
}
|
|
527
|
+
pinchRef.current = null;
|
|
528
|
+
const [remaining] = pointersRef.current.entries();
|
|
529
|
+
if (remaining) {
|
|
530
|
+
dragRef.current = {
|
|
531
|
+
mode: 'pan',
|
|
532
|
+
pointerId: remaining[0],
|
|
533
|
+
point: remaining[1],
|
|
534
|
+
rotationDeg: rotationDegRef.current,
|
|
535
|
+
viewBox: viewBoxRef.current,
|
|
536
|
+
};
|
|
537
|
+
setIsRotating(false);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
dragRef.current = null;
|
|
541
|
+
setIsPanning(false);
|
|
542
|
+
setIsRotating(false);
|
|
543
|
+
setViewBox(viewBoxRef.current);
|
|
544
|
+
setRotationDeg(rotationDegRef.current);
|
|
545
|
+
}
|
|
546
|
+
}, []);
|
|
547
|
+
const onKeyDown = useCallback((event) => {
|
|
548
|
+
const panStep = 0.08;
|
|
549
|
+
switch (event.key) {
|
|
550
|
+
case '+':
|
|
551
|
+
case '=':
|
|
552
|
+
event.preventDefault();
|
|
553
|
+
zoom(0.8);
|
|
554
|
+
return;
|
|
555
|
+
case '-':
|
|
556
|
+
case '_':
|
|
557
|
+
event.preventDefault();
|
|
558
|
+
zoom(1.2);
|
|
559
|
+
return;
|
|
560
|
+
case '0':
|
|
561
|
+
case 'f':
|
|
562
|
+
case 'F':
|
|
563
|
+
event.preventDefault();
|
|
564
|
+
updateLocalViewBox(fittedViewBox);
|
|
565
|
+
return;
|
|
566
|
+
case 'ArrowLeft':
|
|
567
|
+
event.preventDefault();
|
|
568
|
+
updateLocalViewBox((current) => ({
|
|
569
|
+
...current,
|
|
570
|
+
x: current.x - current.width * panStep,
|
|
571
|
+
}));
|
|
572
|
+
return;
|
|
573
|
+
case 'ArrowRight':
|
|
574
|
+
event.preventDefault();
|
|
575
|
+
updateLocalViewBox((current) => ({
|
|
576
|
+
...current,
|
|
577
|
+
x: current.x + current.width * panStep,
|
|
578
|
+
}));
|
|
579
|
+
return;
|
|
580
|
+
case 'ArrowUp':
|
|
581
|
+
event.preventDefault();
|
|
582
|
+
updateLocalViewBox((current) => ({
|
|
583
|
+
...current,
|
|
584
|
+
y: current.y - current.height * panStep,
|
|
585
|
+
}));
|
|
586
|
+
return;
|
|
587
|
+
case 'ArrowDown':
|
|
588
|
+
event.preventDefault();
|
|
589
|
+
updateLocalViewBox((current) => ({
|
|
590
|
+
...current,
|
|
591
|
+
y: current.y + current.height * panStep,
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
}, [fittedViewBox, updateLocalViewBox, zoom]);
|
|
595
|
+
const alignToNorth = useCallback(() => {
|
|
596
|
+
const currentRotationDeg = rotationDegRef.current;
|
|
597
|
+
const nextRotationDeg = nearestEquivalentDegrees(0, currentRotationDeg);
|
|
598
|
+
if (!navigationVisible) {
|
|
599
|
+
const pose = latestNavigationPoseRef.current;
|
|
600
|
+
if (!pose)
|
|
601
|
+
return;
|
|
602
|
+
publishNavigationSyncPose({
|
|
603
|
+
source: '2d',
|
|
604
|
+
target: [...pose.target],
|
|
605
|
+
azimuth: cameraAzimuthFromFloorplanRotation(nextRotationDeg),
|
|
606
|
+
viewWidth: pose.viewWidth,
|
|
607
|
+
});
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const currentViewBox = viewBoxRef.current;
|
|
611
|
+
const currentSceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + currentRotationDeg - buildingRotationDeg;
|
|
612
|
+
const nextSceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg;
|
|
613
|
+
const currentCenter = {
|
|
614
|
+
x: currentViewBox.x + currentViewBox.width / 2,
|
|
615
|
+
y: currentViewBox.y + currentViewBox.height / 2,
|
|
616
|
+
};
|
|
617
|
+
const localCenter = rotateFloorplanPoint(currentCenter, -currentSceneRotationDeg);
|
|
618
|
+
const nextCenter = rotateFloorplanPoint(localCenter, nextSceneRotationDeg);
|
|
619
|
+
const nextViewBox = {
|
|
620
|
+
...currentViewBox,
|
|
621
|
+
x: nextCenter.x - currentViewBox.width / 2,
|
|
622
|
+
y: nextCenter.y - currentViewBox.height / 2,
|
|
623
|
+
};
|
|
624
|
+
commitPresentation(nextViewBox, nextRotationDeg);
|
|
625
|
+
publishNavigation(nextViewBox, nextRotationDeg);
|
|
626
|
+
}, [buildingRotationDeg, commitPresentation, navigationVisible, publishNavigation]);
|
|
627
|
+
const screenUnitsPerPixel = Math.max(viewBox.width / Math.max(viewportSize.width, 1), viewBox.height / Math.max(viewportSize.height, 1));
|
|
628
|
+
const compassControl = (_jsx(FloorplanCompassButton, { needleRef: compassNeedleRef, northRotationDeg: rotationDeg, onAlignNorth: alignToNorth }));
|
|
629
|
+
if (levels.length === 0) {
|
|
630
|
+
return (_jsx("div", { className: className, style: { display: 'grid', placeItems: 'center', background: '#f8fafc', color: '#64748b' }, children: i18n._({
|
|
631
|
+
id: 'viewer.floorplan.empty',
|
|
632
|
+
message: 'No floor plans are available for this scene.',
|
|
633
|
+
}) }));
|
|
634
|
+
}
|
|
635
|
+
return (_jsxs("div", { className: className, style: { position: 'relative', overflow: 'hidden', background: '#f8fafc' }, children: [synchronizeNavigation ? _jsx(FloorplanCameraSyncMount, {}) : null, _jsx("div", { "aria-keyshortcuts": "ArrowUp ArrowDown ArrowLeft ArrowRight + - 0 F", "aria-label": i18n._({
|
|
636
|
+
id: 'viewer.floorplan.viewLabel',
|
|
637
|
+
message: '{floor} 2D view',
|
|
638
|
+
values: {
|
|
639
|
+
floor: activeLevel
|
|
640
|
+
? localizedLevelName(activeLevel, i18n, locale)
|
|
641
|
+
: i18n._({ id: 'viewer.floorplan.floorPlan', message: 'Floor plan' }),
|
|
642
|
+
},
|
|
643
|
+
}), onKeyDown: onKeyDown, role: "application", ref: interactionRef, style: { width: '100%', height: '100%' },
|
|
644
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: The plan canvas supports keyboard pan, zoom, and fit controls.
|
|
645
|
+
tabIndex: 0, children: _jsxs("svg", { "aria-hidden": "true", "data-floorplan-preview": "", onContextMenu: (event) => event.preventDefault(), onPointerCancel: onPointerUp, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onWheel: onWheel, preserveAspectRatio: "xMidYMid meet", ref: svgRef, style: {
|
|
646
|
+
width: '100%',
|
|
647
|
+
height: '100%',
|
|
648
|
+
cursor: isRotating ? 'ew-resize' : isPanning ? 'grabbing' : 'grab',
|
|
649
|
+
touchAction: 'none',
|
|
650
|
+
}, viewBox: `${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`, children: [_jsx("defs", { children: _jsx("pattern", { height: "1", id: gridPatternId, patternUnits: "userSpaceOnUse", width: "1", children: _jsx("path", { d: "M 1 0 L 0 0 0 1", fill: "none", stroke: "#cbd5e1", strokeOpacity: "0.32", strokeWidth: "0.012" }) }) }), _jsx("rect", { fill: `url(#${gridPatternId})`, height: viewBox.height, ref: gridRef, width: viewBox.width, x: viewBox.x, y: viewBox.y }), _jsx("g", { pointerEvents: "none", ref: sceneRef, transform: FLOORPLAN_VIEW_ROTATION_DEG + rotationDeg - buildingRotationDeg === 0
|
|
651
|
+
? undefined
|
|
652
|
+
: `rotate(${FLOORPLAN_VIEW_ROTATION_DEG + rotationDeg - buildingRotationDeg})`, children: renderEntries.map((entry) => (_jsx("g", { "data-floorplan-node-id": entry.id, ref: (element) => {
|
|
653
|
+
if (element)
|
|
654
|
+
fitElementRefs.current.set(entry.id, element);
|
|
655
|
+
else
|
|
656
|
+
fitElementRefs.current.delete(entry.id);
|
|
657
|
+
}, children: _jsx(FloorplanGeometryRenderer, { geometry: entry.geometry, pointerEventsOverride: "none", screenUnitsPerPixel: screenUnitsPerPixel }) }, entry.id))) })] }) }), showLevelSelector && levels.length > 1 ? (_jsxs("label", { style: {
|
|
658
|
+
position: 'absolute',
|
|
659
|
+
bottom: 16,
|
|
660
|
+
left: 64,
|
|
661
|
+
display: 'grid',
|
|
662
|
+
gap: 4,
|
|
663
|
+
color: '#475569',
|
|
664
|
+
fontSize: 11,
|
|
665
|
+
fontWeight: 600,
|
|
666
|
+
}, children: [i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' }), _jsx("select", { "aria-label": i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' }), onChange: (event) => chooseLevel(event.target.value), style: {
|
|
667
|
+
border: '1px solid rgba(148,163,184,.55)',
|
|
668
|
+
borderRadius: 999,
|
|
669
|
+
background: 'rgba(255,255,255,.94)',
|
|
670
|
+
padding: '8px 30px 8px 12px',
|
|
671
|
+
color: '#0f172a',
|
|
672
|
+
boxShadow: '0 8px 24px rgba(15,23,42,.10)',
|
|
673
|
+
}, value: activeLevelId ?? '', children: levels.map((level) => (_jsx("option", { value: level.id, children: localizedLevelName(level, i18n, locale) }, level.id))) })] })) : null, showCompass
|
|
674
|
+
? compassHost
|
|
675
|
+
? createPortal(compassControl, compassHost)
|
|
676
|
+
: compassControl
|
|
677
|
+
: null, _jsxs("div", { style: {
|
|
678
|
+
position: 'absolute',
|
|
679
|
+
right: 16,
|
|
680
|
+
bottom: 16,
|
|
681
|
+
display: 'flex',
|
|
682
|
+
gap: 4,
|
|
683
|
+
border: '1px solid rgba(148,163,184,.45)',
|
|
684
|
+
borderRadius: 999,
|
|
685
|
+
background: 'rgba(255,255,255,.94)',
|
|
686
|
+
padding: 4,
|
|
687
|
+
boxShadow: '0 8px 24px rgba(15,23,42,.10)',
|
|
688
|
+
}, children: [_jsx("button", { "aria-label": i18n._({ id: 'viewer.floorplan.zoomOut', message: 'Zoom out' }), onClick: () => zoom(1.2), style: controlStyle, title: i18n._({ id: 'viewer.floorplan.zoomOut', message: 'Zoom out' }), type: "button", children: _jsx(Minus, { size: 16 }) }), _jsx("button", { "aria-label": i18n._({ id: 'viewer.floorplan.fit', message: 'Fit floor plan' }), onClick: () => updateLocalViewBox(fittedViewBox), style: controlStyle, title: i18n._({ id: 'viewer.floorplan.fit', message: 'Fit floor plan' }), type: "button", children: _jsx(Maximize2, { size: 15 }) }), _jsx("button", { "aria-label": i18n._({ id: 'viewer.floorplan.zoomIn', message: 'Zoom in' }), onClick: () => zoom(0.8), style: controlStyle, title: i18n._({ id: 'viewer.floorplan.zoomIn', message: 'Zoom in' }), type: "button", children: _jsx(Plus, { size: 16 }) })] })] }));
|
|
689
|
+
}
|
|
690
|
+
const controlStyle = {
|
|
691
|
+
display: 'grid',
|
|
692
|
+
width: 32,
|
|
693
|
+
height: 32,
|
|
694
|
+
placeItems: 'center',
|
|
695
|
+
border: 0,
|
|
696
|
+
borderRadius: 999,
|
|
697
|
+
background: 'transparent',
|
|
698
|
+
color: '#334155',
|
|
699
|
+
cursor: 'pointer',
|
|
700
|
+
};
|