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 ( +