Add Alt-detach hints to wall endpoint move controls
This commit is contained in:
@@ -21,7 +21,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { Html } from '@react-three/drei'
|
import { Html } from '@react-three/drei'
|
||||||
import { useFrame } from '@react-three/fiber'
|
import { useFrame } from '@react-three/fiber'
|
||||||
import { Move } from 'lucide-react'
|
import { Move } from 'lucide-react'
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { sfxEmitter } from '../../lib/sfx-bus'
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
||||||
import useEditor from '../../store/use-editor'
|
import useEditor from '../../store/use-editor'
|
||||||
@@ -59,6 +59,7 @@ export function FloatingActionMenu() {
|
|||||||
const groupRef = useRef<THREE.Group>(null)
|
const groupRef = useRef<THREE.Group>(null)
|
||||||
const startEndpointGroupRef = useRef<THREE.Group>(null)
|
const startEndpointGroupRef = useRef<THREE.Group>(null)
|
||||||
const endEndpointGroupRef = useRef<THREE.Group>(null)
|
const endEndpointGroupRef = useRef<THREE.Group>(null)
|
||||||
|
const [altPressed, setAltPressed] = useState(false)
|
||||||
|
|
||||||
// Only show for single selection of specific types
|
// Only show for single selection of specific types
|
||||||
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
||||||
@@ -77,6 +78,34 @@ export function FloatingActionMenu() {
|
|||||||
return false
|
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(() => {
|
useFrame(() => {
|
||||||
if (!(selectedId && isValidType && groupRef.current)) return
|
if (!(selectedId && isValidType && groupRef.current)) return
|
||||||
|
|
||||||
@@ -384,9 +413,14 @@ export function FloatingActionMenu() {
|
|||||||
<Html center style={{ pointerEvents: 'auto', touchAction: 'none' }} zIndexRange={[100, 0]}>
|
<Html center style={{ pointerEvents: 'auto', touchAction: 'none' }} zIndexRange={[100, 0]}>
|
||||||
<button
|
<button
|
||||||
aria-label="Move wall start"
|
aria-label="Move wall start"
|
||||||
className="pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border border-border bg-background/95 text-muted-foreground shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-foreground"
|
className={`pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border bg-background/95 shadow-lg backdrop-blur-md transition-colors ${
|
||||||
|
altPressed
|
||||||
|
? 'border-amber-500/80 bg-amber-500/15 text-amber-100 hover:bg-amber-500/20 hover:text-white'
|
||||||
|
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||||
|
}`}
|
||||||
onClick={(e) => handleEndpointMove('start', e)}
|
onClick={(e) => handleEndpointMove('start', e)}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
title="Move wall start (Alt to detach)"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Move className="h-4 w-4" />
|
<Move className="h-4 w-4" />
|
||||||
@@ -397,9 +431,14 @@ export function FloatingActionMenu() {
|
|||||||
<Html center style={{ pointerEvents: 'auto', touchAction: 'none' }} zIndexRange={[100, 0]}>
|
<Html center style={{ pointerEvents: 'auto', touchAction: 'none' }} zIndexRange={[100, 0]}>
|
||||||
<button
|
<button
|
||||||
aria-label="Move wall end"
|
aria-label="Move wall end"
|
||||||
className="pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border border-border bg-background/95 text-muted-foreground shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-foreground"
|
className={`pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border bg-background/95 shadow-lg backdrop-blur-md transition-colors ${
|
||||||
|
altPressed
|
||||||
|
? 'border-amber-500/80 bg-amber-500/15 text-amber-100 hover:bg-amber-500/20 hover:text-white'
|
||||||
|
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||||
|
}`}
|
||||||
onClick={(e) => handleEndpointMove('end', e)}
|
onClick={(e) => handleEndpointMove('end', e)}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
title="Move wall end (Alt to detach)"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Move className="h-4 w-4" />
|
<Move className="h-4 w-4" />
|
||||||
|
|||||||
@@ -2101,6 +2101,7 @@ function buildWallEndpointDragDraft(
|
|||||||
'wallId' | 'endpoint' | 'fixedPoint' | 'originalStart' | 'originalEnd' | 'linkedWalls'
|
'wallId' | 'endpoint' | 'fixedPoint' | 'originalStart' | 'originalEnd' | 'linkedWalls'
|
||||||
>,
|
>,
|
||||||
movingPoint: WallPlanPoint,
|
movingPoint: WallPlanPoint,
|
||||||
|
detachLinkedWalls = false,
|
||||||
): WallEndpointDraft {
|
): WallEndpointDraft {
|
||||||
const nextDraft = buildWallEndpointDraft(
|
const nextDraft = buildWallEndpointDraft(
|
||||||
dragState.wallId,
|
dragState.wallId,
|
||||||
@@ -2111,13 +2112,15 @@ function buildWallEndpointDragDraft(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...nextDraft,
|
...nextDraft,
|
||||||
linkedUpdates: getLinkedWallUpdates(
|
linkedUpdates: detachLinkedWalls
|
||||||
dragState.linkedWalls,
|
? []
|
||||||
dragState.originalStart,
|
: getLinkedWallUpdates(
|
||||||
dragState.originalEnd,
|
dragState.linkedWalls,
|
||||||
nextDraft.start,
|
dragState.originalStart,
|
||||||
nextDraft.end,
|
dragState.originalEnd,
|
||||||
),
|
nextDraft.start,
|
||||||
|
nextDraft.end,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4971,6 +4974,7 @@ export function FloorplanPanel() {
|
|||||||
const [floorplanCursorPosition, setFloorplanCursorPosition] = useState<SvgPoint | null>(null)
|
const [floorplanCursorPosition, setFloorplanCursorPosition] = useState<SvgPoint | null>(null)
|
||||||
const [wallEndpointDraft, setWallEndpointDraft] = useState<WallEndpointDraft | null>(null)
|
const [wallEndpointDraft, setWallEndpointDraft] = useState<WallEndpointDraft | null>(null)
|
||||||
const [wallCurveDraft, setWallCurveDraft] = useState<WallCurveDraft | null>(null)
|
const [wallCurveDraft, setWallCurveDraft] = useState<WallCurveDraft | null>(null)
|
||||||
|
const [altPressed, setAltPressed] = useState(false)
|
||||||
const [hoveredOpeningId, setHoveredOpeningId] = useState<OpeningNode['id'] | null>(null)
|
const [hoveredOpeningId, setHoveredOpeningId] = useState<OpeningNode['id'] | null>(null)
|
||||||
const [hoveredWallId, setHoveredWallId] = useState<WallNode['id'] | null>(null)
|
const [hoveredWallId, setHoveredWallId] = useState<WallNode['id'] | null>(null)
|
||||||
const [hoveredSlabId, setHoveredSlabId] = useState<SlabNode['id'] | null>(null)
|
const [hoveredSlabId, setHoveredSlabId] = useState<SlabNode['id'] | null>(null)
|
||||||
@@ -6740,6 +6744,9 @@ export function FloorplanPanel() {
|
|||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
setShiftPressed(true)
|
setShiftPressed(true)
|
||||||
}
|
}
|
||||||
|
if (event.key === 'Alt') {
|
||||||
|
setAltPressed(true)
|
||||||
|
}
|
||||||
|
|
||||||
if (isStairBuildActive && (event.key === 'r' || event.key === 'R')) {
|
if (isStairBuildActive && (event.key === 'r' || event.key === 'R')) {
|
||||||
setStairBuildPreviewRotation((current) => current + Math.PI / 4)
|
setStairBuildPreviewRotation((current) => current + Math.PI / 4)
|
||||||
@@ -6762,11 +6769,15 @@ export function FloorplanPanel() {
|
|||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
setShiftPressed(false)
|
setShiftPressed(false)
|
||||||
}
|
}
|
||||||
|
if (event.key === 'Alt') {
|
||||||
|
setAltPressed(false)
|
||||||
|
}
|
||||||
|
|
||||||
setRotationModifierPressed(event.metaKey || event.ctrlKey)
|
setRotationModifierPressed(event.metaKey || event.ctrlKey)
|
||||||
}
|
}
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
setShiftPressed(false)
|
setShiftPressed(false)
|
||||||
|
setAltPressed(false)
|
||||||
setRotationModifierPressed(false)
|
setRotationModifierPressed(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6832,7 +6843,7 @@ export function FloorplanPanel() {
|
|||||||
dragState.currentPoint = snappedPoint
|
dragState.currentPoint = snappedPoint
|
||||||
setCursorPoint(snappedPoint)
|
setCursorPoint(snappedPoint)
|
||||||
setWallEndpointDraft((previousDraft) => {
|
setWallEndpointDraft((previousDraft) => {
|
||||||
const nextDraft = buildWallEndpointDragDraft(dragState, snappedPoint)
|
const nextDraft = buildWallEndpointDragDraft(dragState, snappedPoint, event.altKey)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
@@ -6955,7 +6966,7 @@ export function FloorplanPanel() {
|
|||||||
|
|
||||||
const wall = wallById.get(dragState.wallId)
|
const wall = wallById.get(dragState.wallId)
|
||||||
if (wall) {
|
if (wall) {
|
||||||
const nextDraft = buildWallEndpointDragDraft(dragState, dragState.currentPoint)
|
const nextDraft = buildWallEndpointDragDraft(dragState, dragState.currentPoint, altPressed)
|
||||||
const hasChanged = !(
|
const hasChanged = !(
|
||||||
pointsEqual(nextDraft.start, wall.start) && pointsEqual(nextDraft.end, wall.end)
|
pointsEqual(nextDraft.start, wall.start) && pointsEqual(nextDraft.end, wall.end)
|
||||||
)
|
)
|
||||||
@@ -7043,6 +7054,7 @@ export function FloorplanPanel() {
|
|||||||
getSvgPointFromClientPoint,
|
getSvgPointFromClientPoint,
|
||||||
guideById,
|
guideById,
|
||||||
getPlanPointFromClientPoint,
|
getPlanPointFromClientPoint,
|
||||||
|
altPressed,
|
||||||
shiftPressed,
|
shiftPressed,
|
||||||
updateNode,
|
updateNode,
|
||||||
wallById,
|
wallById,
|
||||||
@@ -9919,21 +9931,47 @@ export function FloorplanPanel() {
|
|||||||
!movingNode &&
|
!movingNode &&
|
||||||
!curvingWall &&
|
!curvingWall &&
|
||||||
selectedWallCornerMoveActions.map(({ endpoint, x, y }) => (
|
selectedWallCornerMoveActions.map(({ endpoint, x, y }) => (
|
||||||
<button
|
<div
|
||||||
aria-label={endpoint === 'start' ? 'Move wall start' : 'Move wall end'}
|
className="absolute z-30"
|
||||||
className="pointer-events-auto absolute z-30 flex h-8 w-8 items-center justify-center rounded-full border border-border bg-background/95 text-muted-foreground shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-foreground"
|
|
||||||
key={`selected-wall-corner-move-${endpoint}`}
|
key={`selected-wall-corner-move-${endpoint}`}
|
||||||
onPointerDown={(event) => handleSelectedWallCornerMovePointerDown(endpoint, event)}
|
|
||||||
style={{
|
style={{
|
||||||
left: x,
|
left: x,
|
||||||
top: y,
|
top: y,
|
||||||
transform: `translate(-50%, calc(-100% - ${FLOORPLAN_ACTION_MENU_OFFSET_Y + 6}px))`,
|
transform: `translate(-50%, calc(-100% - ${FLOORPLAN_ACTION_MENU_OFFSET_Y - 4}px))`,
|
||||||
}}
|
}}
|
||||||
title={endpoint === 'start' ? 'Move wall start' : 'Move wall end'}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<Move className="h-4 w-4" />
|
<button
|
||||||
</button>
|
aria-label={endpoint === 'start' ? 'Move wall start' : 'Move wall end'}
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border bg-background/95 shadow-lg backdrop-blur-md transition-colors',
|
||||||
|
altPressed
|
||||||
|
? 'border-amber-500/80 bg-amber-500/15 text-amber-100 hover:bg-amber-500/20 hover:text-white'
|
||||||
|
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground',
|
||||||
|
)}
|
||||||
|
onPointerDown={(event) => handleSelectedWallCornerMovePointerDown(endpoint, event)}
|
||||||
|
title={
|
||||||
|
endpoint === 'start'
|
||||||
|
? 'Move wall start (Alt to detach)'
|
||||||
|
: 'Move wall end (Alt to detach)'
|
||||||
|
}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Move className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
{wallEndpointDraft?.wallId === selectedWallEntry?.wall.id &&
|
||||||
|
wallEndpointDraft.endpoint === endpoint && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none mt-2 whitespace-nowrap rounded-full border px-2 py-1 text-[11px] font-medium shadow-lg backdrop-blur-md transition-colors',
|
||||||
|
altPressed
|
||||||
|
? 'border-amber-500/80 bg-amber-500/15 text-amber-100'
|
||||||
|
: 'border-border bg-background/95 text-muted-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{altPressed ? 'Detaching corner' : 'Alt to detach'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{selectedSlabActionMenuPosition && isFloorplanHovered && !movingNode && !curvingWall && (
|
{selectedSlabActionMenuPosition && isFloorplanHovered && !movingNode && !curvingWall && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type AnyNodeId, emitter, type GridEvent, useScene, type WallNode } from '@pascal-app/core'
|
import { type AnyNodeId, emitter, type GridEvent, useScene, type WallNode } from '@pascal-app/core'
|
||||||
|
import { Html } from '@react-three/drei'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
@@ -87,6 +88,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
const activatedAtRef = useRef<number>(Date.now())
|
const activatedAtRef = useRef<number>(Date.now())
|
||||||
const previousGridPosRef = useRef<WallPlanPoint | null>(null)
|
const previousGridPosRef = useRef<WallPlanPoint | null>(null)
|
||||||
const shiftPressedRef = useRef(false)
|
const shiftPressedRef = useRef(false)
|
||||||
|
const altPressedRef = useRef(false)
|
||||||
const nodeIdRef = useRef(target.wall.id)
|
const nodeIdRef = useRef(target.wall.id)
|
||||||
const originalStartRef = useRef<WallPlanPoint>([...target.wall.start] as WallPlanPoint)
|
const originalStartRef = useRef<WallPlanPoint>([...target.wall.start] as WallPlanPoint)
|
||||||
const originalEndRef = useRef<WallPlanPoint>([...target.wall.end] as WallPlanPoint)
|
const originalEndRef = useRef<WallPlanPoint>([...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
|
const point = target.endpoint === 'start' ? target.wall.start : target.wall.end
|
||||||
return [point[0], 0, point[1]]
|
return [point[0], 0, point[1]]
|
||||||
})
|
})
|
||||||
|
const [altPressed, setAltPressed] = useState(false)
|
||||||
|
|
||||||
const exitMoveMode = useCallback(() => {
|
const exitMoveMode = useCallback(() => {
|
||||||
useEditor.getState().setMovingWallEndpoint(null)
|
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 nextStart = target.endpoint === 'start' ? movingPoint : fixedPoint
|
||||||
const nextEnd = target.endpoint === 'end' ? movingPoint : fixedPoint
|
const nextEnd = target.endpoint === 'end' ? movingPoint : fixedPoint
|
||||||
previewRef.current = { start: nextStart, end: nextEnd }
|
previewRef.current = { start: nextStart, end: nextEnd }
|
||||||
setCursorLocalPos([movingPoint[0], 0, movingPoint[1]])
|
setCursorLocalPos([movingPoint[0], 0, movingPoint[1]])
|
||||||
applyNodePreview([
|
applyNodePreview([
|
||||||
{ id: nodeId, start: nextStart, end: nextEnd },
|
{ id: nodeId, start: nextStart, end: nextEnd },
|
||||||
...getLinkedWallUpdates(
|
...(detachLinkedWalls
|
||||||
linkedOriginalsRef.current,
|
? []
|
||||||
originalStart,
|
: getLinkedWallUpdates(
|
||||||
originalEnd,
|
linkedOriginalsRef.current,
|
||||||
nextStart,
|
originalStart,
|
||||||
nextEnd,
|
originalEnd,
|
||||||
),
|
nextStart,
|
||||||
|
nextEnd,
|
||||||
|
)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +186,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
}
|
}
|
||||||
previousGridPosRef.current = snappedPoint
|
previousGridPosRef.current = snappedPoint
|
||||||
|
|
||||||
applyPreview(snappedPoint)
|
applyPreview(snappedPoint, event.nativeEvent.altKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onGridClick = (event: GridEvent) => {
|
const onGridClick = (event: GridEvent) => {
|
||||||
@@ -199,13 +204,15 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
useScene.temporal.getState().resume()
|
useScene.temporal.getState().resume()
|
||||||
applyNodePreview([
|
applyNodePreview([
|
||||||
{ id: nodeId, start: preview.start, end: preview.end },
|
{ id: nodeId, start: preview.start, end: preview.end },
|
||||||
...getLinkedWallUpdates(
|
...(altPressedRef.current
|
||||||
linkedOriginalsRef.current,
|
? []
|
||||||
originalStart,
|
: getLinkedWallUpdates(
|
||||||
originalEnd,
|
linkedOriginalsRef.current,
|
||||||
preview.start,
|
originalStart,
|
||||||
preview.end,
|
originalEnd,
|
||||||
),
|
preview.start,
|
||||||
|
preview.end,
|
||||||
|
)),
|
||||||
])
|
])
|
||||||
useScene.temporal.getState().pause()
|
useScene.temporal.getState().pause()
|
||||||
sfxEmitter.emit('sfx:item-place')
|
sfxEmitter.emit('sfx:item-place')
|
||||||
@@ -231,12 +238,26 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
shiftPressedRef.current = true
|
shiftPressedRef.current = true
|
||||||
}
|
}
|
||||||
|
if (event.key === 'Alt') {
|
||||||
|
altPressedRef.current = true
|
||||||
|
setAltPressed(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onKeyUp = (event: KeyboardEvent) => {
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
shiftPressedRef.current = false
|
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)
|
emitter.on('grid:move', onGridMove)
|
||||||
@@ -244,6 +265,7 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
emitter.on('tool:cancel', onCancel)
|
emitter.on('tool:cancel', onCancel)
|
||||||
window.addEventListener('keydown', onKeyDown)
|
window.addEventListener('keydown', onKeyDown)
|
||||||
window.addEventListener('keyup', onKeyUp)
|
window.addEventListener('keyup', onKeyUp)
|
||||||
|
window.addEventListener('blur', onWindowBlur)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!wasCommitted) {
|
if (!wasCommitted) {
|
||||||
@@ -255,12 +277,30 @@ export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({
|
|||||||
emitter.off('tool:cancel', onCancel)
|
emitter.off('tool:cancel', onCancel)
|
||||||
window.removeEventListener('keydown', onKeyDown)
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
window.removeEventListener('keyup', onKeyUp)
|
window.removeEventListener('keyup', onKeyUp)
|
||||||
|
window.removeEventListener('blur', onWindowBlur)
|
||||||
}
|
}
|
||||||
}, [exitMoveMode, target])
|
}, [exitMoveMode, target])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
<CursorSphere position={cursorLocalPos} showTooltip={false} />
|
<CursorSphere position={cursorLocalPos} showTooltip={false} />
|
||||||
|
<Html
|
||||||
|
position={[cursorLocalPos[0], 0, cursorLocalPos[2]]}
|
||||||
|
style={{ pointerEvents: 'none', touchAction: 'none' }}
|
||||||
|
zIndexRange={[100, 0]}
|
||||||
|
>
|
||||||
|
<div className="translate-y-10">
|
||||||
|
<div
|
||||||
|
className={`whitespace-nowrap rounded-full border px-2 py-1 text-[11px] font-medium shadow-lg backdrop-blur-md transition-colors ${
|
||||||
|
altPressed
|
||||||
|
? 'border-amber-500/80 bg-amber-500/15 text-amber-100'
|
||||||
|
: 'border-border bg-background/95 text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{altPressed ? 'Detaching corner' : 'Alt to detach'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Html>
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user