shortcuts and deletion
This commit is contained in:
@@ -9,6 +9,7 @@ import { MathUtils, type Mesh } from 'three'
|
||||
|
||||
import { color, float, fract, fwidth, mix, positionLocal } from 'three/tsl'
|
||||
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
||||
import { useKeyboard } from '@/hooks/use-keyboard'
|
||||
import { ToolManager } from '../tools/tool-manager'
|
||||
import { ActionMenu } from '../ui/action-menu'
|
||||
import { SidebarProvider } from '../ui/primitives/sidebar'
|
||||
@@ -20,6 +21,8 @@ initSpatialGridSync()
|
||||
useScene.getState().loadScene()
|
||||
|
||||
export default function Editor() {
|
||||
useKeyboard()
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
{/* <LevelModeSwitcher /> */}
|
||||
|
||||
@@ -243,6 +243,36 @@ export const ItemTool: React.FC = () => {
|
||||
emitter.on('wall:click', onWallClick)
|
||||
emitter.on('wall:leave', onWallLeave)
|
||||
|
||||
// Keyboard rotation handlers
|
||||
const ROTATION_STEP = Math.PI / 2 // 90 degrees
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (!draftItem.current) return
|
||||
|
||||
let rotationDelta = 0
|
||||
if (event.key === 'r' || event.key === 'R') {
|
||||
rotationDelta = ROTATION_STEP // Counter-clockwise
|
||||
} else if (event.key === 't' || event.key === 'T') {
|
||||
rotationDelta = -ROTATION_STEP // Clockwise
|
||||
}
|
||||
|
||||
if (rotationDelta !== 0) {
|
||||
event.preventDefault()
|
||||
const currentRotation = draftItem.current.rotation
|
||||
const newRotationY = (currentRotation[1] ?? 0) + rotationDelta
|
||||
draftItem.current.rotation = [currentRotation[0], newRotationY, currentRotation[2]]
|
||||
|
||||
useScene.getState().updateNode(draftItem.current.id, {
|
||||
rotation: draftItem.current.rotation,
|
||||
})
|
||||
|
||||
// Update cursor rotation to match
|
||||
cursorRef.current.rotation.y = newRotationY
|
||||
|
||||
checkCanPlace()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
|
||||
const setupBoundingBox = () => {
|
||||
const boxGeometry = new BoxGeometry(
|
||||
selectedItem.dimensions[0],
|
||||
@@ -265,6 +295,7 @@ export const ItemTool: React.FC = () => {
|
||||
emitter.off('wall:leave', onWallLeave)
|
||||
emitter.off('wall:click', onWallClick)
|
||||
emitter.off('wall:move', onWallMove)
|
||||
window.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [selectedItem, canPlaceOnFloor, canPlaceOnWall])
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect } from 'react'
|
||||
import useEditor from '@/store/use-editor'
|
||||
|
||||
export const useKeyboard = () => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Don't handle shortcuts if user is typing in an input
|
||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
// }
|
||||
} else if (e.key === '1' && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
useEditor.getState().setPhase('site')
|
||||
useEditor.getState().setMode('select')
|
||||
} else if (e.key === '2' && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
useEditor.getState().setPhase('structure')
|
||||
useEditor.getState().setMode('select')
|
||||
} else if (e.key === '3' && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
useEditor.getState().setPhase('furnish')
|
||||
useEditor.getState().setMode('select')
|
||||
} if (e.key === 'v' && !e.metaKey && !e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
useEditor.getState().setMode('select')
|
||||
} else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
useScene.temporal.getState().undo()
|
||||
} else if (e.key === 'Z' && e.shiftKey && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
useScene.temporal.getState().redo()
|
||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
e.preventDefault()
|
||||
|
||||
const selectedNodeIds = useViewer.getState().selection.selectedIds
|
||||
|
||||
if (selectedNodeIds.length > 0) {
|
||||
useScene.getState().deleteNodes(selectedNodeIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -2,10 +2,33 @@ import { type AnyNodeId, type ItemNode, useRegistry, useScene } from '@pascal-ap
|
||||
import { Clone } from '@react-three/drei/core/Clone'
|
||||
import { useGLTF } from '@react-three/drei/core/Gltf'
|
||||
import { Suspense, useEffect, useRef } from 'react'
|
||||
import type { Group, Mesh } from 'three'
|
||||
import type { Group, Material, Mesh } from 'three'
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
|
||||
// Shared materials to avoid creating new instances for every mesh
|
||||
const defaultMaterial = new MeshStandardNodeMaterial({
|
||||
color: 0xffffff,
|
||||
roughness: 0.8,
|
||||
metalness: 0,
|
||||
})
|
||||
|
||||
const glassMaterial = new MeshStandardNodeMaterial({
|
||||
name: 'glass',
|
||||
color: 'skyblue',
|
||||
roughness: 0.8,
|
||||
metalness: 0,
|
||||
transparent: true,
|
||||
opacity: 0.25,
|
||||
})
|
||||
|
||||
const getMaterialForOriginal = (original: Material): MeshStandardNodeMaterial => {
|
||||
if (original.name.toLowerCase() === 'glass') {
|
||||
return glassMaterial
|
||||
}
|
||||
return defaultMaterial
|
||||
}
|
||||
|
||||
export const ItemRenderer = ({ node }: { node: ItemNode }) => {
|
||||
const ref = useRef<Group>(null!)
|
||||
|
||||
@@ -37,14 +60,21 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
||||
useEffect(() => {
|
||||
scene.traverse((child) => {
|
||||
if ((child as Mesh).isMesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
// TODO: do it with a shared material
|
||||
child.material = new MeshStandardNodeMaterial({
|
||||
color: 0xffffff,
|
||||
roughness: 0.8,
|
||||
metalness: 0,
|
||||
})
|
||||
const mesh = child as Mesh
|
||||
if (mesh.name === 'cutout') {
|
||||
child.visible = false
|
||||
return
|
||||
}
|
||||
|
||||
mesh.castShadow = true
|
||||
mesh.receiveShadow = true
|
||||
|
||||
// Handle both single material and material array cases
|
||||
if (Array.isArray(mesh.material)) {
|
||||
mesh.material = mesh.material.map((mat) => getMaterialForOriginal(mat))
|
||||
} else {
|
||||
mesh.material = getMaterialForOriginal(mesh.material)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [scene])
|
||||
|
||||
Reference in New Issue
Block a user