From ae3af4318e6003de394fdf7063379f2dfb47ad90 Mon Sep 17 00:00:00 2001 From: wass08 Date: Thu, 12 Feb 2026 10:17:22 +0900 Subject: [PATCH] better placing tools --- apps/editor/components/editor/index.tsx | 2 + .../tools/item/use-placement-coordinator.tsx | 100 +++++++++++++----- .../components/tools/wall/wall-tool.tsx | 27 ++++- .../components/ui/helpers/helper-manager.tsx | 24 +++++ .../components/ui/helpers/item-helper.tsx | 18 ++++ .../components/ui/helpers/wall-helper.tsx | 10 ++ .../components/ui/panels/item-panel.tsx | 3 + apps/editor/hooks/use-keyboard.ts | 13 +++ apps/editor/lib/sfx-bus.ts | 2 + apps/editor/lib/sfx-player.ts | 1 + apps/editor/public/audios/sfx/item_rotate.mp3 | Bin 0 -> 8424 bytes apps/editor/store/use-audio.tsx | 2 +- 12 files changed, 169 insertions(+), 33 deletions(-) create mode 100644 apps/editor/components/ui/helpers/helper-manager.tsx create mode 100644 apps/editor/components/ui/helpers/item-helper.tsx create mode 100644 apps/editor/components/ui/helpers/wall-helper.tsx create mode 100644 apps/editor/public/audios/sfx/item_rotate.mp3 diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index e0280f34..e27abda6 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -11,6 +11,7 @@ import { ActionMenu } from '../ui/action-menu' import { CloudSaveButton } from '@/features/community/components/cloud-save-button' import { PascalRadio } from '../pascal-radio' import { PanelManager } from '../ui/panels/panel-manager' +import { HelperManager } from '../ui/helpers/helper-manager' import { SidebarProvider } from '../ui/primitives/sidebar' import { AppSidebar } from '../ui/sidebar/app-sidebar' import { CustomCameraControls } from './custom-camera-controls' @@ -34,6 +35,7 @@ export default function Editor() {
+ {/* Top-right controls */}
diff --git a/apps/editor/components/tools/item/use-placement-coordinator.tsx b/apps/editor/components/tools/item/use-placement-coordinator.tsx index 457b4fdf..056e465b 100644 --- a/apps/editor/components/tools/item/use-placement-coordinator.tsx +++ b/apps/editor/components/tools/item/use-placement-coordinator.tsx @@ -17,12 +17,17 @@ import { useFrame } from '@react-three/fiber' import { useEffect, useRef } from 'react' import { BoxGeometry, + EdgesGeometry, Euler, + type Group, + type LineSegments, type Mesh, - type MeshStandardMaterial, + PlaneGeometry, Quaternion, Vector3, } from 'three' +import { distance, smoothstep, uv, vec2 } from 'three/tsl' +import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu' import { sfxEmitter } from '@/lib/sfx-bus' import { ceilingStrategy, checkCanPlace, floorStrategy, wallStrategy } from './placement-strategies' import type { PlacementState, TransitionResult } from './placement-types' @@ -30,6 +35,28 @@ import type { DraftNodeHandle } from './use-draft-node' const DEFAULT_DIMENSIONS: [number, number, number] = [1, 1, 1] +// Shared materials for placement cursor - we just change colors, not swap materials +// Note: EdgesGeometry doesn't work with dashed lines, so using solid lines +const edgeMaterial = new LineBasicNodeMaterial({ + color: 0xef4444, // red-500 (invalid) + linewidth: 3, + depthTest: false, + depthWrite: false, +}) + +const basePlaneMaterial = new MeshBasicNodeMaterial({ + color: 0xef4444, // red-500 (invalid) + transparent: true, + depthTest: false, + depthWrite: false, +}) + +// Create radial opacity: transparent in center, opaque at edges +const center = vec2(0.5, 0.5) +const dist = distance(uv(), center) +const radialOpacity = smoothstep(0, 0.7, dist).mul(0.6) +basePlaneMaterial.opacityNode = radialOpacity + export interface PlacementCoordinatorConfig { asset: AssetInput draftNode: DraftNodeHandle @@ -40,7 +67,9 @@ export interface PlacementCoordinatorConfig { } export function usePlacementCoordinator(config: PlacementCoordinatorConfig): React.ReactNode { - const cursorRef = useRef(null!) + const cursorGroupRef = useRef(null!) + const edgesRef = useRef(null!) + const basePlaneRef = useRef(null!) const gridPosition = useRef(new Vector3(0, 0, 0)) const placementState = useRef( config.initialState ?? { surface: 'floor', wallId: null, ceilingId: null }, @@ -77,7 +106,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea const revalidate = (): boolean => { const placeable = checkCanPlace(getContext(), validators) - ;(cursorRef.current.material as MeshStandardMaterial).color.set(placeable ? 'green' : 'red') + const color = placeable ? 0x22c55e : 0xef4444 // green-500 : red-500 + edgeMaterial.color.setHex(color) + basePlaneMaterial.color.setHex(color) return placeable } @@ -85,8 +116,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea Object.assign(placementState.current, result.stateUpdate) gridPosition.current.set(...result.gridPosition) - cursorRef.current.position.set(...result.cursorPosition) - cursorRef.current.rotation.y = result.cursorRotationY + cursorGroupRef.current.position.set(...result.cursorPosition) + cursorGroupRef.current.rotation.y = result.cursorRotationY const draft = draftNode.current if (draft) { @@ -98,8 +129,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea const ensureDraft = (result: TransitionResult) => { gridPosition.current.set(...result.gridPosition) - cursorRef.current.position.set(...result.cursorPosition) - cursorRef.current.rotation.y = result.cursorRotationY + cursorGroupRef.current.position.set(...result.cursorPosition) + cursorGroupRef.current.rotation.y = result.cursorRotationY draftNode.create(gridPosition.current, asset, [0, result.cursorRotationY, 0]) @@ -122,14 +153,14 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea if (draftNode.current) { const mesh = sceneRegistry.nodes.get(draftNode.current.id) if (mesh) { - mesh.getWorldPosition(cursorRef.current.position) + mesh.getWorldPosition(cursorGroupRef.current.position) // Extract world Y rotation (handles wall-parented items correctly) const q = new Quaternion() mesh.getWorldQuaternion(q) - cursorRef.current.rotation.y = new Euler().setFromQuaternion(q, 'YXZ').y + cursorGroupRef.current.rotation.y = new Euler().setFromQuaternion(q, 'YXZ').y } else { - cursorRef.current.position.copy(gridPosition.current) - cursorRef.current.rotation.y = draftNode.current.rotation[1] ?? 0 + cursorGroupRef.current.position.copy(gridPosition.current) + cursorGroupRef.current.rotation.y = draftNode.current.rotation[1] ?? 0 } } @@ -144,17 +175,19 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea if (!result) return // Play snap sound when grid position changes - if (previousGridPos && - (result.gridPosition[0] !== previousGridPos[0] || - result.gridPosition[2] !== previousGridPos[2])) { + if ( + previousGridPos && + (result.gridPosition[0] !== previousGridPos[0] || + result.gridPosition[2] !== previousGridPos[2]) + ) { sfxEmitter.emit('sfx:grid-snap') } previousGridPos = [...result.gridPosition] gridPosition.current.set(...result.gridPosition) // Only update X and Z for cursor - useFrame will handle Y (slab elevation) - cursorRef.current.position.x = result.cursorPosition[0] - cursorRef.current.position.z = result.cursorPosition[2] + cursorGroupRef.current.position.x = result.cursorPosition[0] + cursorGroupRef.current.position.z = result.cursorPosition[2] const draft = draftNode.current if (draft) draft.position = result.gridPosition @@ -167,7 +200,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea if (!result) return // Preserve cursor rotation for the next draft - const currentRotation: [number, number, number] = [0, cursorRef.current.rotation.y, 0] + const currentRotation: [number, number, number] = [0, cursorGroupRef.current.rotation.y, 0] draftNode.commit(result.nodeUpdate) if (configRef.current.onCommitted()) { @@ -237,8 +270,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea gridPosition.current.z !== result.gridPosition[2] gridPosition.current.set(...result.gridPosition) - cursorRef.current.position.set(...result.cursorPosition) - cursorRef.current.rotation.y = result.cursorRotationY + cursorGroupRef.current.position.set(...result.cursorPosition) + cursorGroupRef.current.rotation.y = result.cursorRotationY const draft = draftNode.current if (draft && result.nodeUpdate) { @@ -361,7 +394,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea event.stopPropagation() gridPosition.current.set(...result.gridPosition) - cursorRef.current.position.set(...result.cursorPosition) + cursorGroupRef.current.position.set(...result.cursorPosition) revalidate() @@ -441,12 +474,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea if (rotationDelta !== 0) { event.preventDefault() + sfxEmitter.emit('sfx:item-rotate') const currentRotation = draft.rotation const newRotationY = (currentRotation[1] ?? 0) + rotationDelta draft.rotation = [currentRotation[0], newRotationY, currentRotation[2]] // Ref + cursor mesh + item mesh — no store update during drag - cursorRef.current.rotation.y = newRotationY + cursorGroupRef.current.rotation.y = newRotationY const mesh = sceneRegistry.nodes.get(draft.id) if (mesh) mesh.rotation.y = newRotationY revalidate() @@ -468,7 +502,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea const dims = asset.dimensions ?? DEFAULT_DIMENSIONS const boxGeometry = new BoxGeometry(dims[0], dims[1], dims[2]) boxGeometry.translate(0, dims[1] / 2, 0) - cursorRef.current.geometry = boxGeometry + const edgesGeometry = new EdgesGeometry(boxGeometry) + edgesRef.current.geometry = edgesGeometry // ---- Subscribe ---- @@ -532,17 +567,26 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea draftNode.current.rotation, ) mesh.position.y = slabElevation - cursorRef.current.position.y = slabElevation + cursorGroupRef.current.position.y = slabElevation } } }) + const dims = config.asset.dimensions ?? DEFAULT_DIMENSIONS + const initialBoxGeometry = new BoxGeometry(dims[0], dims[1], dims[2]) + initialBoxGeometry.translate(0, dims[1] / 2, 0) + + // Base plane geometry (colored rectangle on the ground) + const basePlaneGeometry = new PlaneGeometry(dims[0], dims[2]) + basePlaneGeometry.rotateX(-Math.PI / 2) // Make it horizontal + basePlaneGeometry.translate(0, 0.01, 0) // Slightly above ground to avoid z-fighting + return ( - - - - - + + + + + ) } diff --git a/apps/editor/components/tools/wall/wall-tool.tsx b/apps/editor/components/tools/wall/wall-tool.tsx index adf08431..12c19860 100644 --- a/apps/editor/components/tools/wall/wall-tool.tsx +++ b/apps/editor/components/tools/wall/wall-tool.tsx @@ -98,6 +98,7 @@ export const WallTool: React.FC = () => { const startingPoint = useRef(new Vector3(0, 0, 0)) const endingPoint = useRef(new Vector3(0, 0, 0)) const buildingState = useRef(0) + const shiftPressed = useRef(false) useEffect(() => { let gridPosition: [number, number] = [0, 0] @@ -111,8 +112,10 @@ export const WallTool: React.FC = () => { cursorRef.current.position.set(gridPosition[0], event.position[1], gridPosition[1]) if (buildingState.current === 1) { - // Snap to 45° angles - const snapped = snapTo45Degrees(startingPoint.current, cursorPosition) + // Snap to 45° angles only if shift is not pressed + const snapped = shiftPressed.current + ? cursorPosition + : snapTo45Degrees(startingPoint.current, cursorPosition) endingPoint.current.copy(snapped) // Play snap sound only when the actual wall end position changes @@ -143,12 +146,28 @@ export const WallTool: React.FC = () => { } } + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Shift') { + shiftPressed.current = true + } + } + + const onKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Shift') { + shiftPressed.current = false + } + } + emitter.on('grid:move', onGridMove) emitter.on('grid:click', onGridClick) + window.addEventListener('keydown', onKeyDown) + window.addEventListener('keyup', onKeyUp) return () => { emitter.off('grid:move', onGridMove) emitter.off('grid:click', onGridClick) + window.removeEventListener('keydown', onKeyDown) + window.removeEventListener('keyup', onKeyUp) } }, []) @@ -156,8 +175,8 @@ export const WallTool: React.FC = () => { {/* Cursor indicator */} - - + + {/* Wall preview */} diff --git a/apps/editor/components/ui/helpers/helper-manager.tsx b/apps/editor/components/ui/helpers/helper-manager.tsx new file mode 100644 index 00000000..6db3e4ac --- /dev/null +++ b/apps/editor/components/ui/helpers/helper-manager.tsx @@ -0,0 +1,24 @@ +'use client' + +import useEditor from '@/store/use-editor' +import { ItemHelper } from './item-helper' +import { WallHelper } from './wall-helper' + +export function HelperManager() { + const tool = useEditor((s) => s.tool) + const movingNode = useEditor((state) => state.movingNode) + + if (movingNode) { + return + } + + // Show appropriate helper based on current tool + switch (tool) { + case 'wall': + return + case 'item': + return + default: + return null + } +} diff --git a/apps/editor/components/ui/helpers/item-helper.tsx b/apps/editor/components/ui/helpers/item-helper.tsx new file mode 100644 index 00000000..91345b4d --- /dev/null +++ b/apps/editor/components/ui/helpers/item-helper.tsx @@ -0,0 +1,18 @@ +export function ItemHelper() { + return ( +
+
+ R + Rotate counterclockwise +
+
+ T + Rotate clockwise +
+
+ Esc + Cancel +
+
+ ) +} diff --git a/apps/editor/components/ui/helpers/wall-helper.tsx b/apps/editor/components/ui/helpers/wall-helper.tsx new file mode 100644 index 00000000..7e880e80 --- /dev/null +++ b/apps/editor/components/ui/helpers/wall-helper.tsx @@ -0,0 +1,10 @@ +export function WallHelper() { + return ( +
+
+ Shift + Allow non-45° angles +
+
+ ) +} diff --git a/apps/editor/components/ui/panels/item-panel.tsx b/apps/editor/components/ui/panels/item-panel.tsx index 79ee38e5..e9360923 100644 --- a/apps/editor/components/ui/panels/item-panel.tsx +++ b/apps/editor/components/ui/panels/item-panel.tsx @@ -53,6 +53,7 @@ export function ItemPanel() { const handleDelete = useCallback(() => { if (!selectedId) return + sfxEmitter.emit('sfx:item-delete') deleteNode(selectedId as AnyNode['id']) setSelection({ selectedIds: [] }) }, [selectedId, deleteNode, setSelection]) @@ -144,6 +145,7 @@ export function ItemPanel() { type="button" className="flex-1 rounded border border-border px-2 py-1.5 text-xs hover:bg-accent cursor-pointer" onClick={() => { + sfxEmitter.emit('sfx:item-rotate') const currentDegrees = (node.rotation[1] * 180) / Math.PI const newDegrees = currentDegrees - 90 const radians = (newDegrees * Math.PI) / 180 @@ -156,6 +158,7 @@ export function ItemPanel() { type="button" className="flex-1 rounded border border-border px-2 py-1.5 text-xs hover:bg-accent cursor-pointer" onClick={() => { + sfxEmitter.emit('sfx:item-rotate') const currentDegrees = (node.rotation[1] * 180) / Math.PI const newDegrees = currentDegrees + 90 const radians = (newDegrees * Math.PI) / 180 diff --git a/apps/editor/hooks/use-keyboard.ts b/apps/editor/hooks/use-keyboard.ts index 4026277f..ce8ee20f 100644 --- a/apps/editor/hooks/use-keyboard.ts +++ b/apps/editor/hooks/use-keyboard.ts @@ -2,6 +2,7 @@ import { type AnyNodeId, 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' export const useKeyboard = () => { useEffect(() => { @@ -47,6 +48,18 @@ export const useKeyboard = () => { const selectedNodeIds = useViewer.getState().selection.selectedIds as AnyNodeId[] if (selectedNodeIds.length > 0) { + // Play appropriate SFX based on what's being deleted + if (selectedNodeIds.length === 1) { + const node = useScene.getState().nodes[selectedNodeIds[0]!] + if (node?.type === 'item') { + sfxEmitter.emit('sfx:item-delete') + } else { + sfxEmitter.emit('sfx:structure-delete') + } + } else { + sfxEmitter.emit('sfx:structure-delete') + } + useScene.getState().deleteNodes(selectedNodeIds) } } diff --git a/apps/editor/lib/sfx-bus.ts b/apps/editor/lib/sfx-bus.ts index 9bc74fe2..308718d2 100644 --- a/apps/editor/lib/sfx-bus.ts +++ b/apps/editor/lib/sfx-bus.ts @@ -9,6 +9,7 @@ type SFXEvents = { 'sfx:item-delete': undefined 'sfx:item-pick': undefined 'sfx:item-place': undefined + 'sfx:item-rotate': undefined 'sfx:structure-build': undefined 'sfx:structure-delete': undefined } @@ -29,6 +30,7 @@ export function initSFXBus() { sfxEmitter.on('sfx:item-delete', () => playSFX('itemDelete')) sfxEmitter.on('sfx:item-pick', () => playSFX('itemPick')) sfxEmitter.on('sfx:item-place', () => playSFX('itemPlace')) + sfxEmitter.on('sfx:item-rotate', () => playSFX('itemRotate')) sfxEmitter.on('sfx:structure-build', () => playSFX('structureBuild')) sfxEmitter.on('sfx:structure-delete', () => playSFX('structureDelete')) } diff --git a/apps/editor/lib/sfx-player.ts b/apps/editor/lib/sfx-player.ts index 402eb446..f305893e 100644 --- a/apps/editor/lib/sfx-player.ts +++ b/apps/editor/lib/sfx-player.ts @@ -7,6 +7,7 @@ export const SFX = { itemDelete: '/audios/sfx/item_delete.mp3', itemPick: '/audios/sfx/item_pick.mp3', itemPlace: '/audios/sfx/item_place.mp3', + itemRotate: '/audios/sfx/item_rotate.mp3', structureBuild: '/audios/sfx/structure_build.mp3', structureDelete: '/audios/sfx/structure_delete.mp3', } as const diff --git a/apps/editor/public/audios/sfx/item_rotate.mp3 b/apps/editor/public/audios/sfx/item_rotate.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e5c94a6ae0c1eaef005fdd2f8839c71d889c305f GIT binary patch literal 8424 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 literal 0 HcmV?d00001 diff --git a/apps/editor/store/use-audio.tsx b/apps/editor/store/use-audio.tsx index 6f8759b1..4f43964d 100644 --- a/apps/editor/store/use-audio.tsx +++ b/apps/editor/store/use-audio.tsx @@ -21,7 +21,7 @@ const useAudio = create()( (set) => ({ masterVolume: 70, sfxVolume: 50, - radioVolume: 50, + radioVolume: 25, muted: false, autoplay: true, setMasterVolume: (v) => set({ masterVolume: v }),