From d833245fbf8b4d2b4fb78e9bc47e866b60b49d76 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 17 Feb 2026 12:29:00 +0900 Subject: [PATCH] esc key cancel tool --- .../components/tools/ceiling/ceiling-tool.tsx | 6 ++++++ .../components/tools/item/use-draft-node.ts | 16 +++++++++++++--- .../tools/item/use-placement-coordinator.tsx | 16 +++++++++------- apps/editor/components/tools/roof/roof-tool.tsx | 10 ++++++++++ apps/editor/components/tools/slab/slab-tool.tsx | 6 ++++++ apps/editor/components/tools/wall/wall-tool.tsx | 9 +++++++++ .../components/ui/helpers/ceiling-helper.tsx | 10 ++++++++++ .../components/ui/helpers/helper-manager.tsx | 13 +++++++++++-- .../editor/components/ui/helpers/item-helper.tsx | 16 +++++++++++----- .../editor/components/ui/helpers/roof-helper.tsx | 10 ++++++++++ .../editor/components/ui/helpers/slab-helper.tsx | 10 ++++++++++ .../editor/components/ui/helpers/wall-helper.tsx | 6 +++++- apps/editor/hooks/use-keyboard.ts | 15 +++++---------- packages/core/src/events/bus.ts | 7 ++++++- 14 files changed, 121 insertions(+), 29 deletions(-) create mode 100644 apps/editor/components/ui/helpers/ceiling-helper.tsx create mode 100644 apps/editor/components/ui/helpers/roof-helper.tsx create mode 100644 apps/editor/components/ui/helpers/slab-helper.tsx diff --git a/apps/editor/components/tools/ceiling/ceiling-tool.tsx b/apps/editor/components/tools/ceiling/ceiling-tool.tsx index 0621208d..bab38edc 100644 --- a/apps/editor/components/tools/ceiling/ceiling-tool.tsx +++ b/apps/editor/components/tools/ceiling/ceiling-tool.tsx @@ -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]) diff --git a/apps/editor/components/tools/item/use-draft-node.ts b/apps/editor/components/tools/item/use-draft-node.ts index d6e8ae21..07213229 100644 --- a/apps/editor/components/tools/item/use-draft-node.ts +++ b/apps/editor/components/tools/item/use-draft-node.ts @@ -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) diff --git a/apps/editor/components/tools/item/use-placement-coordinator.tsx b/apps/editor/components/tools/item/use-placement-coordinator.tsx index 10a215df..f6d38f2c 100644 --- a/apps/editor/components/tools/item/use-placement-coordinator.tsx +++ b/apps/editor/components/tools/item/use-placement-coordinator.tsx @@ -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) } diff --git a/apps/editor/components/tools/roof/roof-tool.tsx b/apps/editor/components/tools/roof/roof-tool.tsx index 5c5bb52e..9f351007 100644 --- a/apps/editor/components/tools/roof/roof-tool.tsx +++ b/apps/editor/components/tools/roof/roof-tool.tsx @@ -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; diff --git a/apps/editor/components/tools/slab/slab-tool.tsx b/apps/editor/components/tools/slab/slab-tool.tsx index 3022cb2d..59739d2e 100644 --- a/apps/editor/components/tools/slab/slab-tool.tsx +++ b/apps/editor/components/tools/slab/slab-tool.tsx @@ -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]) diff --git a/apps/editor/components/tools/wall/wall-tool.tsx b/apps/editor/components/tools/wall/wall-tool.tsx index 12c19860..fa51d65b 100644 --- a/apps/editor/components/tools/wall/wall-tool.tsx +++ b/apps/editor/components/tools/wall/wall-tool.tsx @@ -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) } diff --git a/apps/editor/components/ui/helpers/ceiling-helper.tsx b/apps/editor/components/ui/helpers/ceiling-helper.tsx new file mode 100644 index 00000000..8a93ae05 --- /dev/null +++ b/apps/editor/components/ui/helpers/ceiling-helper.tsx @@ -0,0 +1,10 @@ +export function CeilingHelper() { + return ( +
+
+ Esc + Cancel +
+
+ ) +} diff --git a/apps/editor/components/ui/helpers/helper-manager.tsx b/apps/editor/components/ui/helpers/helper-manager.tsx index 6db3e4ac..591689f4 100644 --- a/apps/editor/components/ui/helpers/helper-manager.tsx +++ b/apps/editor/components/ui/helpers/helper-manager.tsx @@ -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,15 +12,21 @@ export function HelperManager() { const movingNode = useEditor((state) => state.movingNode) if (movingNode) { - return + return } - + // Show appropriate helper based on current tool switch (tool) { case 'wall': return case 'item': return + case 'slab': + return + case 'ceiling': + return + case 'roof': + return default: return null } diff --git a/apps/editor/components/ui/helpers/item-helper.tsx b/apps/editor/components/ui/helpers/item-helper.tsx index 91345b4d..2b85af60 100644 --- a/apps/editor/components/ui/helpers/item-helper.tsx +++ b/apps/editor/components/ui/helpers/item-helper.tsx @@ -1,4 +1,8 @@ -export function ItemHelper() { +interface ItemHelperProps { + showEsc?: boolean +} + +export function ItemHelper({ showEsc }: ItemHelperProps) { return (
@@ -9,10 +13,12 @@ export function ItemHelper() { T Rotate clockwise
-
- Esc - Cancel -
+ {showEsc && ( +
+ Esc + Cancel +
+ )}
) } diff --git a/apps/editor/components/ui/helpers/roof-helper.tsx b/apps/editor/components/ui/helpers/roof-helper.tsx new file mode 100644 index 00000000..8571a3da --- /dev/null +++ b/apps/editor/components/ui/helpers/roof-helper.tsx @@ -0,0 +1,10 @@ +export function RoofHelper() { + return ( +
+
+ Esc + Cancel +
+
+ ) +} diff --git a/apps/editor/components/ui/helpers/slab-helper.tsx b/apps/editor/components/ui/helpers/slab-helper.tsx new file mode 100644 index 00000000..135616ca --- /dev/null +++ b/apps/editor/components/ui/helpers/slab-helper.tsx @@ -0,0 +1,10 @@ +export function SlabHelper() { + return ( +
+
+ Esc + Cancel +
+
+ ) +} diff --git a/apps/editor/components/ui/helpers/wall-helper.tsx b/apps/editor/components/ui/helpers/wall-helper.tsx index 7e880e80..dd0339d4 100644 --- a/apps/editor/components/ui/helpers/wall-helper.tsx +++ b/apps/editor/components/ui/helpers/wall-helper.tsx @@ -1,10 +1,14 @@ export function WallHelper() { return ( -
+
Shift Allow non-45° angles
+
+ Esc + Cancel +
) } diff --git a/apps/editor/hooks/use-keyboard.ts b/apps/editor/hooks/use-keyboard.ts index ce8ee20f..88b1c3e2 100644 --- a/apps/editor/hooks/use-keyboard.ts +++ b/apps/editor/hooks/use-keyboard.ts @@ -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)) { diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts index b98155ac..4e72c79b 100644 --- a/packages/core/src/events/bus.ts +++ b/packages/core/src/events/bus.ts @@ -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()