esc key cancel tool

This commit is contained in:
wass08
2026-02-17 12:29:00 +09:00
parent 078b2500a3
commit d833245fbf
14 changed files with 121 additions and 29 deletions
@@ -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>
)
}
+5 -10
View File
@@ -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)) {
+6 -1
View File
@@ -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>()