draft attach item to ceiling
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
type CeilingEvent,
|
||||||
emitter,
|
emitter,
|
||||||
type GridEvent,
|
type GridEvent,
|
||||||
ItemNode,
|
ItemNode,
|
||||||
@@ -113,6 +114,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
const selectedItem = useEditor((state) => state.selectedItem)
|
const selectedItem = useEditor((state) => state.selectedItem)
|
||||||
const { canPlaceOnFloor, canPlaceOnWall } = useSpatialQuery()
|
const { canPlaceOnFloor, canPlaceOnWall } = useSpatialQuery()
|
||||||
const isOnWall = useRef(false)
|
const isOnWall = useRef(false)
|
||||||
|
const isOnCeiling = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedItem) {
|
if (!selectedItem) {
|
||||||
@@ -120,12 +122,15 @@ export const ItemTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let currentWallId: string | null = null
|
let currentWallId: string | null = null
|
||||||
|
let currentCeilingId: string | null = null
|
||||||
|
|
||||||
const checkCanPlace = () => {
|
const checkCanPlace = () => {
|
||||||
const currentLevelId = useViewer.getState().selection.levelId
|
const currentLevelId = useViewer.getState().selection.levelId
|
||||||
if (currentLevelId && draftItem.current) {
|
if (currentLevelId && draftItem.current) {
|
||||||
let placeable = true
|
let placeable = true
|
||||||
if (draftItem.current.asset.attachTo) {
|
if (draftItem.current.asset.attachTo === 'ceiling') {
|
||||||
|
placeable = isOnCeiling.current && !!currentCeilingId
|
||||||
|
} else if (draftItem.current.asset.attachTo) {
|
||||||
if (!isOnWall.current || !currentWallId) {
|
if (!isOnWall.current || !currentWallId) {
|
||||||
placeable = false
|
placeable = false
|
||||||
} else {
|
} else {
|
||||||
@@ -181,7 +186,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
if (!cursorRef.current) return
|
if (!cursorRef.current) return
|
||||||
|
|
||||||
if (isOnWall.current) return
|
if (isOnWall.current || isOnCeiling.current) return
|
||||||
|
|
||||||
const [dimX, , dimZ] = selectedItem.dimensions
|
const [dimX, , dimZ] = selectedItem.dimensions
|
||||||
gridPosition.current.set(
|
gridPosition.current.set(
|
||||||
@@ -201,7 +206,7 @@ export const ItemTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
const onGridClick = (event: GridEvent) => {
|
const onGridClick = (event: GridEvent) => {
|
||||||
const currentLevelId = useViewer.getState().selection.levelId
|
const currentLevelId = useViewer.getState().selection.levelId
|
||||||
if (isOnWall.current) return
|
if (isOnWall.current || isOnCeiling.current) return
|
||||||
|
|
||||||
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return
|
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return
|
||||||
|
|
||||||
@@ -358,12 +363,125 @@ export const ItemTool: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// CEILING HANDLERS
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
const onCeilingEnter = (event: CeilingEvent) => {
|
||||||
|
if (draftItem.current?.asset.attachTo !== 'ceiling') return
|
||||||
|
if (
|
||||||
|
useViewer.getState().selection.levelId !==
|
||||||
|
resolveLevelId(event.node, useScene.getState().nodes)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
isOnCeiling.current = true
|
||||||
|
currentCeilingId = event.node.id
|
||||||
|
|
||||||
|
const [dimX, , dimZ] = selectedItem.dimensions
|
||||||
|
const itemHeight = selectedItem.dimensions[1]
|
||||||
|
|
||||||
|
gridPosition.current.set(
|
||||||
|
snapToGrid(event.position[0], dimX),
|
||||||
|
-itemHeight,
|
||||||
|
snapToGrid(event.position[2], dimZ),
|
||||||
|
)
|
||||||
|
cursorRef.current.position.set(
|
||||||
|
gridPosition.current.x,
|
||||||
|
event.position[1] - itemHeight,
|
||||||
|
gridPosition.current.z,
|
||||||
|
)
|
||||||
|
|
||||||
|
draftItem.current.parentId = event.node.id
|
||||||
|
|
||||||
|
useScene.getState().updateNode(draftItem.current.id, {
|
||||||
|
position: [gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
||||||
|
parentId: event.node.id,
|
||||||
|
})
|
||||||
|
checkCanPlace()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCeilingMove = (event: CeilingEvent) => {
|
||||||
|
if (!isOnCeiling.current || !draftItem.current) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const [dimX, , dimZ] = selectedItem.dimensions
|
||||||
|
const itemHeight = selectedItem.dimensions[1]
|
||||||
|
|
||||||
|
gridPosition.current.set(
|
||||||
|
snapToGrid(event.position[0], dimX),
|
||||||
|
-itemHeight,
|
||||||
|
snapToGrid(event.position[2], dimZ),
|
||||||
|
)
|
||||||
|
cursorRef.current.position.set(
|
||||||
|
gridPosition.current.x,
|
||||||
|
event.position[1] - itemHeight,
|
||||||
|
gridPosition.current.z,
|
||||||
|
)
|
||||||
|
|
||||||
|
checkCanPlace()
|
||||||
|
if (draftItem.current) {
|
||||||
|
draftItem.current.position = [
|
||||||
|
gridPosition.current.x,
|
||||||
|
gridPosition.current.y,
|
||||||
|
gridPosition.current.z,
|
||||||
|
]
|
||||||
|
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id)
|
||||||
|
if (draftItemMesh) {
|
||||||
|
draftItemMesh.position.copy(gridPosition.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCeilingClick = (event: CeilingEvent) => {
|
||||||
|
if (!isOnCeiling.current) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const currentLevelId = useViewer.getState().selection.levelId
|
||||||
|
if (!currentLevelId || !draftItem.current || !checkCanPlace()) return
|
||||||
|
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
|
useScene.getState().updateNode(draftItem.current.id, {
|
||||||
|
position: [gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
||||||
|
parentId: event.node.id,
|
||||||
|
metadata: stripTransient(draftItem.current.metadata),
|
||||||
|
})
|
||||||
|
draftItem.current = null
|
||||||
|
|
||||||
|
useScene.temporal.getState().pause()
|
||||||
|
createDraftItem()
|
||||||
|
checkCanPlace()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCeilingLeave = (event: CeilingEvent) => {
|
||||||
|
if (!isOnCeiling.current) return
|
||||||
|
isOnCeiling.current = false
|
||||||
|
currentCeilingId = null
|
||||||
|
event.stopPropagation()
|
||||||
|
if (!draftItem.current) return
|
||||||
|
const currentLevelId = useViewer.getState().selection.levelId
|
||||||
|
draftItem.current.parentId = currentLevelId
|
||||||
|
useScene.getState().updateNode(draftItem.current.id, {
|
||||||
|
position: [gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
|
||||||
|
parentId: currentLevelId,
|
||||||
|
})
|
||||||
|
checkCanPlace()
|
||||||
|
}
|
||||||
|
|
||||||
emitter.on('grid:move', onGridMove)
|
emitter.on('grid:move', onGridMove)
|
||||||
emitter.on('grid:click', onGridClick)
|
emitter.on('grid:click', onGridClick)
|
||||||
emitter.on('wall:enter', onWallEnter)
|
emitter.on('wall:enter', onWallEnter)
|
||||||
emitter.on('wall:move', onWallMove)
|
emitter.on('wall:move', onWallMove)
|
||||||
emitter.on('wall:click', onWallClick)
|
emitter.on('wall:click', onWallClick)
|
||||||
emitter.on('wall:leave', onWallLeave)
|
emitter.on('wall:leave', onWallLeave)
|
||||||
|
emitter.on('ceiling:enter', onCeilingEnter)
|
||||||
|
emitter.on('ceiling:move', onCeilingMove)
|
||||||
|
emitter.on('ceiling:click', onCeilingClick)
|
||||||
|
emitter.on('ceiling:leave', onCeilingLeave)
|
||||||
|
|
||||||
// Keyboard rotation handlers
|
// Keyboard rotation handlers
|
||||||
const ROTATION_STEP = Math.PI / 2 // 90 degrees
|
const ROTATION_STEP = Math.PI / 2 // 90 degrees
|
||||||
@@ -417,12 +535,16 @@ export const ItemTool: React.FC = () => {
|
|||||||
emitter.off('wall:leave', onWallLeave)
|
emitter.off('wall:leave', onWallLeave)
|
||||||
emitter.off('wall:click', onWallClick)
|
emitter.off('wall:click', onWallClick)
|
||||||
emitter.off('wall:move', onWallMove)
|
emitter.off('wall:move', onWallMove)
|
||||||
|
emitter.off('ceiling:enter', onCeilingEnter)
|
||||||
|
emitter.off('ceiling:move', onCeilingMove)
|
||||||
|
emitter.off('ceiling:click', onCeilingClick)
|
||||||
|
emitter.off('ceiling:leave', onCeilingLeave)
|
||||||
window.removeEventListener('keydown', onKeyDown)
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
}
|
}
|
||||||
}, [selectedItem, canPlaceOnFloor, canPlaceOnWall])
|
}, [selectedItem, canPlaceOnFloor, canPlaceOnWall])
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
if (draftItem.current && !isOnWall.current) {
|
if (draftItem.current && !isOnWall.current && !isOnCeiling.current) {
|
||||||
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id)
|
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id)
|
||||||
if (draftItemMesh) {
|
if (draftItemMesh) {
|
||||||
draftItemMesh.position.lerp(gridPosition.current, delta * 20)
|
draftItemMesh.position.lerp(gridPosition.current, delta * 20)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import dedent from 'dedent'
|
import dedent from 'dedent'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { BaseNode, nodeType, objectId } from '../base'
|
import { BaseNode, nodeType, objectId } from '../base'
|
||||||
|
import { ItemNode } from './item'
|
||||||
|
|
||||||
export const CeilingNode = BaseNode.extend({
|
export const CeilingNode = BaseNode.extend({
|
||||||
id: objectId('ceiling'),
|
id: objectId('ceiling'),
|
||||||
type: nodeType('ceiling'),
|
type: nodeType('ceiling'),
|
||||||
|
children: z.array(ItemNode.shape.id).default([]),
|
||||||
// Specific props
|
// Specific props
|
||||||
// Polygon boundary - array of [x, z] coordinates defining the ceiling
|
// Polygon boundary - array of [x, z] coordinates defining the ceiling
|
||||||
polygon: z.array(z.tuple([z.number(), z.number()])),
|
polygon: z.array(z.tuple([z.number(), z.number()])),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useRef } from 'react'
|
|||||||
import { faceDirection, float, mix } from 'three/tsl'
|
import { faceDirection, float, mix } from 'three/tsl'
|
||||||
import { DoubleSide, type Mesh, MeshStandardNodeMaterial } from 'three/webgpu'
|
import { DoubleSide, type Mesh, MeshStandardNodeMaterial } from 'three/webgpu'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
|
import { NodeRenderer } from '../node-renderer'
|
||||||
|
|
||||||
// TSL material that renders differently based on face direction:
|
// TSL material that renders differently based on face direction:
|
||||||
// - Back face (looking up at ceiling from below): solid
|
// - Back face (looking up at ceiling from below): solid
|
||||||
@@ -27,6 +28,9 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
|||||||
<mesh ref={ref} material={ceilingMaterial} {...handlers}>
|
<mesh ref={ref} material={ceilingMaterial} {...handlers}>
|
||||||
{/* CeilingSystem will replace this geometry in the next frame */}
|
{/* CeilingSystem will replace this geometry in the next frame */}
|
||||||
<boxGeometry args={[0, 0, 0]} />
|
<boxGeometry args={[0, 0, 0]} />
|
||||||
|
{node.children.map((childId) => (
|
||||||
|
<NodeRenderer key={childId} nodeId={childId} />
|
||||||
|
))}
|
||||||
</mesh>
|
</mesh>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const Viewer: React.FC<ViewerProps> = ({ children }) => {
|
|||||||
>
|
>
|
||||||
<color attach="background" args={['#ececec']} />
|
<color attach="background" args={['#ececec']} />
|
||||||
|
|
||||||
<directionalLight position={[10, 10, 5]} intensity={1.5} castShadow />
|
<directionalLight position={[10, 10, 5]} intensity={0.5} castShadow />
|
||||||
<Environment preset="sunset" />
|
<Environment preset="sunset" environmentIntensity={0.3} />
|
||||||
<Bvh>
|
<Bvh>
|
||||||
<SceneRenderer />
|
<SceneRenderer />
|
||||||
</Bvh>
|
</Bvh>
|
||||||
|
|||||||
Reference in New Issue
Block a user