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:click', onGridClick)
|
||||
emitter.on('grid:double-click', onGridDoubleClick)
|
||||
emitter.on('tool:cancel', onCancel)
|
||||
|
||||
return () => {
|
||||
emitter.off('grid:move', onGridMove)
|
||||
emitter.off('grid:click', onGridClick)
|
||||
emitter.off('grid:double-click', onGridDoubleClick)
|
||||
emitter.off('tool:cancel', onCancel)
|
||||
}
|
||||
}, [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 { useCallback, useMemo, useRef } from 'react'
|
||||
import type { Vector3 } from 'three'
|
||||
import type { AssetInput } from '@pascal-app/core'
|
||||
import { stripTransient } from './placement-math'
|
||||
|
||||
interface OriginalState {
|
||||
@@ -159,14 +158,25 @@ export function useDraftNode(): DraftNodeHandle {
|
||||
if (adoptedRef.current && originalStateRef.current) {
|
||||
// Move mode: restore original state instead of deleting
|
||||
const original = originalStateRef.current
|
||||
const id = draftRef.current.id
|
||||
|
||||
useScene.getState().updateNode(draftRef.current.id, {
|
||||
useScene.getState().updateNode(id, {
|
||||
position: original.position,
|
||||
rotation: original.rotation,
|
||||
side: original.side,
|
||||
parentId: original.parentId,
|
||||
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 {
|
||||
// Create mode: delete the transient node
|
||||
useScene.getState().deleteNode(draftRef.current.id)
|
||||
|
||||
@@ -474,13 +474,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
||||
|
||||
const ROTATION_STEP = Math.PI / 2
|
||||
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
|
||||
if (!draft) return
|
||||
|
||||
@@ -504,6 +497,14 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
||||
}
|
||||
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 ----
|
||||
const onContextMenu = (event: MouseEvent) => {
|
||||
if (configRef.current.onCancel) {
|
||||
@@ -547,6 +548,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
||||
emitter.off('ceiling:move', onCeilingMove)
|
||||
emitter.off('ceiling:click', onCeilingClick)
|
||||
emitter.off('ceiling:leave', onCeilingLeave)
|
||||
emitter.off('tool:cancel', onCancel)
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
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
|
||||
emitter.on("grid:move", onGridMove);
|
||||
emitter.on("grid:click", onGridClick);
|
||||
emitter.on("tool:cancel", onCancel);
|
||||
|
||||
return () => {
|
||||
emitter.off("grid:move", onGridMove);
|
||||
emitter.off("grid:click", onGridClick);
|
||||
emitter.off("tool:cancel", onCancel);
|
||||
|
||||
// Reset state on unmount
|
||||
corner1Ref.current = null;
|
||||
|
||||
@@ -138,14 +138,20 @@ export const SlabTool: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setPoints([])
|
||||
}
|
||||
|
||||
emitter.on('grid:move', onGridMove)
|
||||
emitter.on('grid:click', onGridClick)
|
||||
emitter.on('grid:double-click', onGridDoubleClick)
|
||||
emitter.on('tool:cancel', onCancel)
|
||||
|
||||
return () => {
|
||||
emitter.off('grid:move', onGridMove)
|
||||
emitter.off('grid:click', onGridClick)
|
||||
emitter.off('grid:double-click', onGridDoubleClick)
|
||||
emitter.off('tool:cancel', onCancel)
|
||||
}
|
||||
}, [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:click', onGridClick)
|
||||
emitter.on('tool:cancel', onCancel)
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
window.addEventListener('keyup', onKeyUp)
|
||||
|
||||
return () => {
|
||||
emitter.off('grid:move', onGridMove)
|
||||
emitter.off('grid:click', onGridClick)
|
||||
emitter.off('tool:cancel', onCancel)
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
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'
|
||||
|
||||
import useEditor from '@/store/use-editor'
|
||||
import { CeilingHelper } from './ceiling-helper'
|
||||
import { ItemHelper } from './item-helper'
|
||||
import { RoofHelper } from './roof-helper'
|
||||
import { SlabHelper } from './slab-helper'
|
||||
import { WallHelper } from './wall-helper'
|
||||
|
||||
export function HelperManager() {
|
||||
@@ -9,7 +12,7 @@ export function HelperManager() {
|
||||
const movingNode = useEditor((state) => state.movingNode)
|
||||
|
||||
if (movingNode) {
|
||||
return <ItemHelper />
|
||||
return <ItemHelper showEsc />
|
||||
}
|
||||
|
||||
// Show appropriate helper based on current tool
|
||||
@@ -18,6 +21,12 @@ export function HelperManager() {
|
||||
return <WallHelper />
|
||||
case 'item':
|
||||
return <ItemHelper />
|
||||
case 'slab':
|
||||
return <SlabHelper />
|
||||
case 'ceiling':
|
||||
return <CeilingHelper />
|
||||
case 'roof':
|
||||
return <RoofHelper />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export function ItemHelper() {
|
||||
interface ItemHelperProps {
|
||||
showEsc?: boolean
|
||||
}
|
||||
|
||||
export function ItemHelper({ showEsc }: ItemHelperProps) {
|
||||
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">
|
||||
@@ -9,10 +13,12 @@ export function ItemHelper() {
|
||||
<kbd className="rounded bg-muted px-2 py-1 text-xs font-medium">T</kbd>
|
||||
<span className="text-muted-foreground">Rotate clockwise</span>
|
||||
</div>
|
||||
{showEsc && (
|
||||
<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 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() {
|
||||
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">
|
||||
<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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 { useEffect } from 'react'
|
||||
import useEditor from '@/store/use-editor'
|
||||
import { sfxEmitter } from '@/lib/sfx-bus'
|
||||
import useEditor from '@/store/use-editor'
|
||||
|
||||
export const useKeyboard = () => {
|
||||
useEffect(() => {
|
||||
@@ -14,13 +14,7 @@ export const useKeyboard = () => {
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
// Emit tool:cancel event - each tool handles its own cancellation logic
|
||||
// if (useEditor.getState().controlMode === 'building') {
|
||||
// emitter.emit('tool:cancel', undefined)
|
||||
// }
|
||||
// if (selectedNodeIds.length > 0) {
|
||||
// handleClear()
|
||||
// }
|
||||
emitter.emit('tool:cancel')
|
||||
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
useEditor.getState().setPhase('site')
|
||||
@@ -33,7 +27,8 @@ export const useKeyboard = () => {
|
||||
e.preventDefault()
|
||||
useEditor.getState().setPhase('furnish')
|
||||
useEditor.getState().setMode('select')
|
||||
} if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
||||
}
|
||||
if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
useEditor.getState().setMode('select')
|
||||
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
||||
|
||||
@@ -67,6 +67,10 @@ type CameraControlEvents = {
|
||||
'camera-controls:generate-thumbnail': ThumbnailGenerateEvent
|
||||
}
|
||||
|
||||
type ToolEvents = {
|
||||
'tool:cancel': undefined
|
||||
}
|
||||
|
||||
type EditorEvents = GridEvents &
|
||||
NodeEvents<'wall', WallEvent> &
|
||||
NodeEvents<'item', ItemEvent> &
|
||||
@@ -77,6 +81,7 @@ type EditorEvents = GridEvents &
|
||||
NodeEvents<'slab', SlabEvent> &
|
||||
NodeEvents<'ceiling', CeilingEvent> &
|
||||
NodeEvents<'roof', RoofEvent> &
|
||||
CameraControlEvents
|
||||
CameraControlEvents &
|
||||
ToolEvents
|
||||
|
||||
export const emitter = mitt<EditorEvents>()
|
||||
|
||||
Reference in New Issue
Block a user