esc key cancel tool
This commit is contained in:
@@ -144,14 +144,20 @@ export const CeilingTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setPoints([])
|
||||||
|
}
|
||||||
|
|
||||||
emitter.on('grid:move', onGridMove)
|
emitter.on('grid:move', onGridMove)
|
||||||
emitter.on('grid:click', onGridClick)
|
emitter.on('grid:click', onGridClick)
|
||||||
emitter.on('grid:double-click', onGridDoubleClick)
|
emitter.on('grid:double-click', onGridDoubleClick)
|
||||||
|
emitter.on('tool:cancel', onCancel)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
emitter.off('grid:move', onGridMove)
|
emitter.off('grid:move', onGridMove)
|
||||||
emitter.off('grid:click', onGridClick)
|
emitter.off('grid:click', onGridClick)
|
||||||
emitter.off('grid:double-click', onGridDoubleClick)
|
emitter.off('grid:double-click', onGridDoubleClick)
|
||||||
|
emitter.off('tool:cancel', onCancel)
|
||||||
}
|
}
|
||||||
}, [currentLevelId, points, cursorPosition, setTool])
|
}, [currentLevelId, points, cursorPosition, setTool])
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { type AnyNodeId, ItemNode, useScene } from '@pascal-app/core'
|
import { type AnyNodeId, type AssetInput, ItemNode, sceneRegistry, useScene } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback, useMemo, useRef } from 'react'
|
import { useCallback, useMemo, useRef } from 'react'
|
||||||
import type { Vector3 } from 'three'
|
import type { Vector3 } from 'three'
|
||||||
import type { AssetInput } from '@pascal-app/core'
|
|
||||||
import { stripTransient } from './placement-math'
|
import { stripTransient } from './placement-math'
|
||||||
|
|
||||||
interface OriginalState {
|
interface OriginalState {
|
||||||
@@ -159,14 +158,25 @@ export function useDraftNode(): DraftNodeHandle {
|
|||||||
if (adoptedRef.current && originalStateRef.current) {
|
if (adoptedRef.current && originalStateRef.current) {
|
||||||
// Move mode: restore original state instead of deleting
|
// Move mode: restore original state instead of deleting
|
||||||
const original = originalStateRef.current
|
const original = originalStateRef.current
|
||||||
|
const id = draftRef.current.id
|
||||||
|
|
||||||
useScene.getState().updateNode(draftRef.current.id, {
|
useScene.getState().updateNode(id, {
|
||||||
position: original.position,
|
position: original.position,
|
||||||
rotation: original.rotation,
|
rotation: original.rotation,
|
||||||
side: original.side,
|
side: original.side,
|
||||||
parentId: original.parentId,
|
parentId: original.parentId,
|
||||||
metadata: original.metadata,
|
metadata: original.metadata,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Also reset the Three.js mesh directly — the store update triggers a React
|
||||||
|
// re-render but the mesh position was mutated by useFrame and may not reset
|
||||||
|
// until the next render cycle, leaving a visual glitch.
|
||||||
|
const mesh = sceneRegistry.nodes.get(id as AnyNodeId)
|
||||||
|
if (mesh) {
|
||||||
|
mesh.position.set(original.position[0], original.position[1], original.position[2])
|
||||||
|
mesh.rotation.y = original.rotation[1] ?? 0
|
||||||
|
mesh.visible = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create mode: delete the transient node
|
// Create mode: delete the transient node
|
||||||
useScene.getState().deleteNode(draftRef.current.id)
|
useScene.getState().deleteNode(draftRef.current.id)
|
||||||
|
|||||||
@@ -474,13 +474,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
|
|
||||||
const ROTATION_STEP = Math.PI / 2
|
const ROTATION_STEP = Math.PI / 2
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
// Escape / right-click → cancel
|
|
||||||
if (event.key === 'Escape' && configRef.current.onCancel) {
|
|
||||||
event.preventDefault()
|
|
||||||
configRef.current.onCancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const draft = draftNode.current
|
const draft = draftNode.current
|
||||||
if (!draft) return
|
if (!draft) return
|
||||||
|
|
||||||
@@ -504,6 +497,14 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
}
|
}
|
||||||
window.addEventListener('keydown', onKeyDown)
|
window.addEventListener('keydown', onKeyDown)
|
||||||
|
|
||||||
|
// ---- tool:cancel (Escape / programmatic) ----
|
||||||
|
const onCancel = () => {
|
||||||
|
if (configRef.current.onCancel) {
|
||||||
|
configRef.current.onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitter.on('tool:cancel', onCancel)
|
||||||
|
|
||||||
// ---- Right-click cancel ----
|
// ---- Right-click cancel ----
|
||||||
const onContextMenu = (event: MouseEvent) => {
|
const onContextMenu = (event: MouseEvent) => {
|
||||||
if (configRef.current.onCancel) {
|
if (configRef.current.onCancel) {
|
||||||
@@ -547,6 +548,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
emitter.off('ceiling:move', onCeilingMove)
|
emitter.off('ceiling:move', onCeilingMove)
|
||||||
emitter.off('ceiling:click', onCeilingClick)
|
emitter.off('ceiling:click', onCeilingClick)
|
||||||
emitter.off('ceiling:leave', onCeilingLeave)
|
emitter.off('ceiling:leave', onCeilingLeave)
|
||||||
|
emitter.off('tool:cancel', onCancel)
|
||||||
window.removeEventListener('keydown', onKeyDown)
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
window.removeEventListener('contextmenu', onContextMenu)
|
window.removeEventListener('contextmenu', onContextMenu)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,13 +151,23 @@ export const RoofTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
if (corner1Ref.current) {
|
||||||
|
corner1Ref.current = null;
|
||||||
|
outlineRef.current.visible = false;
|
||||||
|
setPreview((prev) => ({ ...prev, corner1: null }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Subscribe to events
|
// Subscribe to events
|
||||||
emitter.on("grid:move", onGridMove);
|
emitter.on("grid:move", onGridMove);
|
||||||
emitter.on("grid:click", onGridClick);
|
emitter.on("grid:click", onGridClick);
|
||||||
|
emitter.on("tool:cancel", onCancel);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
emitter.off("grid:move", onGridMove);
|
emitter.off("grid:move", onGridMove);
|
||||||
emitter.off("grid:click", onGridClick);
|
emitter.off("grid:click", onGridClick);
|
||||||
|
emitter.off("tool:cancel", onCancel);
|
||||||
|
|
||||||
// Reset state on unmount
|
// Reset state on unmount
|
||||||
corner1Ref.current = null;
|
corner1Ref.current = null;
|
||||||
|
|||||||
@@ -138,14 +138,20 @@ export const SlabTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setPoints([])
|
||||||
|
}
|
||||||
|
|
||||||
emitter.on('grid:move', onGridMove)
|
emitter.on('grid:move', onGridMove)
|
||||||
emitter.on('grid:click', onGridClick)
|
emitter.on('grid:click', onGridClick)
|
||||||
emitter.on('grid:double-click', onGridDoubleClick)
|
emitter.on('grid:double-click', onGridDoubleClick)
|
||||||
|
emitter.on('tool:cancel', onCancel)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
emitter.off('grid:move', onGridMove)
|
emitter.off('grid:move', onGridMove)
|
||||||
emitter.off('grid:click', onGridClick)
|
emitter.off('grid:click', onGridClick)
|
||||||
emitter.off('grid:double-click', onGridDoubleClick)
|
emitter.off('grid:double-click', onGridDoubleClick)
|
||||||
|
emitter.off('tool:cancel', onCancel)
|
||||||
}
|
}
|
||||||
}, [currentLevelId, points, cursorPosition, setSelection])
|
}, [currentLevelId, points, cursorPosition, setSelection])
|
||||||
|
|
||||||
|
|||||||
@@ -158,14 +158,23 @@ export const WallTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
if (buildingState.current === 1) {
|
||||||
|
buildingState.current = 0
|
||||||
|
wallPreviewRef.current.visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emitter.on('grid:move', onGridMove)
|
emitter.on('grid:move', onGridMove)
|
||||||
emitter.on('grid:click', onGridClick)
|
emitter.on('grid:click', onGridClick)
|
||||||
|
emitter.on('tool:cancel', onCancel)
|
||||||
window.addEventListener('keydown', onKeyDown)
|
window.addEventListener('keydown', onKeyDown)
|
||||||
window.addEventListener('keyup', onKeyUp)
|
window.addEventListener('keyup', onKeyUp)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
emitter.off('grid:move', onGridMove)
|
emitter.off('grid:move', onGridMove)
|
||||||
emitter.off('grid:click', onGridClick)
|
emitter.off('grid:click', onGridClick)
|
||||||
|
emitter.off('tool:cancel', onCancel)
|
||||||
window.removeEventListener('keydown', onKeyDown)
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
window.removeEventListener('keyup', onKeyUp)
|
window.removeEventListener('keyup', onKeyUp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export function CeilingHelper() {
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Esc</kbd>
|
||||||
|
<span className="text-muted-foreground">Cancel</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import useEditor from '@/store/use-editor'
|
import useEditor from '@/store/use-editor'
|
||||||
|
import { CeilingHelper } from './ceiling-helper'
|
||||||
import { ItemHelper } from './item-helper'
|
import { ItemHelper } from './item-helper'
|
||||||
|
import { RoofHelper } from './roof-helper'
|
||||||
|
import { SlabHelper } from './slab-helper'
|
||||||
import { WallHelper } from './wall-helper'
|
import { WallHelper } from './wall-helper'
|
||||||
|
|
||||||
export function HelperManager() {
|
export function HelperManager() {
|
||||||
@@ -9,7 +12,7 @@ export function HelperManager() {
|
|||||||
const movingNode = useEditor((state) => state.movingNode)
|
const movingNode = useEditor((state) => state.movingNode)
|
||||||
|
|
||||||
if (movingNode) {
|
if (movingNode) {
|
||||||
return <ItemHelper />
|
return <ItemHelper showEsc />
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show appropriate helper based on current tool
|
// Show appropriate helper based on current tool
|
||||||
@@ -18,6 +21,12 @@ export function HelperManager() {
|
|||||||
return <WallHelper />
|
return <WallHelper />
|
||||||
case 'item':
|
case 'item':
|
||||||
return <ItemHelper />
|
return <ItemHelper />
|
||||||
|
case 'slab':
|
||||||
|
return <SlabHelper />
|
||||||
|
case 'ceiling':
|
||||||
|
return <CeilingHelper />
|
||||||
|
case 'roof':
|
||||||
|
return <RoofHelper />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
export function ItemHelper() {
|
interface ItemHelperProps {
|
||||||
|
showEsc?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ItemHelper({ showEsc }: ItemHelperProps) {
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
@@ -9,10 +13,12 @@ export function ItemHelper() {
|
|||||||
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">T</kbd>
|
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">T</kbd>
|
||||||
<span className="text-muted-foreground">Rotate clockwise</span>
|
<span className="text-muted-foreground">Rotate clockwise</span>
|
||||||
</div>
|
</div>
|
||||||
|
{showEsc && (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Esc</kbd>
|
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Esc</kbd>
|
||||||
<span className="text-muted-foreground">Cancel</span>
|
<span className="text-muted-foreground">Cancel</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export function RoofHelper() {
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Esc</kbd>
|
||||||
|
<span className="text-muted-foreground">Cancel</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export function SlabHelper() {
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Esc</kbd>
|
||||||
|
<span className="text-muted-foreground">Cancel</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
export function WallHelper() {
|
export function WallHelper() {
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex items-center gap-2 rounded-lg border border-border bg-background/95 px-4 py-2 shadow-lg backdrop-blur-md">
|
<div className="pointer-events-none fixed right-4 top-1/2 -translate-y-1/2 z-40 flex flex-col gap-2 rounded-lg border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur-md">
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Shift</kbd>
|
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Shift</kbd>
|
||||||
<span className="text-muted-foreground">Allow non-45° angles</span>
|
<span className="text-muted-foreground">Allow non-45° angles</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">Esc</kbd>
|
||||||
|
<span className="text-muted-foreground">Cancel</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import useEditor from '@/store/use-editor'
|
|
||||||
import { sfxEmitter } from '@/lib/sfx-bus'
|
import { sfxEmitter } from '@/lib/sfx-bus'
|
||||||
|
import useEditor from '@/store/use-editor'
|
||||||
|
|
||||||
export const useKeyboard = () => {
|
export const useKeyboard = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -14,13 +14,7 @@ export const useKeyboard = () => {
|
|||||||
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
// Emit tool:cancel event - each tool handles its own cancellation logic
|
emitter.emit('tool:cancel')
|
||||||
// if (useEditor.getState().controlMode === 'building') {
|
|
||||||
// emitter.emit('tool:cancel', undefined)
|
|
||||||
// }
|
|
||||||
// if (selectedNodeIds.length > 0) {
|
|
||||||
// handleClear()
|
|
||||||
// }
|
|
||||||
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useEditor.getState().setPhase('site')
|
useEditor.getState().setPhase('site')
|
||||||
@@ -33,7 +27,8 @@ export const useKeyboard = () => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useEditor.getState().setPhase('furnish')
|
useEditor.getState().setPhase('furnish')
|
||||||
useEditor.getState().setMode('select')
|
useEditor.getState().setMode('select')
|
||||||
} if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
}
|
||||||
|
if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
useEditor.getState().setMode('select')
|
useEditor.getState().setMode('select')
|
||||||
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ type CameraControlEvents = {
|
|||||||
'camera-controls:generate-thumbnail': ThumbnailGenerateEvent
|
'camera-controls:generate-thumbnail': ThumbnailGenerateEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ToolEvents = {
|
||||||
|
'tool:cancel': undefined
|
||||||
|
}
|
||||||
|
|
||||||
type EditorEvents = GridEvents &
|
type EditorEvents = GridEvents &
|
||||||
NodeEvents<'wall', WallEvent> &
|
NodeEvents<'wall', WallEvent> &
|
||||||
NodeEvents<'item', ItemEvent> &
|
NodeEvents<'item', ItemEvent> &
|
||||||
@@ -77,6 +81,7 @@ type EditorEvents = GridEvents &
|
|||||||
NodeEvents<'slab', SlabEvent> &
|
NodeEvents<'slab', SlabEvent> &
|
||||||
NodeEvents<'ceiling', CeilingEvent> &
|
NodeEvents<'ceiling', CeilingEvent> &
|
||||||
NodeEvents<'roof', RoofEvent> &
|
NodeEvents<'roof', RoofEvent> &
|
||||||
CameraControlEvents
|
CameraControlEvents &
|
||||||
|
ToolEvents
|
||||||
|
|
||||||
export const emitter = mitt<EditorEvents>()
|
export const emitter = mitt<EditorEvents>()
|
||||||
|
|||||||
Reference in New Issue
Block a user