@pascal-app/editor 1.0.0-beta.5 → 1.0.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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +9 -12
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +22 -5
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +1 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
STAND_CAPSULE,
|
|
41
41
|
STAND_CLEARANCE,
|
|
42
42
|
STAND_FLOAT_HEIGHT,
|
|
43
|
+
setSurfaceRaycastLayers,
|
|
43
44
|
useViewer,
|
|
44
45
|
WALKTHROUGH_FOV,
|
|
45
46
|
} from '@pascal-app/viewer'
|
|
@@ -89,6 +90,12 @@ import {
|
|
|
89
90
|
|
|
90
91
|
const CAMERA_EYE_OFFSET = 0.45
|
|
91
92
|
const LOOK_SENSITIVITY = 0.002
|
|
93
|
+
// Drone mode: metres per second, and how hard Shift boosts it. The smoothing
|
|
94
|
+
// constant is an exponential approach rate, not a linear acceleration.
|
|
95
|
+
const DRONE_SPEED = 7
|
|
96
|
+
const DRONE_RUN_MULTIPLIER = 3
|
|
97
|
+
const DRONE_SLOW_MULTIPLIER = 0.2
|
|
98
|
+
const DRONE_SMOOTHING = 12
|
|
92
99
|
const CONTROLLER_CENTER_FROM_EYE = 0.85
|
|
93
100
|
const DOOR_INTERACTION_DISTANCE = 2.5
|
|
94
101
|
const DOOR_LEAF_INTERACTION_DEPTH = 0.08
|
|
@@ -143,10 +150,15 @@ function focusFirstPersonCanvas(canvas: HTMLCanvasElement) {
|
|
|
143
150
|
|
|
144
151
|
const cameraOffset = new Vector3()
|
|
145
152
|
const cameraEuler = new Euler(0, 0, 0, 'YXZ')
|
|
153
|
+
const droneEuler = new Euler(0, 0, 0, 'YXZ')
|
|
154
|
+
const droneForward = new Vector3()
|
|
155
|
+
const droneRight = new Vector3()
|
|
156
|
+
const droneDesiredVelocity = new Vector3()
|
|
146
157
|
const standClearanceRaycaster = new Raycaster()
|
|
147
158
|
const standClearanceUp = new Vector3(0, 1, 0)
|
|
148
159
|
const centerScreenPoint = new Vector2(0, 0)
|
|
149
160
|
const doorInteractionRaycaster = new Raycaster()
|
|
161
|
+
setSurfaceRaycastLayers(doorInteractionRaycaster.layers)
|
|
150
162
|
const doorLeafBox = new Box3()
|
|
151
163
|
const doorLeafInverseMatrix = new Matrix4()
|
|
152
164
|
const doorLeafLocalHit = new Vector3()
|
|
@@ -169,6 +181,7 @@ const elevatorColliderMaterial = new MeshBasicMaterial({ visible: false })
|
|
|
169
181
|
const spawnWorldPosition = new Vector3()
|
|
170
182
|
const spawnWorldEuler = new Euler(0, 0, 0, 'YXZ')
|
|
171
183
|
const windowInteractionRaycaster = new Raycaster()
|
|
184
|
+
setSurfaceRaycastLayers(windowInteractionRaycaster.layers)
|
|
172
185
|
const hudBuildingLocalEyePosition = new Vector3()
|
|
173
186
|
const hudWorldEyePosition = new Vector3()
|
|
174
187
|
const hudLevelBounds = new Box3()
|
|
@@ -653,6 +666,7 @@ export const FirstPersonControls = () => {
|
|
|
653
666
|
const { camera, gl } = useThree()
|
|
654
667
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
655
668
|
const placedSpawnNode = useScene((state) => resolvePlacedSpawnNode(state.nodes, selectedLevelId))
|
|
669
|
+
const isDroneMode = useEditor((state) => state.firstPersonMovementMode === 'drone')
|
|
656
670
|
const controllerRef = useRef<BVHEcctrlApi | null>(null)
|
|
657
671
|
const movementInputRef = useRef<MovementInput>({ ...inactiveMovementInput })
|
|
658
672
|
const hadPointerLockRef = useRef(false)
|
|
@@ -661,9 +675,14 @@ export const FirstPersonControls = () => {
|
|
|
661
675
|
const interactableTargetRef = useRef<FirstPersonInteractableTarget | null>(null)
|
|
662
676
|
const hudLabelFrameRef = useRef(HUD_LABEL_SAMPLE_FRAMES - 1)
|
|
663
677
|
const crouchKeyRef = useRef(false)
|
|
678
|
+
const droneAscendKeyRef = useRef(false)
|
|
679
|
+
const droneSlowKeyRef = useRef(false)
|
|
680
|
+
const droneDescendKeyRef = useRef(false)
|
|
681
|
+
const droneVelocityRef = useRef(new Vector3())
|
|
664
682
|
const suspendRef = useRef(false)
|
|
665
683
|
const eyeOffsetRef = useRef(CAMERA_EYE_OFFSET)
|
|
666
684
|
const [crouched, setCrouched] = useState(false)
|
|
685
|
+
const captureShutterHold = useEditor((state) => state.captureShutterHold)
|
|
667
686
|
const [isElevatorRideLocked, setIsElevatorRideLocked] = useState(false)
|
|
668
687
|
const ridingElevatorRef = useRef<{
|
|
669
688
|
elevatorId: AnyNodeId
|
|
@@ -692,13 +711,19 @@ export const FirstPersonControls = () => {
|
|
|
692
711
|
}
|
|
693
712
|
}, [])
|
|
694
713
|
|
|
714
|
+
// While a snapshot is being framed the capture rig owns the fov (the user is
|
|
715
|
+
// driving it from the overlay's slider), so walkthrough neither applies its
|
|
716
|
+
// own nor restores one underneath it. Read imperatively: this must not re-run
|
|
717
|
+
// — and therefore restore — when capture mode toggles mid-walkthrough.
|
|
695
718
|
useEffect(() => {
|
|
696
719
|
const perspectiveCamera = camera as PerspectiveCamera
|
|
697
720
|
if (!perspectiveCamera.isPerspectiveCamera) return
|
|
721
|
+
if (useEditor.getState().isCaptureMode) return
|
|
698
722
|
const previousFov = perspectiveCamera.fov
|
|
699
723
|
perspectiveCamera.fov = WALKTHROUGH_FOV
|
|
700
724
|
perspectiveCamera.updateProjectionMatrix()
|
|
701
725
|
return () => {
|
|
726
|
+
if (useEditor.getState().isCaptureMode) return
|
|
702
727
|
perspectiveCamera.fov = previousFov
|
|
703
728
|
perspectiveCamera.updateProjectionMatrix()
|
|
704
729
|
}
|
|
@@ -931,6 +956,13 @@ export const FirstPersonControls = () => {
|
|
|
931
956
|
}, [resolveInteractableDoorId, resolveInteractableElevatorTarget, resolveInteractableWindowId])
|
|
932
957
|
|
|
933
958
|
const toggleInteractableTarget = useCallback(() => {
|
|
959
|
+
// Drone is a camera, not an avatar: the click that re-acquires pointer lock
|
|
960
|
+
// must not swing a door open under the shot being framed. (In capture
|
|
961
|
+
// mode's walk camera the CLICK path is gated at handleMouseDown — there a
|
|
962
|
+
// locked-pointer click is the shutter — but E/R still open doors, so the
|
|
963
|
+
// photographer can stage the shot.)
|
|
964
|
+
if (isDroneMode) return
|
|
965
|
+
|
|
934
966
|
const target = interactableTargetRef.current ?? resolveInteractableTarget()
|
|
935
967
|
if (!target) return
|
|
936
968
|
|
|
@@ -984,9 +1016,11 @@ export const FirstPersonControls = () => {
|
|
|
984
1016
|
if (node?.type !== 'door' || node.openingKind === 'opening') return
|
|
985
1017
|
|
|
986
1018
|
toggleDoorOpenState(doorId, { persist: false })
|
|
987
|
-
}, [resolveInteractableTarget])
|
|
1019
|
+
}, [isDroneMode, resolveInteractableTarget])
|
|
988
1020
|
|
|
989
1021
|
const closeInteractableTarget = useCallback(() => {
|
|
1022
|
+
if (isDroneMode) return
|
|
1023
|
+
|
|
990
1024
|
const target = interactableTargetRef.current ?? resolveInteractableTarget()
|
|
991
1025
|
if (!target) return
|
|
992
1026
|
|
|
@@ -1010,7 +1044,7 @@ export const FirstPersonControls = () => {
|
|
|
1010
1044
|
if (node?.type !== 'door' || node.openingKind === 'opening') return
|
|
1011
1045
|
|
|
1012
1046
|
closeDoorOpenState(target.id, { persist: false })
|
|
1013
|
-
}, [resolveInteractableTarget])
|
|
1047
|
+
}, [isDroneMode, resolveInteractableTarget])
|
|
1014
1048
|
|
|
1015
1049
|
const placedSpawn = useMemo<FirstPersonSpawn | null>(() => {
|
|
1016
1050
|
if (!(placedSpawnNode && placedSpawnNode.type === 'spawn')) return null
|
|
@@ -1042,7 +1076,10 @@ export const FirstPersonControls = () => {
|
|
|
1042
1076
|
}, [placedSpawnNode])
|
|
1043
1077
|
|
|
1044
1078
|
useEffect(() => {
|
|
1045
|
-
|
|
1079
|
+
// Drone has no gravity, no floor and no collision, so the BVH collider world
|
|
1080
|
+
// (a full-scene traversal, rebuilt on every door/window animation) is pure
|
|
1081
|
+
// cost there. Switching modes disposes it and rebuilds on the way back.
|
|
1082
|
+
if (!isDroneMode) rebuildColliderWorld()
|
|
1046
1083
|
|
|
1047
1084
|
return () => {
|
|
1048
1085
|
worldRef.current?.dispose()
|
|
@@ -1052,16 +1089,39 @@ export const FirstPersonControls = () => {
|
|
|
1052
1089
|
setElevatorColliderMeshes([])
|
|
1053
1090
|
setWorld(null)
|
|
1054
1091
|
}
|
|
1055
|
-
}, [rebuildColliderWorld])
|
|
1092
|
+
}, [isDroneMode, rebuildColliderWorld])
|
|
1056
1093
|
|
|
1057
1094
|
useEffect(() => {
|
|
1095
|
+
if (isDroneMode) return
|
|
1058
1096
|
emitter.on('door:animation-completed', rebuildColliderWorld)
|
|
1059
1097
|
emitter.on('window:animation-completed', rebuildColliderWorld)
|
|
1060
1098
|
return () => {
|
|
1061
1099
|
emitter.off('door:animation-completed', rebuildColliderWorld)
|
|
1062
1100
|
emitter.off('window:animation-completed', rebuildColliderWorld)
|
|
1063
1101
|
}
|
|
1064
|
-
}, [rebuildColliderWorld])
|
|
1102
|
+
}, [isDroneMode, rebuildColliderWorld])
|
|
1103
|
+
|
|
1104
|
+
// A walk session started before a drone detour would otherwise resume at its
|
|
1105
|
+
// original spawn; drop it so the next walk re-derives one.
|
|
1106
|
+
useEffect(() => {
|
|
1107
|
+
if (isDroneMode) setControllerStart(null)
|
|
1108
|
+
}, [isDroneMode])
|
|
1109
|
+
|
|
1110
|
+
// Drone picks up wherever the camera currently is — orbit pose or walk eye.
|
|
1111
|
+
useEffect(() => {
|
|
1112
|
+
if (!isDroneMode) return
|
|
1113
|
+
droneEuler.setFromQuaternion(camera.quaternion)
|
|
1114
|
+
yawRef.current = droneEuler.y
|
|
1115
|
+
pitchRef.current = droneEuler.x
|
|
1116
|
+
droneVelocityRef.current.set(0, 0, 0)
|
|
1117
|
+
|
|
1118
|
+
// Interaction prompts belong to walk; drop whatever its frame left behind.
|
|
1119
|
+
if (useViewer.getState().hoveredId === interactableTargetRef.current?.id) {
|
|
1120
|
+
useViewer.getState().setHoveredId(null)
|
|
1121
|
+
}
|
|
1122
|
+
interactableTargetRef.current = null
|
|
1123
|
+
useFirstPersonHud.getState().reset()
|
|
1124
|
+
}, [camera, isDroneMode])
|
|
1065
1125
|
|
|
1066
1126
|
useEffect(() => {
|
|
1067
1127
|
if (!world) return
|
|
@@ -1103,11 +1163,18 @@ export const FirstPersonControls = () => {
|
|
|
1103
1163
|
const canvas = gl.domElement
|
|
1104
1164
|
const handleMouseMove = (e: MouseEvent) => {
|
|
1105
1165
|
if (document.pointerLockElement !== canvas) return
|
|
1106
|
-
|
|
1107
|
-
|
|
1166
|
+
// Shutter hold: the shot is rendering — a mouse twitch must not pan it.
|
|
1167
|
+
if (useEditor.getState().captureShutterHold) return
|
|
1168
|
+
|
|
1169
|
+
const lookSensitivity =
|
|
1170
|
+
LOOK_SENSITIVITY *
|
|
1171
|
+
(useEditor.getState().firstPersonMovementMode === 'drone' && droneSlowKeyRef.current
|
|
1172
|
+
? DRONE_SLOW_MULTIPLIER
|
|
1173
|
+
: 1)
|
|
1174
|
+
yawRef.current -= e.movementX * lookSensitivity
|
|
1108
1175
|
pitchRef.current = Math.max(
|
|
1109
1176
|
-(Math.PI / 2 - 0.05),
|
|
1110
|
-
Math.min(Math.PI / 2 - 0.05, pitchRef.current - e.movementY *
|
|
1177
|
+
Math.min(Math.PI / 2 - 0.05, pitchRef.current - e.movementY * lookSensitivity),
|
|
1111
1178
|
)
|
|
1112
1179
|
}
|
|
1113
1180
|
|
|
@@ -1124,6 +1191,10 @@ export const FirstPersonControls = () => {
|
|
|
1124
1191
|
if (document.pointerLockElement !== canvas) return
|
|
1125
1192
|
if (event.button !== 0) return
|
|
1126
1193
|
|
|
1194
|
+
// Capture mode: the locked-pointer click is the SHUTTER (the snapshot
|
|
1195
|
+
// overlay's window-capture listener already fired); doors stay on E/R.
|
|
1196
|
+
if (useEditor.getState().isCaptureMode) return
|
|
1197
|
+
|
|
1127
1198
|
event.preventDefault()
|
|
1128
1199
|
event.stopPropagation()
|
|
1129
1200
|
toggleInteractableTargetRef.current()
|
|
@@ -1142,6 +1213,19 @@ export const FirstPersonControls = () => {
|
|
|
1142
1213
|
// clicking the canvas re-locks.
|
|
1143
1214
|
if (suspendRef.current) return
|
|
1144
1215
|
|
|
1216
|
+
// Capture mode: Esc (the browser's own unlock — no keydown reaches us)
|
|
1217
|
+
// acts like P. Dropping back to orbit would throw away the framed pose,
|
|
1218
|
+
// which reads as a crash to anyone who never noticed P.
|
|
1219
|
+
if (
|
|
1220
|
+
hadPointerLockRef.current &&
|
|
1221
|
+
useEditor.getState().isCaptureMode &&
|
|
1222
|
+
useEditor.getState().isFirstPersonMode
|
|
1223
|
+
) {
|
|
1224
|
+
suspendRef.current = true
|
|
1225
|
+
useViewer.getState().setWalkthroughSuspended(true)
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1145
1229
|
if (hadPointerLockRef.current && useEditor.getState().isFirstPersonMode) {
|
|
1146
1230
|
useEditor.getState().setFirstPersonMode(false)
|
|
1147
1231
|
}
|
|
@@ -1194,9 +1278,36 @@ export const FirstPersonControls = () => {
|
|
|
1194
1278
|
// While paused (P), crouch is frozen as-is — ⌃⇧⌘4 (clipboard
|
|
1195
1279
|
// screenshot) must not toggle it under the user.
|
|
1196
1280
|
if (!suspendRef.current) crouchKeyRef.current = true
|
|
1281
|
+
} else if (event.code === 'KeyQ') {
|
|
1282
|
+
// Drone descend. Space (already bound to jump) and E are the matching ascend.
|
|
1283
|
+
event.preventDefault()
|
|
1284
|
+
event.stopPropagation()
|
|
1285
|
+
if (!suspendRef.current) droneDescendKeyRef.current = true
|
|
1286
|
+
} else if (event.code === 'KeyE' && isDroneMode) {
|
|
1287
|
+
event.preventDefault()
|
|
1288
|
+
event.stopPropagation()
|
|
1289
|
+
if (!suspendRef.current) droneAscendKeyRef.current = true
|
|
1290
|
+
} else if ((event.code === 'AltLeft' || event.code === 'AltRight') && isDroneMode) {
|
|
1291
|
+
event.preventDefault()
|
|
1292
|
+
event.stopPropagation()
|
|
1293
|
+
if (!suspendRef.current) droneSlowKeyRef.current = true
|
|
1197
1294
|
} else if (event.code === 'Escape') {
|
|
1198
1295
|
event.preventDefault()
|
|
1199
1296
|
event.stopPropagation()
|
|
1297
|
+
// Capture mode, first Esc frees the cursor (see handlePointerLockChange
|
|
1298
|
+
// — while locked the browser usually unlocks without delivering the
|
|
1299
|
+
// keydown); with the cursor already free, Esc cancels the snapshot
|
|
1300
|
+
// (setCaptureMode(false) also lands the camera back on orbit).
|
|
1301
|
+
if (useEditor.getState().isCaptureMode) {
|
|
1302
|
+
if (document.pointerLockElement === canvas) {
|
|
1303
|
+
suspendRef.current = true
|
|
1304
|
+
useViewer.getState().setWalkthroughSuspended(true)
|
|
1305
|
+
document.exitPointerLock()
|
|
1306
|
+
} else {
|
|
1307
|
+
useEditor.getState().setCaptureMode(false)
|
|
1308
|
+
}
|
|
1309
|
+
return
|
|
1310
|
+
}
|
|
1200
1311
|
if (document.pointerLockElement === canvas) {
|
|
1201
1312
|
document.exitPointerLock()
|
|
1202
1313
|
}
|
|
@@ -1230,11 +1341,25 @@ export const FirstPersonControls = () => {
|
|
|
1230
1341
|
if ((event.code === 'ControlLeft' || event.code === 'ControlRight') && !suspendRef.current) {
|
|
1231
1342
|
crouchKeyRef.current = false
|
|
1232
1343
|
}
|
|
1344
|
+
if (event.code === 'KeyQ' && !suspendRef.current) {
|
|
1345
|
+
droneDescendKeyRef.current = false
|
|
1346
|
+
}
|
|
1347
|
+
if (event.code === 'KeyE' && !suspendRef.current) {
|
|
1348
|
+
droneAscendKeyRef.current = false
|
|
1349
|
+
}
|
|
1350
|
+
if ((event.code === 'AltLeft' || event.code === 'AltRight') && !suspendRef.current) {
|
|
1351
|
+
droneSlowKeyRef.current = false
|
|
1352
|
+
}
|
|
1233
1353
|
applyMovementKey(event, false)
|
|
1234
1354
|
}
|
|
1235
1355
|
|
|
1236
1356
|
const handleBlur = () => {
|
|
1237
|
-
if (!suspendRef.current)
|
|
1357
|
+
if (!suspendRef.current) {
|
|
1358
|
+
crouchKeyRef.current = false
|
|
1359
|
+
droneAscendKeyRef.current = false
|
|
1360
|
+
droneDescendKeyRef.current = false
|
|
1361
|
+
droneSlowKeyRef.current = false
|
|
1362
|
+
}
|
|
1238
1363
|
}
|
|
1239
1364
|
|
|
1240
1365
|
document.addEventListener('keydown', handleKeyDown, true)
|
|
@@ -1245,7 +1370,7 @@ export const FirstPersonControls = () => {
|
|
|
1245
1370
|
document.removeEventListener('keyup', handleKeyUp, true)
|
|
1246
1371
|
window.removeEventListener('blur', handleBlur)
|
|
1247
1372
|
}
|
|
1248
|
-
}, [closeInteractableTarget, gl, toggleInteractableTarget])
|
|
1373
|
+
}, [closeInteractableTarget, gl, isDroneMode, toggleInteractableTarget])
|
|
1249
1374
|
|
|
1250
1375
|
const syncElevatorColliderMeshes = useCallback(() => {
|
|
1251
1376
|
const nodes = useScene.getState().nodes
|
|
@@ -1321,6 +1446,7 @@ export const FirstPersonControls = () => {
|
|
|
1321
1446
|
}, [])
|
|
1322
1447
|
|
|
1323
1448
|
useFrame(() => {
|
|
1449
|
+
if (isDroneMode) return
|
|
1324
1450
|
syncElevatorColliderMeshes()
|
|
1325
1451
|
}, -1)
|
|
1326
1452
|
|
|
@@ -1480,7 +1606,49 @@ export const FirstPersonControls = () => {
|
|
|
1480
1606
|
return standClearanceRaycaster.intersectObjects(meshes, false).length === 0
|
|
1481
1607
|
}, [])
|
|
1482
1608
|
|
|
1609
|
+
// Drone: a free camera driven straight from the look angles — no controller,
|
|
1610
|
+
// no gravity or collision clamping. WASD move along the view axes, Space or E
|
|
1611
|
+
// rises, Q (or Ctrl) sinks, and Shift boosts.
|
|
1612
|
+
useFrame((_, delta) => {
|
|
1613
|
+
if (!isDroneMode) return
|
|
1614
|
+
// Shutter hold: freeze the drone mid-air while the shot renders.
|
|
1615
|
+
if (useEditor.getState().captureShutterHold) return
|
|
1616
|
+
|
|
1617
|
+
const step = Math.min(delta, 0.1)
|
|
1618
|
+
const movement = movementInputRef.current
|
|
1619
|
+
|
|
1620
|
+
droneEuler.set(pitchRef.current, yawRef.current, 0, 'YXZ')
|
|
1621
|
+
camera.quaternion.setFromEuler(droneEuler)
|
|
1622
|
+
droneForward.set(0, 0, -1).applyEuler(droneEuler)
|
|
1623
|
+
droneRight.set(1, 0, 0).applyEuler(droneEuler)
|
|
1624
|
+
|
|
1625
|
+
droneDesiredVelocity.set(0, 0, 0)
|
|
1626
|
+
if (movement.forward) droneDesiredVelocity.add(droneForward)
|
|
1627
|
+
if (movement.backward) droneDesiredVelocity.sub(droneForward)
|
|
1628
|
+
if (movement.rightward) droneDesiredVelocity.add(droneRight)
|
|
1629
|
+
if (movement.leftward) droneDesiredVelocity.sub(droneRight)
|
|
1630
|
+
if (movement.jump || droneAscendKeyRef.current) droneDesiredVelocity.y += 1
|
|
1631
|
+
if (droneDescendKeyRef.current || crouchKeyRef.current) droneDesiredVelocity.y -= 1
|
|
1632
|
+
if (droneDesiredVelocity.lengthSq() > 0) {
|
|
1633
|
+
droneDesiredVelocity
|
|
1634
|
+
.normalize()
|
|
1635
|
+
.multiplyScalar(
|
|
1636
|
+
DRONE_SPEED *
|
|
1637
|
+
(droneSlowKeyRef.current
|
|
1638
|
+
? DRONE_SLOW_MULTIPLIER
|
|
1639
|
+
: movement.run
|
|
1640
|
+
? DRONE_RUN_MULTIPLIER
|
|
1641
|
+
: 1),
|
|
1642
|
+
)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
droneVelocityRef.current.lerp(droneDesiredVelocity, 1 - Math.exp(-step * DRONE_SMOOTHING))
|
|
1646
|
+
camera.position.addScaledVector(droneVelocityRef.current, step)
|
|
1647
|
+
camera.updateMatrixWorld(true)
|
|
1648
|
+
}, 2.5)
|
|
1649
|
+
|
|
1483
1650
|
useFrame((_, delta) => {
|
|
1651
|
+
if (isDroneMode) return
|
|
1484
1652
|
if (!controllerRef.current?.group) return
|
|
1485
1653
|
|
|
1486
1654
|
const group = controllerRef.current.group
|
|
@@ -1562,7 +1730,7 @@ export const FirstPersonControls = () => {
|
|
|
1562
1730
|
[world, elevatorColliderMeshes],
|
|
1563
1731
|
)
|
|
1564
1732
|
|
|
1565
|
-
if (!world) {
|
|
1733
|
+
if (isDroneMode || !world) {
|
|
1566
1734
|
return null
|
|
1567
1735
|
}
|
|
1568
1736
|
|
|
@@ -1594,7 +1762,7 @@ export const FirstPersonControls = () => {
|
|
|
1594
1762
|
maxRunSpeed={crouched ? CROUCH_RUN_SPEED : 5}
|
|
1595
1763
|
maxSlope={1.2}
|
|
1596
1764
|
maxWalkSpeed={crouched ? CROUCH_WALK_SPEED : 2}
|
|
1597
|
-
paused={isElevatorRideLocked}
|
|
1765
|
+
paused={isElevatorRideLocked || captureShutterHold}
|
|
1598
1766
|
position={controllerStart.position}
|
|
1599
1767
|
ref={setControllerApi}
|
|
1600
1768
|
/>
|
|
@@ -43,12 +43,13 @@ import * as THREE from 'three'
|
|
|
43
43
|
import { useShallow } from 'zustand/react/shallow'
|
|
44
44
|
import { useReducedMotion } from '../../hooks/use-reduced-motion'
|
|
45
45
|
import { resolveMoveActionNode } from '../../lib/direct-manipulation'
|
|
46
|
+
import { getFloatingMenuScale } from '../../lib/floating-menu-scale'
|
|
46
47
|
import {
|
|
47
48
|
createFreshPlacementSubtree,
|
|
48
49
|
duplicatesAsFreshSubtree,
|
|
49
50
|
prepareFreshPlacementRootDuplicate,
|
|
50
51
|
} from '../../lib/fresh-planar-placement'
|
|
51
|
-
import {
|
|
52
|
+
import { resolveFloatingActionMenuVisibility } from '../../lib/interaction/overlay-policy'
|
|
52
53
|
import { curveReshapeScope, holeEditScope } from '../../lib/interaction/scope'
|
|
53
54
|
import { playBlockedQuickActionFeedback } from '../../lib/quick-action-feedback'
|
|
54
55
|
import { collectQuickActionNodeScope } from '../../lib/quick-action-nodes'
|
|
@@ -99,17 +100,6 @@ const ALLOWED_TYPES = [
|
|
|
99
100
|
const DELETE_ONLY_TYPES: string[] = []
|
|
100
101
|
const HOLE_TYPES = ['slab', 'ceiling']
|
|
101
102
|
|
|
102
|
-
// Menu scales with camera zoom so it feels anchored to the object, but is
|
|
103
|
-
// clamped on both ends so it stays readable when zoomed way out and doesn't
|
|
104
|
-
// dominate the screen when zoomed in close. Reference values are picked so
|
|
105
|
-
// scale = 1 lands near the editor's default framing.
|
|
106
|
-
const MIN_MENU_SCALE = 0.5
|
|
107
|
-
// Cap at 1 so zooming in doesn't grow the menu past its default pixel size —
|
|
108
|
-
// only zoom-out shrinks it (down to MIN_MENU_SCALE).
|
|
109
|
-
const MAX_MENU_SCALE = 1
|
|
110
|
-
const REF_ORTHO_ZOOM = 20
|
|
111
|
-
const REF_CAMERA_DISTANCE = 12
|
|
112
|
-
|
|
113
103
|
// World-space Y distance from a node's bbox top to the floating menu anchor.
|
|
114
104
|
// Per-type because in-world chrome above the node (height-resize arrows,
|
|
115
105
|
// measurement labels) varies in vertical reach.
|
|
@@ -301,10 +291,7 @@ export function FloatingActionMenu() {
|
|
|
301
291
|
const activeHandleDrag = useActiveHandleDrag()
|
|
302
292
|
// R/T rotation axis for kinds with full 3D orientation (duct fittings).
|
|
303
293
|
const rotationAxis = useEditor((s) => s.rotationAxis)
|
|
304
|
-
// The floating action menu is an action-conflicting control: hard-hidden
|
|
305
|
-
// during any active interaction so it never competes with the live action.
|
|
306
294
|
const scope = useInteractionScope((s) => s.scope)
|
|
307
|
-
const menuStepBack = resolveOverlayPolicy(scope).conflictingControls === 'hidden'
|
|
308
295
|
|
|
309
296
|
const groupRef = useRef<THREE.Group>(null)
|
|
310
297
|
const menuScaleRef = useRef<HTMLDivElement>(null)
|
|
@@ -366,6 +353,7 @@ export function FloatingActionMenu() {
|
|
|
366
353
|
activeHandleDrag?.nodeId === selectedId &&
|
|
367
354
|
activeHandleDrag?.label === 'height'
|
|
368
355
|
const pillDims = pillNode ? getHeightPillDimensions(pillNode) : null
|
|
356
|
+
const menuVisibility = resolveFloatingActionMenuVisibility(scope, isHeightDragPill)
|
|
369
357
|
|
|
370
358
|
// Boolean selector, only re-renders when curving availability actually flips.
|
|
371
359
|
const canCurveSelectedWall = useScene((s) => {
|
|
@@ -392,12 +380,7 @@ export function FloatingActionMenu() {
|
|
|
392
380
|
// so it stays readable at extreme zoom-out and doesn't fill the screen
|
|
393
381
|
// when zoomed in close.
|
|
394
382
|
if (menuScaleRef.current) {
|
|
395
|
-
const
|
|
396
|
-
state.camera instanceof THREE.OrthographicCamera
|
|
397
|
-
? state.camera.zoom / REF_ORTHO_ZOOM
|
|
398
|
-
: REF_CAMERA_DISTANCE /
|
|
399
|
-
Math.max(state.camera.position.distanceTo(groupRef.current.position), 0.001)
|
|
400
|
-
const scale = Math.min(MAX_MENU_SCALE, Math.max(MIN_MENU_SCALE, raw))
|
|
383
|
+
const scale = getFloatingMenuScale(state.camera, groupRef.current.position)
|
|
401
384
|
menuScaleRef.current.style.transform = `scale(${scale})`
|
|
402
385
|
}
|
|
403
386
|
|
|
@@ -781,7 +764,7 @@ export function FloatingActionMenu() {
|
|
|
781
764
|
!(selectedId && node && isValidType && !isFloorplanHovered && mode !== 'delete') ||
|
|
782
765
|
endpointReshape ||
|
|
783
766
|
isCurveReshape ||
|
|
784
|
-
|
|
767
|
+
!menuVisibility.root
|
|
785
768
|
)
|
|
786
769
|
return null
|
|
787
770
|
|
|
@@ -801,35 +784,43 @@ export function FloatingActionMenu() {
|
|
|
801
784
|
ref={menuScaleRef}
|
|
802
785
|
style={{ transformOrigin: 'center center' }}
|
|
803
786
|
>
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
787
|
+
{menuVisibility.actions ? (
|
|
788
|
+
<NodeActionMenu
|
|
789
|
+
onFind={
|
|
790
|
+
node &&
|
|
791
|
+
canFindNode &&
|
|
792
|
+
nodeRegistry.get(node.type)?.presentation?.findInCatalog !== false
|
|
793
|
+
? handleFind
|
|
794
|
+
: undefined
|
|
795
|
+
}
|
|
796
|
+
onAddHole={node && HOLE_TYPES.includes(node.type) ? handleAddHole : undefined}
|
|
797
|
+
onCurve={
|
|
798
|
+
(node?.type === 'fence' && !isSplineFence(node) && !isCurvedWall(node)) ||
|
|
799
|
+
(node?.type === 'wall' && canCurveSelectedWall)
|
|
800
|
+
? handleCurve
|
|
801
|
+
: undefined
|
|
802
|
+
}
|
|
803
|
+
onMove={
|
|
804
|
+
// Fully registry-driven: any kind that declares
|
|
805
|
+
// `capabilities.movable`, a `floorplanMoveTarget`, or a
|
|
806
|
+
// 3D `affordanceTools.move` mover gets the Move button.
|
|
807
|
+
// Adding a new movable kind never touches this file.
|
|
808
|
+
node && isRegistryMovable(node.type) ? handleMove : undefined
|
|
809
|
+
}
|
|
810
|
+
onDelete={handleDelete}
|
|
811
|
+
onDuplicate={
|
|
812
|
+
node &&
|
|
813
|
+
node.type !== 'spawn' &&
|
|
814
|
+
!DELETE_ONLY_TYPES.includes(node.type) &&
|
|
815
|
+
!HOLE_TYPES.includes(node.type)
|
|
816
|
+
? handleDuplicate
|
|
817
|
+
: undefined
|
|
818
|
+
}
|
|
819
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
820
|
+
onPointerUp={(e) => e.stopPropagation()}
|
|
821
|
+
/>
|
|
822
|
+
) : null}
|
|
823
|
+
{menuVisibility.actions && quickActions.length > 0 ? (
|
|
833
824
|
<div
|
|
834
825
|
className="pointer-events-auto mt-1 inline-flex w-max items-center justify-center gap-0.5 rounded-lg border border-border/50 bg-background/90 px-1.5 py-1 shadow-md backdrop-blur-md"
|
|
835
826
|
onPointerDown={(e) => e.stopPropagation()}
|