From a7b97caeb0fa6c068f78c741d33f65565d8c8311 Mon Sep 17 00:00:00 2001 From: sudhir Date: Thu, 16 Apr 2026 12:27:58 +0530 Subject: [PATCH] Add Alt-detach hints to wall endpoint move controls --- .../editor/floating-action-menu.tsx | 45 ++++++++++- .../src/components/editor/floorplan-panel.tsx | 74 ++++++++++++++----- .../tools/wall/move-wall-endpoint-tool.tsx | 72 ++++++++++++++---- 3 files changed, 154 insertions(+), 37 deletions(-) diff --git a/packages/editor/src/components/editor/floating-action-menu.tsx b/packages/editor/src/components/editor/floating-action-menu.tsx index 071dda03..05fb368c 100755 --- a/packages/editor/src/components/editor/floating-action-menu.tsx +++ b/packages/editor/src/components/editor/floating-action-menu.tsx @@ -21,7 +21,7 @@ import { useViewer } from '@pascal-app/viewer' import { Html } from '@react-three/drei' import { useFrame } from '@react-three/fiber' import { Move } from 'lucide-react' -import { useCallback, useRef } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import * as THREE from 'three' import { sfxEmitter } from '../../lib/sfx-bus' import useEditor from '../../store/use-editor' @@ -59,6 +59,7 @@ export function FloatingActionMenu() { const groupRef = useRef(null) const startEndpointGroupRef = useRef(null) const endEndpointGroupRef = useRef(null) + const [altPressed, setAltPressed] = useState(false) // Only show for single selection of specific types const selectedId = selectedIds.length === 1 ? selectedIds[0] : null @@ -77,6 +78,34 @@ export function FloatingActionMenu() { return false }) + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Alt') { + setAltPressed(true) + } + } + + const handleKeyUp = (event: KeyboardEvent) => { + if (event.key === 'Alt') { + setAltPressed(false) + } + } + + const handleBlur = () => { + setAltPressed(false) + } + + window.addEventListener('keydown', handleKeyDown) + window.addEventListener('keyup', handleKeyUp) + window.addEventListener('blur', handleBlur) + + return () => { + window.removeEventListener('keydown', handleKeyDown) + window.removeEventListener('keyup', handleKeyUp) + window.removeEventListener('blur', handleBlur) + } + }, []) + useFrame(() => { if (!(selectedId && isValidType && groupRef.current)) return @@ -384,9 +413,14 @@ export function FloatingActionMenu() { + + {wallEndpointDraft?.wallId === selectedWallEntry?.wall.id && + wallEndpointDraft.endpoint === endpoint && ( +
+ {altPressed ? 'Detaching corner' : 'Alt to detach'} +
+ )} + ))} {selectedSlabActionMenuPosition && isFloorplanHovered && !movingNode && !curvingWall && (
= ({ const activatedAtRef = useRef(Date.now()) const previousGridPosRef = useRef(null) const shiftPressedRef = useRef(false) + const altPressedRef = useRef(false) const nodeIdRef = useRef(target.wall.id) const originalStartRef = useRef([...target.wall.start] as WallPlanPoint) const originalEndRef = useRef([...target.wall.end] as WallPlanPoint) @@ -109,6 +111,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ const point = target.endpoint === 'start' ? target.wall.start : target.wall.end return [point[0], 0, point[1]] }) + const [altPressed, setAltPressed] = useState(false) const exitMoveMode = useCallback(() => { useEditor.getState().setMovingWallEndpoint(null) @@ -141,20 +144,22 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ } } - const applyPreview = (movingPoint: WallPlanPoint) => { + const applyPreview = (movingPoint: WallPlanPoint, detachLinkedWalls = false) => { const nextStart = target.endpoint === 'start' ? movingPoint : fixedPoint const nextEnd = target.endpoint === 'end' ? movingPoint : fixedPoint previewRef.current = { start: nextStart, end: nextEnd } setCursorLocalPos([movingPoint[0], 0, movingPoint[1]]) applyNodePreview([ { id: nodeId, start: nextStart, end: nextEnd }, - ...getLinkedWallUpdates( - linkedOriginalsRef.current, - originalStart, - originalEnd, - nextStart, - nextEnd, - ), + ...(detachLinkedWalls + ? [] + : getLinkedWallUpdates( + linkedOriginalsRef.current, + originalStart, + originalEnd, + nextStart, + nextEnd, + )), ]) } @@ -181,7 +186,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ } previousGridPosRef.current = snappedPoint - applyPreview(snappedPoint) + applyPreview(snappedPoint, event.nativeEvent.altKey) } const onGridClick = (event: GridEvent) => { @@ -199,13 +204,15 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ useScene.temporal.getState().resume() applyNodePreview([ { id: nodeId, start: preview.start, end: preview.end }, - ...getLinkedWallUpdates( - linkedOriginalsRef.current, - originalStart, - originalEnd, - preview.start, - preview.end, - ), + ...(altPressedRef.current + ? [] + : getLinkedWallUpdates( + linkedOriginalsRef.current, + originalStart, + originalEnd, + preview.start, + preview.end, + )), ]) useScene.temporal.getState().pause() sfxEmitter.emit('sfx:item-place') @@ -231,12 +238,26 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ if (event.key === 'Shift') { shiftPressedRef.current = true } + if (event.key === 'Alt') { + altPressedRef.current = true + setAltPressed(true) + } } const onKeyUp = (event: KeyboardEvent) => { if (event.key === 'Shift') { shiftPressedRef.current = false } + if (event.key === 'Alt') { + altPressedRef.current = false + setAltPressed(false) + } + } + + const onWindowBlur = () => { + shiftPressedRef.current = false + altPressedRef.current = false + setAltPressed(false) } emitter.on('grid:move', onGridMove) @@ -244,6 +265,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ emitter.on('tool:cancel', onCancel) window.addEventListener('keydown', onKeyDown) window.addEventListener('keyup', onKeyUp) + window.addEventListener('blur', onWindowBlur) return () => { if (!wasCommitted) { @@ -255,12 +277,30 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ emitter.off('tool:cancel', onCancel) window.removeEventListener('keydown', onKeyDown) window.removeEventListener('keyup', onKeyUp) + window.removeEventListener('blur', onWindowBlur) } }, [exitMoveMode, target]) return ( + +
+
+ {altPressed ? 'Detaching corner' : 'Alt to detach'} +
+
+
) }