@pascal-app/editor 1.0.1 → 1.0.3
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/package.json +10 -5
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floorplan-panel.tsx +210 -163
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +5 -9
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/use-handle-drag.ts +75 -6
- package/src/components/editor/index.tsx +24 -9
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +141 -7
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- package/src/components/editor/wall-move-side-handles.tsx +71 -21
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
- package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +67 -21
- package/src/components/systems/zone/zone-system.tsx +8 -4
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-strategies.ts +55 -43
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.ts +41 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +166 -73
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +23 -9
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- package/src/components/ui/controls/material-paint-panel.tsx +6 -67
- package/src/components/ui/controls/material-picker.tsx +5 -67
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/helpers/helper-manager.tsx +24 -6
- package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/parametric-inspector.tsx +3 -15
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +585 -369
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +1 -12
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.integration.test.tsx +197 -0
- package/src/hooks/use-auto-save.ts +21 -6
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +7 -0
- package/src/hooks/use-keyboard.ts +42 -29
- package/src/index.tsx +63 -3
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/floorplan/floorplan-export.test.ts +3 -1
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/fresh-planar-placement.ts +168 -36
- package/src/lib/glb-export.test.ts +59 -16
- package/src/lib/glb-export.ts +26 -13
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/interaction/scope.ts +9 -1
- package/src/lib/keyboard-pan.test.ts +62 -0
- package/src/lib/keyboard-pan.ts +89 -0
- package/src/lib/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/panel-rows.ts +75 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- package/src/lib/snapping-mode.test.ts +13 -2
- package/src/lib/snapping-mode.ts +11 -6
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/use-editor.tsx +10 -2
- package/src/store/use-floorplan-draft-preview.ts +10 -1
- package/src/store/use-interaction-scope.ts +155 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Pascal building editor component",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"test": "bun test src"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@pascal-app/core": "^1.0.
|
|
18
|
-
"@pascal-app/viewer": "^1.0.
|
|
17
|
+
"@pascal-app/core": "^1.0.3",
|
|
18
|
+
"@pascal-app/viewer": "^1.0.3",
|
|
19
19
|
"@react-three/drei": "^10",
|
|
20
20
|
"@react-three/fiber": "^9",
|
|
21
21
|
"next": ">=15",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"zustand": "^5.0.11"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@pascal-app/core": "^1.0.
|
|
66
|
-
"@pascal-app/viewer": "^1.0.
|
|
65
|
+
"@pascal-app/core": "^1.0.3",
|
|
66
|
+
"@pascal-app/viewer": "^1.0.3",
|
|
67
67
|
"@pascal/typescript-config": "*",
|
|
68
68
|
"@react-three/test-renderer": "^9.1.0",
|
|
69
69
|
"@types/blob-stream": "^0.1.33",
|
|
@@ -75,5 +75,10 @@
|
|
|
75
75
|
"@types/three": "^0.184.0",
|
|
76
76
|
"fast-xml-parser": "^5.4.2",
|
|
77
77
|
"typescript": "6.0.3"
|
|
78
|
+
},
|
|
79
|
+
"repository": {
|
|
80
|
+
"type": "git",
|
|
81
|
+
"url": "git+https://github.com/pascalorg/editor.git",
|
|
82
|
+
"directory": "packages/editor"
|
|
78
83
|
}
|
|
79
84
|
}
|
|
@@ -31,6 +31,17 @@ import {
|
|
|
31
31
|
withCameraPoseDistance,
|
|
32
32
|
} from '../../lib/camera-pose'
|
|
33
33
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
34
|
+
import {
|
|
35
|
+
acceptsKeyboardPan,
|
|
36
|
+
clearKeyboardPanKeys,
|
|
37
|
+
hasKeyboardPanInput,
|
|
38
|
+
isEditableKeyboardTarget,
|
|
39
|
+
isKeyboardPanKey,
|
|
40
|
+
type KeyboardPanState,
|
|
41
|
+
keyboardPanDirection,
|
|
42
|
+
keyboardPanSpeed,
|
|
43
|
+
setKeyboardPanKey,
|
|
44
|
+
} from '../../lib/keyboard-pan'
|
|
34
45
|
import { editorOwnsOneFingerDrag } from '../../lib/touch-gesture-priority'
|
|
35
46
|
import { publishCameraPose } from '../../store/camera-pose-store'
|
|
36
47
|
import useEditor from '../../store/use-editor'
|
|
@@ -51,11 +62,11 @@ const tempTarget = new Vector3()
|
|
|
51
62
|
const transitionFreezePosition = new Vector3()
|
|
52
63
|
const transitionFreezeTarget = new Vector3()
|
|
53
64
|
const keyboardPanSpherical = new Spherical()
|
|
65
|
+
// In 2D-only view the canvas is paused, so the floor plan drives WASD, orbit and
|
|
66
|
+
// top view itself (`floorplan-panel.tsx`) and the camera stands down.
|
|
67
|
+
const planOwnsNavigation = () => useEditor.getState().viewMode === '2d'
|
|
54
68
|
const DEFAULT_MAX_POLAR_ANGLE = Math.PI / 2 - 0.1
|
|
55
69
|
const DEBUG_MAX_POLAR_ANGLE = Math.PI - 0.05
|
|
56
|
-
const KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND = 0.65
|
|
57
|
-
const KEYBOARD_PAN_MIN_SPEED = 2
|
|
58
|
-
const KEYBOARD_PAN_MAX_SPEED = 55
|
|
59
70
|
type CameraMode = ReturnType<typeof useViewer.getState>['cameraMode']
|
|
60
71
|
type CameraPoseSnapshot = {
|
|
61
72
|
mode: CameraMode
|
|
@@ -114,54 +125,6 @@ function freezeCameraControlTransition(control: CameraControlsImpl) {
|
|
|
114
125
|
)
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
function isEditableKeyboardTarget(target: EventTarget | null) {
|
|
118
|
-
return (
|
|
119
|
-
target instanceof HTMLInputElement ||
|
|
120
|
-
target instanceof HTMLTextAreaElement ||
|
|
121
|
-
target instanceof HTMLSelectElement ||
|
|
122
|
-
(target instanceof HTMLElement && target.isContentEditable)
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
type KeyboardPanState = {
|
|
127
|
-
forward: boolean
|
|
128
|
-
backward: boolean
|
|
129
|
-
left: boolean
|
|
130
|
-
right: boolean
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function setKeyboardPanKey(state: KeyboardPanState, code: string, pressed: boolean): boolean {
|
|
134
|
-
if (code === 'KeyW') {
|
|
135
|
-
const changed = state.forward !== pressed
|
|
136
|
-
state.forward = pressed
|
|
137
|
-
return changed
|
|
138
|
-
}
|
|
139
|
-
if (code === 'KeyS') {
|
|
140
|
-
const changed = state.backward !== pressed
|
|
141
|
-
state.backward = pressed
|
|
142
|
-
return changed
|
|
143
|
-
}
|
|
144
|
-
if (code === 'KeyA') {
|
|
145
|
-
const changed = state.left !== pressed
|
|
146
|
-
state.left = pressed
|
|
147
|
-
return changed
|
|
148
|
-
}
|
|
149
|
-
if (code === 'KeyD') {
|
|
150
|
-
const changed = state.right !== pressed
|
|
151
|
-
state.right = pressed
|
|
152
|
-
return changed
|
|
153
|
-
}
|
|
154
|
-
return false
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function isKeyboardPanKey(code: string): boolean {
|
|
158
|
-
return code === 'KeyW' || code === 'KeyA' || code === 'KeyS' || code === 'KeyD'
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function hasKeyboardPanInput(state: KeyboardPanState): boolean {
|
|
162
|
-
return state.forward || state.backward || state.left || state.right
|
|
163
|
-
}
|
|
164
|
-
|
|
165
128
|
type CameraViewportSize = {
|
|
166
129
|
width: number
|
|
167
130
|
height: number
|
|
@@ -667,19 +630,14 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
667
630
|
}
|
|
668
631
|
}
|
|
669
632
|
|
|
670
|
-
const
|
|
671
|
-
const horizontal = (panKeys.right ? 1 : 0) - (panKeys.left ? 1 : 0)
|
|
672
|
-
const vertical = (panKeys.forward ? 1 : 0) - (panKeys.backward ? 1 : 0)
|
|
633
|
+
const { horizontal, vertical } = keyboardPanDirection(keyboardPanKeys.current)
|
|
673
634
|
if (horizontal === 0 && vertical === 0) return
|
|
674
635
|
|
|
675
636
|
const control = controls.current
|
|
676
637
|
|
|
677
638
|
control.getSpherical(keyboardPanSpherical, false)
|
|
678
639
|
const viewWidth = getCameraViewWidth(camera, keyboardPanSpherical.radius, viewportSize)
|
|
679
|
-
const speed =
|
|
680
|
-
Math.max(viewWidth * KEYBOARD_PAN_VIEW_WIDTH_PER_SECOND, KEYBOARD_PAN_MIN_SPEED),
|
|
681
|
-
KEYBOARD_PAN_MAX_SPEED,
|
|
682
|
-
)
|
|
640
|
+
const speed = keyboardPanSpeed(viewWidth)
|
|
683
641
|
const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
|
|
684
642
|
|
|
685
643
|
if (horizontal !== 0) control.truck(horizontal * step, 0, true)
|
|
@@ -766,13 +724,6 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
766
724
|
let panPointerId: number | null = null
|
|
767
725
|
let panPointerButton: number | null = null
|
|
768
726
|
|
|
769
|
-
const clearKeyboardPanKeys = () => {
|
|
770
|
-
keyboardPanKeys.current.forward = false
|
|
771
|
-
keyboardPanKeys.current.backward = false
|
|
772
|
-
keyboardPanKeys.current.left = false
|
|
773
|
-
keyboardPanKeys.current.right = false
|
|
774
|
-
}
|
|
775
|
-
|
|
776
727
|
const setNavigationCursor = (cursor: 'grab' | 'grabbing') => {
|
|
777
728
|
document.body.style.cursor = cursor
|
|
778
729
|
gl.domElement.style.cursor = cursor
|
|
@@ -835,10 +786,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
835
786
|
|
|
836
787
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
837
788
|
if (isKeyboardPanKey(event.code)) {
|
|
838
|
-
if (
|
|
839
|
-
!(event.metaKey || event.ctrlKey || event.altKey) &&
|
|
840
|
-
!isEditableKeyboardTarget(event.target)
|
|
841
|
-
) {
|
|
789
|
+
if (acceptsKeyboardPan(event) && !planOwnsNavigation()) {
|
|
842
790
|
const changed = setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
|
|
843
791
|
if (changed) beginLocalCameraInteraction()
|
|
844
792
|
event.preventDefault()
|
|
@@ -930,7 +878,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
930
878
|
|
|
931
879
|
const onBlur = () => {
|
|
932
880
|
keyState.space = false
|
|
933
|
-
clearKeyboardPanKeys()
|
|
881
|
+
clearKeyboardPanKeys(keyboardPanKeys.current)
|
|
934
882
|
panPointerId = null
|
|
935
883
|
panPointerButton = null
|
|
936
884
|
clearNavigationCursor()
|
|
@@ -955,7 +903,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
955
903
|
window.removeEventListener('pointercancel', onPointerUp, true)
|
|
956
904
|
window.removeEventListener('blur', onBlur)
|
|
957
905
|
gl.domElement.removeEventListener('wheel', onWheel, true)
|
|
958
|
-
clearKeyboardPanKeys()
|
|
906
|
+
clearKeyboardPanKeys(keyboardPanKeys.current)
|
|
959
907
|
clearNavigationCursor()
|
|
960
908
|
cameraDraggingLifecycle.end()
|
|
961
909
|
}
|
|
@@ -1196,7 +1144,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1196
1144
|
}
|
|
1197
1145
|
|
|
1198
1146
|
const handleTopView = () => {
|
|
1199
|
-
if (isFirstPersonMode || !controls.current) return
|
|
1147
|
+
if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
|
|
1200
1148
|
|
|
1201
1149
|
const currentPolarAngle = controls.current.polarAngle
|
|
1202
1150
|
|
|
@@ -1208,7 +1156,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1208
1156
|
}
|
|
1209
1157
|
|
|
1210
1158
|
const handleOrbitCW = () => {
|
|
1211
|
-
if (isFirstPersonMode || !controls.current) return
|
|
1159
|
+
if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
|
|
1212
1160
|
|
|
1213
1161
|
const currentAzimuth = controls.current.azimuthAngle
|
|
1214
1162
|
const currentPolar = controls.current.polarAngle
|
|
@@ -1220,7 +1168,7 @@ export const CustomCameraControls = ({ paused = false }: { paused?: boolean }) =
|
|
|
1220
1168
|
}
|
|
1221
1169
|
|
|
1222
1170
|
const handleOrbitCCW = () => {
|
|
1223
|
-
if (isFirstPersonMode || !controls.current) return
|
|
1171
|
+
if (isFirstPersonMode || !controls.current || planOwnsNavigation()) return
|
|
1224
1172
|
|
|
1225
1173
|
const currentAzimuth = controls.current.azimuthAngle
|
|
1226
1174
|
const currentPolar = controls.current.polarAngle
|
|
@@ -236,6 +236,7 @@ export function EditorLayoutMobile({
|
|
|
236
236
|
<div className="relative h-full w-full overflow-hidden">{viewerContent}</div>
|
|
237
237
|
{overlays && (
|
|
238
238
|
<div
|
|
239
|
+
data-viewer-bounds
|
|
239
240
|
className="pointer-events-none absolute inset-0 z-30"
|
|
240
241
|
style={{ transform: 'translateZ(0)' }}
|
|
241
242
|
>
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
type WindowNode,
|
|
56
56
|
WindowNode as WindowNodeSchema,
|
|
57
57
|
wallClosesRoom,
|
|
58
|
+
wallRectangleCorners,
|
|
58
59
|
ZoneNode as ZoneNodeSchema,
|
|
59
60
|
type ZoneNode as ZoneNodeType,
|
|
60
61
|
} from '@pascal-app/core'
|
|
@@ -94,6 +95,15 @@ import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-
|
|
|
94
95
|
import type { EditorGridEvent } from '../../lib/grid-event-presentation'
|
|
95
96
|
import { groundHeightAt } from '../../lib/ground-surface'
|
|
96
97
|
import { guideEmitter } from '../../lib/guide-events'
|
|
98
|
+
import {
|
|
99
|
+
acceptsKeyboardPan,
|
|
100
|
+
clearKeyboardPanKeys,
|
|
101
|
+
isKeyboardPanKey,
|
|
102
|
+
type KeyboardPanState,
|
|
103
|
+
keyboardPanDirection,
|
|
104
|
+
keyboardPanSpeed,
|
|
105
|
+
setKeyboardPanKey,
|
|
106
|
+
} from '../../lib/keyboard-pan'
|
|
97
107
|
import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
|
|
98
108
|
import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements'
|
|
99
109
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
@@ -137,6 +147,7 @@ import {
|
|
|
137
147
|
import { FloorplanSnapBeaconLayer } from '../editor-2d/floorplan-snap-beacon-layer'
|
|
138
148
|
import { FloorplanWallMoveGhostLayer } from '../editor-2d/floorplan-wall-move-ghost-layer'
|
|
139
149
|
import { FloorplanDraftLayer } from '../editor-2d/renderers/floorplan-draft-layer'
|
|
150
|
+
import { FloorplanDraftWallMeasurement } from '../editor-2d/renderers/floorplan-draft-wall-measurement'
|
|
140
151
|
import { FloorplanGeometryRenderer } from '../editor-2d/renderers/floorplan-geometry-renderer'
|
|
141
152
|
import { FloorplanMarqueeLayer } from '../editor-2d/renderers/floorplan-marquee-layer'
|
|
142
153
|
import { FloorplanPlacementPreviewLayer } from '../editor-2d/renderers/floorplan-placement-preview-layer'
|
|
@@ -2320,168 +2331,6 @@ function buildDraftWall(levelId: string, start: WallPlanPoint, end: WallPlanPoin
|
|
|
2320
2331
|
}
|
|
2321
2332
|
}
|
|
2322
2333
|
|
|
2323
|
-
type DraftWallMeasurement = {
|
|
2324
|
-
lengthLabel: string
|
|
2325
|
-
midpoint: WallPlanPoint
|
|
2326
|
-
direction: WallPlanPoint
|
|
2327
|
-
angleLabels: {
|
|
2328
|
-
id: string
|
|
2329
|
-
label: string
|
|
2330
|
-
center: WallPlanPoint
|
|
2331
|
-
radius: number
|
|
2332
|
-
startAngle: number
|
|
2333
|
-
endAngle: number
|
|
2334
|
-
midAngle: number
|
|
2335
|
-
}[]
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
function FloorplanDraftWallMeasurement({
|
|
2339
|
-
measurement,
|
|
2340
|
-
measurementStroke,
|
|
2341
|
-
labelBackground,
|
|
2342
|
-
labelText,
|
|
2343
|
-
sceneRotationDeg,
|
|
2344
|
-
unitsPerPixel,
|
|
2345
|
-
}: {
|
|
2346
|
-
measurement: DraftWallMeasurement
|
|
2347
|
-
measurementStroke: string
|
|
2348
|
-
labelBackground: string
|
|
2349
|
-
labelText: string
|
|
2350
|
-
sceneRotationDeg: number
|
|
2351
|
-
unitsPerPixel: number
|
|
2352
|
-
}) {
|
|
2353
|
-
const stroke = measurementStroke
|
|
2354
|
-
const labelBg = labelBackground
|
|
2355
|
-
|
|
2356
|
-
const upx = unitsPerPixel
|
|
2357
|
-
const fontSize = Math.max(upx * 10, 0.08)
|
|
2358
|
-
const padX = upx * 6
|
|
2359
|
-
const padY = upx * 3
|
|
2360
|
-
|
|
2361
|
-
// Length plate: rotates to follow the wall direction, but flips 180°
|
|
2362
|
-
// when its on-screen orientation would read upside-down (same trick as
|
|
2363
|
-
// `floorplan-registry-layer.tsx` for dimension labels).
|
|
2364
|
-
const wallAngleDeg =
|
|
2365
|
-
(Math.atan2(measurement.direction[1], measurement.direction[0]) * 180) / Math.PI
|
|
2366
|
-
let labelAngleDeg = wallAngleDeg
|
|
2367
|
-
let screenDeg = wallAngleDeg + sceneRotationDeg
|
|
2368
|
-
screenDeg = ((((screenDeg + 180) % 360) + 360) % 360) - 180
|
|
2369
|
-
if (screenDeg > 90) labelAngleDeg -= 180
|
|
2370
|
-
else if (screenDeg <= -90) labelAngleDeg += 180
|
|
2371
|
-
|
|
2372
|
-
// Push the plate perpendicular to the wall so the dashed footprint
|
|
2373
|
-
// stays visible underneath.
|
|
2374
|
-
const perpX = -measurement.direction[1]
|
|
2375
|
-
const perpY = measurement.direction[0]
|
|
2376
|
-
const offset = upx * 18
|
|
2377
|
-
const cx = measurement.midpoint[0] + perpX * offset
|
|
2378
|
-
const cy = measurement.midpoint[1] + perpY * offset
|
|
2379
|
-
|
|
2380
|
-
const lengthTextWidth = measurement.lengthLabel.length * upx * 6.2
|
|
2381
|
-
const lengthPlateW = lengthTextWidth + padX * 2
|
|
2382
|
-
const lengthPlateH = fontSize + padY * 2
|
|
2383
|
-
|
|
2384
|
-
const arcSampleCount = 32
|
|
2385
|
-
|
|
2386
|
-
return (
|
|
2387
|
-
<g pointerEvents="none">
|
|
2388
|
-
<g transform={`translate(${cx} ${cy}) rotate(${labelAngleDeg})`}>
|
|
2389
|
-
<rect
|
|
2390
|
-
fill={labelBg}
|
|
2391
|
-
height={lengthPlateH}
|
|
2392
|
-
opacity={0.92}
|
|
2393
|
-
rx={upx * 3}
|
|
2394
|
-
ry={upx * 3}
|
|
2395
|
-
stroke={stroke}
|
|
2396
|
-
strokeWidth={upx * 0.5}
|
|
2397
|
-
vectorEffect="non-scaling-stroke"
|
|
2398
|
-
width={lengthPlateW}
|
|
2399
|
-
x={-lengthPlateW / 2}
|
|
2400
|
-
y={-lengthPlateH / 2}
|
|
2401
|
-
/>
|
|
2402
|
-
<text
|
|
2403
|
-
dominantBaseline="middle"
|
|
2404
|
-
fill={labelText}
|
|
2405
|
-
fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
2406
|
-
fontSize={fontSize}
|
|
2407
|
-
fontWeight={600}
|
|
2408
|
-
textAnchor="middle"
|
|
2409
|
-
x={0}
|
|
2410
|
-
y={0}
|
|
2411
|
-
>
|
|
2412
|
-
{measurement.lengthLabel}
|
|
2413
|
-
</text>
|
|
2414
|
-
</g>
|
|
2415
|
-
|
|
2416
|
-
{measurement.angleLabels.map((arc) => {
|
|
2417
|
-
// Sample the arc as a polyline — avoids the SVG arc command's
|
|
2418
|
-
// sweep-flag direction quirks across negative/positive sweeps.
|
|
2419
|
-
const points: string[] = []
|
|
2420
|
-
for (let i = 0; i <= arcSampleCount; i += 1) {
|
|
2421
|
-
const t = i / arcSampleCount
|
|
2422
|
-
const a = arc.startAngle + (arc.endAngle - arc.startAngle) * t
|
|
2423
|
-
const px = arc.center[0] + Math.cos(a) * arc.radius
|
|
2424
|
-
const py = arc.center[1] + Math.sin(a) * arc.radius
|
|
2425
|
-
points.push(`${px},${py}`)
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
const aFontSize = Math.max(upx * 9, 0.075)
|
|
2429
|
-
const aPadX = upx * 5
|
|
2430
|
-
const aPadY = upx * 2.5
|
|
2431
|
-
const aTextWidth = arc.label.length * upx * 6.2
|
|
2432
|
-
const aPlateW = aTextWidth + aPadX * 2
|
|
2433
|
-
const aPlateH = aFontSize + aPadY * 2
|
|
2434
|
-
|
|
2435
|
-
const labelDist = arc.radius + upx * 16
|
|
2436
|
-
const lx = arc.center[0] + Math.cos(arc.midAngle) * labelDist
|
|
2437
|
-
const ly = arc.center[1] + Math.sin(arc.midAngle) * labelDist
|
|
2438
|
-
|
|
2439
|
-
return (
|
|
2440
|
-
<g key={`draft-angle-${arc.id}`}>
|
|
2441
|
-
<polyline
|
|
2442
|
-
fill="none"
|
|
2443
|
-
points={points.join(' ')}
|
|
2444
|
-
stroke={stroke}
|
|
2445
|
-
strokeLinecap="round"
|
|
2446
|
-
strokeLinejoin="round"
|
|
2447
|
-
strokeOpacity={0.95}
|
|
2448
|
-
strokeWidth={upx * 1.2}
|
|
2449
|
-
vectorEffect="non-scaling-stroke"
|
|
2450
|
-
/>
|
|
2451
|
-
<g transform={`translate(${lx} ${ly})`}>
|
|
2452
|
-
<rect
|
|
2453
|
-
fill={labelBg}
|
|
2454
|
-
height={aPlateH}
|
|
2455
|
-
opacity={0.92}
|
|
2456
|
-
rx={upx * 3}
|
|
2457
|
-
ry={upx * 3}
|
|
2458
|
-
stroke={stroke}
|
|
2459
|
-
strokeWidth={upx * 0.5}
|
|
2460
|
-
vectorEffect="non-scaling-stroke"
|
|
2461
|
-
width={aPlateW}
|
|
2462
|
-
x={-aPlateW / 2}
|
|
2463
|
-
y={-aPlateH / 2}
|
|
2464
|
-
/>
|
|
2465
|
-
<text
|
|
2466
|
-
dominantBaseline="middle"
|
|
2467
|
-
fill={labelText}
|
|
2468
|
-
fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
2469
|
-
fontSize={aFontSize}
|
|
2470
|
-
fontWeight={600}
|
|
2471
|
-
textAnchor="middle"
|
|
2472
|
-
x={0}
|
|
2473
|
-
y={0}
|
|
2474
|
-
>
|
|
2475
|
-
{arc.label}
|
|
2476
|
-
</text>
|
|
2477
|
-
</g>
|
|
2478
|
-
</g>
|
|
2479
|
-
)
|
|
2480
|
-
})}
|
|
2481
|
-
</g>
|
|
2482
|
-
)
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
2334
|
function pointsEqual(a: WallPlanPoint, b: WallPlanPoint): boolean {
|
|
2486
2335
|
return a[0] === b[0] && a[1] === b[1]
|
|
2487
2336
|
}
|
|
@@ -4570,7 +4419,7 @@ function FloorplanDraftCursorLayer({
|
|
|
4570
4419
|
)}
|
|
4571
4420
|
|
|
4572
4421
|
{cursorPoint && (
|
|
4573
|
-
<g>
|
|
4422
|
+
<g pointerEvents="none">
|
|
4574
4423
|
<circle
|
|
4575
4424
|
cx={toSvgX(cursorPoint[0])}
|
|
4576
4425
|
cy={toSvgY(cursorPoint[1])}
|
|
@@ -4680,6 +4529,12 @@ function FloorplanLinearDraftLayer({
|
|
|
4680
4529
|
const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
|
|
4681
4530
|
const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
|
|
4682
4531
|
const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn)
|
|
4532
|
+
const wallRectangleDraftStart = useFloorplanDraftPreview((s) => s.wallRectangleDraftStart)
|
|
4533
|
+
// The live cursor is the rectangle's opposite corner; select it only while a
|
|
4534
|
+
// rectangle draft is open so idle moves don't re-render this layer.
|
|
4535
|
+
const wallRectangleDraftEnd = useFloorplanDraftPreview((s) =>
|
|
4536
|
+
s.wallRectangleDraftStart ? s.cursorPoint : null,
|
|
4537
|
+
)
|
|
4683
4538
|
|
|
4684
4539
|
const draftPolygon = useMemo(() => {
|
|
4685
4540
|
if (
|
|
@@ -4697,6 +4552,53 @@ function FloorplanLinearDraftLayer({
|
|
|
4697
4552
|
return getWallPlanFootprint(draftWall, EMPTY_WALL_MITER_DATA)
|
|
4698
4553
|
}, [levelId, wallDraftStart, wallDraftEnd])
|
|
4699
4554
|
|
|
4555
|
+
// Rectangle mode drafts four walls at once; they read exactly like a line
|
|
4556
|
+
// draft — same mitered footprints, measurement plates and axis guides.
|
|
4557
|
+
const rectangleDraft = useMemo(() => {
|
|
4558
|
+
if (!(levelId && isWallBuildActive && wallRectangleDraftStart && wallRectangleDraftEnd)) {
|
|
4559
|
+
return null
|
|
4560
|
+
}
|
|
4561
|
+
const corners = wallRectangleCorners(wallRectangleDraftStart, wallRectangleDraftEnd)
|
|
4562
|
+
if (corners.length !== 4) return null
|
|
4563
|
+
const draftWalls = corners.map((start, index) =>
|
|
4564
|
+
getSharedFloorplanWall({
|
|
4565
|
+
...buildDraftWall(levelId, start, corners[(index + 1) % 4]!),
|
|
4566
|
+
id: `wall_draft_${index}` as WallNode['id'],
|
|
4567
|
+
}),
|
|
4568
|
+
)
|
|
4569
|
+
const miterData = calculateLevelMiters(draftWalls)
|
|
4570
|
+
const polygons = draftWalls.map((wall) =>
|
|
4571
|
+
formatPolygonPoints(getWallPlanFootprint(wall, miterData)),
|
|
4572
|
+
)
|
|
4573
|
+
// Measure the two sides meeting at the cursor corner. Corners wind
|
|
4574
|
+
// counter-clockwise, so each side is measured end → start to push its
|
|
4575
|
+
// plate outside the rectangle.
|
|
4576
|
+
const cursorIndex = corners.findIndex(
|
|
4577
|
+
([x, z]) => x === wallRectangleDraftEnd[0] && z === wallRectangleDraftEnd[1],
|
|
4578
|
+
)
|
|
4579
|
+
const measurements = [cursorIndex - 1, cursorIndex].map((side) => {
|
|
4580
|
+
const from = corners[(side + 5) % 4]!
|
|
4581
|
+
const to = corners[(side + 4) % 4]!
|
|
4582
|
+
const dx = to[0] - from[0]
|
|
4583
|
+
const dy = to[1] - from[1]
|
|
4584
|
+
const length = Math.hypot(dx, dy)
|
|
4585
|
+
return {
|
|
4586
|
+
lengthLabel: formatMeasurement(length, unit, null, metricNotation),
|
|
4587
|
+
midpoint: [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2] as WallPlanPoint,
|
|
4588
|
+
direction: [dx / length, dy / length] as WallPlanPoint,
|
|
4589
|
+
angleLabels: [],
|
|
4590
|
+
}
|
|
4591
|
+
})
|
|
4592
|
+
return { polygons, measurements }
|
|
4593
|
+
}, [
|
|
4594
|
+
isWallBuildActive,
|
|
4595
|
+
levelId,
|
|
4596
|
+
metricNotation,
|
|
4597
|
+
unit,
|
|
4598
|
+
wallRectangleDraftEnd,
|
|
4599
|
+
wallRectangleDraftStart,
|
|
4600
|
+
])
|
|
4601
|
+
|
|
4700
4602
|
const draftPolygonPoints = useMemo(() => {
|
|
4701
4603
|
if (isRoofBuildActive && roofDraftStart && roofDraftEnd) {
|
|
4702
4604
|
const minX = Math.min(roofDraftStart[0], roofDraftEnd[0])
|
|
@@ -4864,6 +4766,10 @@ function FloorplanLinearDraftLayer({
|
|
|
4864
4766
|
if (isWallBuildActive && wallDraftStart && wallDraftEnd) {
|
|
4865
4767
|
pushDraft(wallDraftStart, wallDraftEnd)
|
|
4866
4768
|
}
|
|
4769
|
+
if (isWallBuildActive && wallRectangleDraftStart && wallRectangleDraftEnd) {
|
|
4770
|
+
pushCross(wallRectangleDraftStart)
|
|
4771
|
+
pushCross(wallRectangleDraftEnd)
|
|
4772
|
+
}
|
|
4867
4773
|
if (isFenceBuildActive && fenceDraftStart && fenceDraftEnd) {
|
|
4868
4774
|
pushDraft(fenceDraftStart, fenceDraftEnd)
|
|
4869
4775
|
}
|
|
@@ -4875,6 +4781,8 @@ function FloorplanLinearDraftLayer({
|
|
|
4875
4781
|
fenceDraftStart,
|
|
4876
4782
|
wallDraftEnd,
|
|
4877
4783
|
wallDraftStart,
|
|
4784
|
+
wallRectangleDraftEnd,
|
|
4785
|
+
wallRectangleDraftStart,
|
|
4878
4786
|
])
|
|
4879
4787
|
|
|
4880
4788
|
return (
|
|
@@ -4932,6 +4840,34 @@ function FloorplanLinearDraftLayer({
|
|
|
4932
4840
|
unitsPerPixel={unitsPerPixel}
|
|
4933
4841
|
/>
|
|
4934
4842
|
)}
|
|
4843
|
+
|
|
4844
|
+
{rectangleDraft?.polygons.map((points, index) => (
|
|
4845
|
+
<FloorplanDraftLayer
|
|
4846
|
+
anchorFill={draftStroke}
|
|
4847
|
+
draftAnchorPoints={EMPTY_DRAFT_ANCHOR_POINTS}
|
|
4848
|
+
draftFill={draftFill}
|
|
4849
|
+
draftPolygonPoints={points}
|
|
4850
|
+
draftStroke={draftStroke}
|
|
4851
|
+
key={index}
|
|
4852
|
+
linearDraftSegment={null}
|
|
4853
|
+
polygonDraftClosingSegment={null}
|
|
4854
|
+
polygonDraftPolygonPoints={null}
|
|
4855
|
+
polygonDraftPolylinePoints={null}
|
|
4856
|
+
unitsPerPixel={unitsPerPixel}
|
|
4857
|
+
/>
|
|
4858
|
+
))}
|
|
4859
|
+
|
|
4860
|
+
{rectangleDraft?.measurements.map((measurement, index) => (
|
|
4861
|
+
<FloorplanDraftWallMeasurement
|
|
4862
|
+
key={index}
|
|
4863
|
+
labelBackground={isDark ? '#0f172a' : '#ffffff'}
|
|
4864
|
+
labelText={isDark ? '#e2e8f0' : '#171717'}
|
|
4865
|
+
measurement={measurement}
|
|
4866
|
+
measurementStroke={measurementStroke}
|
|
4867
|
+
sceneRotationDeg={sceneRotationDeg}
|
|
4868
|
+
unitsPerPixel={unitsPerPixel}
|
|
4869
|
+
/>
|
|
4870
|
+
))}
|
|
4935
4871
|
</>
|
|
4936
4872
|
)
|
|
4937
4873
|
}
|
|
@@ -5036,6 +4972,7 @@ export function FloorplanPanel({
|
|
|
5036
4972
|
// the user closes and re-opens the 2D editor instead of restoring the
|
|
5037
4973
|
// stale viewport from before they closed it.
|
|
5038
4974
|
const isFloorplanOpen = useEditor((state) => state.isFloorplanOpen)
|
|
4975
|
+
const viewMode = useEditor((state) => state.viewMode)
|
|
5039
4976
|
// Mirror for callbacks that fire outside React's render (the per-frame
|
|
5040
4977
|
// navigation-pose subscriber): when the 2D panel is hidden (`display:none` in
|
|
5041
4978
|
// 3D mode) it must NOT re-render on every camera-zoom frame.
|
|
@@ -7784,6 +7721,116 @@ export function FloorplanPanel({
|
|
|
7784
7721
|
[publishFloorplanNavigationPose],
|
|
7785
7722
|
)
|
|
7786
7723
|
|
|
7724
|
+
// WASD and the orbit buttons in 2D-only view. The 3D canvas is paused there
|
|
7725
|
+
// (`renderPaused`), so the plan drives them itself — same keys and speed as
|
|
7726
|
+
// the camera — and hands the pose to 3D when the move ends.
|
|
7727
|
+
const keyboardPanKeysRef = useRef<KeyboardPanState>({
|
|
7728
|
+
forward: false,
|
|
7729
|
+
backward: false,
|
|
7730
|
+
left: false,
|
|
7731
|
+
right: false,
|
|
7732
|
+
})
|
|
7733
|
+
useEffect(() => {
|
|
7734
|
+
if (viewMode !== '2d') return
|
|
7735
|
+
const keys = keyboardPanKeysRef.current
|
|
7736
|
+
let frame: number | null = null
|
|
7737
|
+
let lastTime: number | null = null
|
|
7738
|
+
const sceneRotationDeg = (userRotationDeg: number) =>
|
|
7739
|
+
FLOORPLAN_VIEW_ROTATION_DEG + userRotationDeg - buildingRotationDeg
|
|
7740
|
+
const step = (time: number) => {
|
|
7741
|
+
const { horizontal, vertical } = keyboardPanDirection(keys)
|
|
7742
|
+
const viewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
7743
|
+
if ((horizontal === 0 && vertical === 0) || !viewport) {
|
|
7744
|
+
frame = null
|
|
7745
|
+
lastTime = null
|
|
7746
|
+
commitFloorplanPan()
|
|
7747
|
+
return
|
|
7748
|
+
}
|
|
7749
|
+
const elapsed = lastTime === null ? 0 : Math.min((time - lastTime) / 1000, 0.05)
|
|
7750
|
+
lastTime = time
|
|
7751
|
+
const distance =
|
|
7752
|
+
(keyboardPanSpeed(viewport.width) * elapsed) / Math.hypot(horizontal, vertical)
|
|
7753
|
+
const next = {
|
|
7754
|
+
centerX: viewport.centerX + horizontal * distance,
|
|
7755
|
+
centerY: viewport.centerY - vertical * distance,
|
|
7756
|
+
width: viewport.width,
|
|
7757
|
+
}
|
|
7758
|
+
const userRotationDeg = latestFloorplanUserRotationDegRef.current
|
|
7759
|
+
floorplanViewportInteractionInProgressRef.current = true
|
|
7760
|
+
applyFloorplanViewportImperatively(next)
|
|
7761
|
+
floorplanPanPoseRef.current = {
|
|
7762
|
+
localCenter: rotateSvgPoint(
|
|
7763
|
+
{ x: next.centerX, y: next.centerY },
|
|
7764
|
+
-sceneRotationDeg(userRotationDeg),
|
|
7765
|
+
),
|
|
7766
|
+
userRotationDeg,
|
|
7767
|
+
viewWidth: next.width,
|
|
7768
|
+
}
|
|
7769
|
+
frame = requestAnimationFrame(step)
|
|
7770
|
+
}
|
|
7771
|
+
const start = () => {
|
|
7772
|
+
if (frame === null) frame = requestAnimationFrame(step)
|
|
7773
|
+
}
|
|
7774
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
7775
|
+
if (!(isKeyboardPanKey(event.code) && acceptsKeyboardPan(event))) return
|
|
7776
|
+
if (setKeyboardPanKey(keys, event.code, true)) start()
|
|
7777
|
+
event.preventDefault()
|
|
7778
|
+
event.stopPropagation()
|
|
7779
|
+
}
|
|
7780
|
+
const onKeyUp = (event: KeyboardEvent) => {
|
|
7781
|
+
if (isKeyboardPanKey(event.code) && setKeyboardPanKey(keys, event.code, false)) {
|
|
7782
|
+
event.preventDefault()
|
|
7783
|
+
event.stopPropagation()
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
const onBlur = () => clearKeyboardPanKeys(keys)
|
|
7787
|
+
// Same rule as the 3D buttons: snap to the nearest quarter turn, then turn one more.
|
|
7788
|
+
const orbit = (clockwise: boolean) => {
|
|
7789
|
+
const viewport = latestViewportRef.current ?? latestFittedViewportRef.current
|
|
7790
|
+
if (!viewport) return
|
|
7791
|
+
const userRotationDeg = latestFloorplanUserRotationDegRef.current
|
|
7792
|
+
const quarter = Math.PI / 2
|
|
7793
|
+
const azimuth =
|
|
7794
|
+
Math.round(cameraAzimuthFromFloorplanRotation(userRotationDeg) / quarter) * quarter +
|
|
7795
|
+
(clockwise ? -quarter : quarter)
|
|
7796
|
+
const nextRotationDeg = floorplanRotationFromCameraAzimuth(azimuth, userRotationDeg)
|
|
7797
|
+
const localCenter = rotateSvgPoint(
|
|
7798
|
+
{ x: viewport.centerX, y: viewport.centerY },
|
|
7799
|
+
-sceneRotationDeg(userRotationDeg),
|
|
7800
|
+
)
|
|
7801
|
+
smoothFloorplanNavigationView(localCenter, nextRotationDeg, viewport.width)
|
|
7802
|
+
publishFloorplanNavigationPose(localCenter, nextRotationDeg, viewport.width)
|
|
7803
|
+
}
|
|
7804
|
+
const orbitClockwise = () => orbit(true)
|
|
7805
|
+
const orbitCounterClockwise = () => orbit(false)
|
|
7806
|
+
document.addEventListener('keydown', onKeyDown)
|
|
7807
|
+
document.addEventListener('keyup', onKeyUp)
|
|
7808
|
+
window.addEventListener('blur', onBlur)
|
|
7809
|
+
emitter.on('camera-controls:orbit-cw', orbitClockwise)
|
|
7810
|
+
emitter.on('camera-controls:orbit-ccw', orbitCounterClockwise)
|
|
7811
|
+
return () => {
|
|
7812
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
7813
|
+
document.removeEventListener('keyup', onKeyUp)
|
|
7814
|
+
window.removeEventListener('blur', onBlur)
|
|
7815
|
+
emitter.off('camera-controls:orbit-cw', orbitClockwise)
|
|
7816
|
+
emitter.off('camera-controls:orbit-ccw', orbitCounterClockwise)
|
|
7817
|
+
// A key released while 3D owns the keys would otherwise still read as
|
|
7818
|
+
// held when the plan takes them back; the camera clears its own the same way.
|
|
7819
|
+
clearKeyboardPanKeys(keys)
|
|
7820
|
+
if (frame !== null) {
|
|
7821
|
+
cancelAnimationFrame(frame)
|
|
7822
|
+
commitFloorplanPan()
|
|
7823
|
+
}
|
|
7824
|
+
}
|
|
7825
|
+
}, [
|
|
7826
|
+
applyFloorplanViewportImperatively,
|
|
7827
|
+
buildingRotationDeg,
|
|
7828
|
+
commitFloorplanPan,
|
|
7829
|
+
publishFloorplanNavigationPose,
|
|
7830
|
+
smoothFloorplanNavigationView,
|
|
7831
|
+
viewMode,
|
|
7832
|
+
])
|
|
7833
|
+
|
|
7787
7834
|
useLayoutEffect(() => {
|
|
7788
7835
|
flushFloorplanRotationPresentationRestore(pendingFloorplanRotationRestoreRef)
|
|
7789
7836
|
})
|