fix attach items
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
type AnyNodeId,
|
||||||
type CeilingEvent,
|
type CeilingEvent,
|
||||||
emitter,
|
emitter,
|
||||||
type GridEvent,
|
type GridEvent,
|
||||||
@@ -10,7 +11,7 @@ import {
|
|||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useFrame } from '@react-three/fiber'
|
import { useFrame } from '@react-three/fiber'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { BoxGeometry, type Mesh, type MeshStandardMaterial, Vector3 } from 'three'
|
import { BoxGeometry, Euler, type Mesh, type MeshStandardMaterial, Quaternion, Vector3 } from 'three'
|
||||||
import { resolveLevelId } from '../../../../../packages/core/src/hooks/spatial-grid/spatial-grid-sync'
|
import { resolveLevelId } from '../../../../../packages/core/src/hooks/spatial-grid/spatial-grid-sync'
|
||||||
import {
|
import {
|
||||||
ceilingStrategy,
|
ceilingStrategy,
|
||||||
@@ -107,13 +108,18 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
// ---- Init draft ----
|
// ---- Init draft ----
|
||||||
config.initDraft(gridPosition.current)
|
config.initDraft(gridPosition.current)
|
||||||
|
|
||||||
// Sync cursor to the draft mesh's world position (handles floor + wall/ceiling items)
|
// Sync cursor to the draft mesh's world position and rotation
|
||||||
if (draftNode.current) {
|
if (draftNode.current) {
|
||||||
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
||||||
if (mesh) {
|
if (mesh) {
|
||||||
mesh.getWorldPosition(cursorRef.current.position)
|
mesh.getWorldPosition(cursorRef.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
|
||||||
} else {
|
} else {
|
||||||
cursorRef.current.position.copy(gridPosition.current)
|
cursorRef.current.position.copy(gridPosition.current)
|
||||||
|
cursorRef.current.rotation.y = draftNode.current.rotation[1] ?? 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +163,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
|
|
||||||
if (!draftNode.current) {
|
if (!draftNode.current) {
|
||||||
ensureDraft(result)
|
ensureDraft(result)
|
||||||
|
} else if (result.nodeUpdate.parentId) {
|
||||||
|
// Existing draft (move mode): reparent to new wall
|
||||||
|
useScene.getState().updateNode(draftNode.current.id, result.nodeUpdate)
|
||||||
|
if (result.stateUpdate.wallId) {
|
||||||
|
useScene.getState().dirtyNodes.add(result.stateUpdate.wallId as AnyNodeId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +182,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
applyTransition(enterResult)
|
applyTransition(enterResult)
|
||||||
|
if (draftNode.current && enterResult.nodeUpdate.parentId) {
|
||||||
|
useScene.getState().updateNode(draftNode.current.id, enterResult.nodeUpdate)
|
||||||
|
if (enterResult.stateUpdate.wallId) {
|
||||||
|
useScene.getState().dirtyNodes.add(enterResult.stateUpdate.wallId as AnyNodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +205,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
if (!result) return
|
if (!result) return
|
||||||
|
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const posChanged =
|
||||||
|
gridPosition.current.x !== result.gridPosition[0] ||
|
||||||
|
gridPosition.current.y !== result.gridPosition[1] ||
|
||||||
|
gridPosition.current.z !== result.gridPosition[2]
|
||||||
|
|
||||||
gridPosition.current.set(...result.gridPosition)
|
gridPosition.current.set(...result.gridPosition)
|
||||||
cursorRef.current.position.set(...result.cursorPosition)
|
cursorRef.current.position.set(...result.cursorPosition)
|
||||||
cursorRef.current.rotation.y = result.cursorRotationY
|
cursorRef.current.rotation.y = result.cursorRotationY
|
||||||
@@ -208,8 +232,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
const rot = result.nodeUpdate?.rotation
|
const rot = result.nodeUpdate?.rotation
|
||||||
if (rot) mesh.rotation.y = rot[1]
|
if (rot) mesh.rotation.y = rot[1]
|
||||||
}
|
}
|
||||||
// Mark parent wall dirty so it rebuilds geometry — no store update needed
|
// Mark parent wall dirty so it rebuilds geometry — only when position changed
|
||||||
if (result.dirtyNodeId) {
|
if (result.dirtyNodeId && posChanged) {
|
||||||
useScene.getState().dirtyNodes.add(result.dirtyNodeId)
|
useScene.getState().dirtyNodes.add(result.dirtyNodeId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,8 +267,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
if (asset.attachTo) {
|
if (asset.attachTo) {
|
||||||
|
if (draftNode.isAdopted) {
|
||||||
|
// Move mode: keep draft alive, reparent to level
|
||||||
|
const oldWallId = placementState.current.wallId
|
||||||
|
applyTransition(result)
|
||||||
|
const draft = draftNode.current
|
||||||
|
if (draft) {
|
||||||
|
useScene.getState().updateNode(draft.id, { parentId: result.nodeUpdate.parentId as string })
|
||||||
|
}
|
||||||
|
if (oldWallId) {
|
||||||
|
useScene.getState().dirtyNodes.add(oldWallId as AnyNodeId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create mode: destroy transient and reset state
|
||||||
draftNode.destroy()
|
draftNode.destroy()
|
||||||
Object.assign(placementState.current, result.stateUpdate)
|
Object.assign(placementState.current, result.stateUpdate)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
applyTransition(result)
|
applyTransition(result)
|
||||||
}
|
}
|
||||||
@@ -262,6 +300,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
|
|
||||||
if (!draftNode.current) {
|
if (!draftNode.current) {
|
||||||
ensureDraft(result)
|
ensureDraft(result)
|
||||||
|
} else if (result.nodeUpdate.parentId) {
|
||||||
|
// Existing draft (move mode): reparent to new ceiling
|
||||||
|
useScene.getState().updateNode(draftNode.current.id, result.nodeUpdate)
|
||||||
|
if (result.stateUpdate.ceilingId) {
|
||||||
|
useScene.getState().dirtyNodes.add(result.stateUpdate.ceilingId as AnyNodeId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,8 +362,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
if (asset.attachTo) {
|
if (asset.attachTo) {
|
||||||
|
if (draftNode.isAdopted) {
|
||||||
|
// Move mode: keep draft alive, reparent to level
|
||||||
|
const oldCeilingId = placementState.current.ceilingId
|
||||||
|
applyTransition(result)
|
||||||
|
const draft = draftNode.current
|
||||||
|
if (draft) {
|
||||||
|
useScene.getState().updateNode(draft.id, { parentId: result.nodeUpdate.parentId as string })
|
||||||
|
}
|
||||||
|
if (oldCeilingId) {
|
||||||
|
useScene.getState().dirtyNodes.add(oldCeilingId as AnyNodeId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create mode: destroy transient and reset state
|
||||||
draftNode.destroy()
|
draftNode.destroy()
|
||||||
Object.assign(placementState.current, result.stateUpdate)
|
Object.assign(placementState.current, result.stateUpdate)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
applyTransition(result)
|
applyTransition(result)
|
||||||
}
|
}
|
||||||
@@ -407,9 +465,18 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
}, [asset, canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling, draftNode])
|
}, [asset, canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling, draftNode])
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
if (draftNode.current && placementState.current.surface === 'floor') {
|
if (!draftNode.current) return
|
||||||
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
||||||
if (mesh) {
|
if (!mesh) return
|
||||||
|
|
||||||
|
// Hide wall/ceiling-attached items when between surfaces (only cursor visible)
|
||||||
|
if (asset.attachTo && placementState.current.surface === 'floor') {
|
||||||
|
mesh.visible = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mesh.visible = true
|
||||||
|
|
||||||
|
if (placementState.current.surface === 'floor') {
|
||||||
const distance = mesh.position.distanceToSquared(gridPosition.current)
|
const distance = mesh.position.distanceToSquared(gridPosition.current)
|
||||||
if (distance > 1) {
|
if (distance > 1) {
|
||||||
mesh.position.copy(gridPosition.current)
|
mesh.position.copy(gridPosition.current)
|
||||||
@@ -417,7 +484,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
}
|
}
|
||||||
mesh.position.lerp(gridPosition.current, delta * 20)
|
mesh.position.lerp(gridPosition.current, delta * 20)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -24,14 +24,13 @@ export const ItemSystem = () => {
|
|||||||
const mesh = sceneRegistry.nodes.get(id) as THREE.Object3D
|
const mesh = sceneRegistry.nodes.get(id) as THREE.Object3D
|
||||||
if (!mesh) return
|
if (!mesh) return
|
||||||
|
|
||||||
|
|
||||||
if (item.asset.attachTo === 'wall-side') {
|
if (item.asset.attachTo === 'wall-side') {
|
||||||
// Wall-attached item: offset Z by half the parent wall's thickness
|
// Wall-attached item: offset Z by half the parent wall's thickness
|
||||||
const parentWall = item.parentId ? nodes[item.parentId as AnyNodeId] : undefined
|
const parentWall = item.parentId ? nodes[item.parentId as AnyNodeId] : undefined
|
||||||
if (parentWall && parentWall.type === 'wall') {
|
if (parentWall && parentWall.type === 'wall') {
|
||||||
const wallThickness = (parentWall as WallNode).thickness ?? 0.1
|
const wallThickness = (parentWall as WallNode).thickness ?? 0.1
|
||||||
const side = item.side === 'front' ? 1 : -1
|
const side = item.side === 'front' ? 1 : -1
|
||||||
mesh.position.z = (wallThickness / 2) * side
|
mesh.position.z = (wallThickness / 2) * side;
|
||||||
}
|
}
|
||||||
} else if (!item.asset.attachTo) {
|
} else if (!item.asset.attachTo) {
|
||||||
// Floor item: elevate by slab height (using full footprint overlap)
|
// Floor item: elevate by slab height (using full footprint overlap)
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export const WallSystem = () => {
|
|||||||
const levelId = node.parentId
|
const levelId = node.parentId
|
||||||
if (!levelId) return
|
if (!levelId) return
|
||||||
|
|
||||||
|
|
||||||
if (!dirtyWallsByLevel.has(levelId)) {
|
if (!dirtyWallsByLevel.has(levelId)) {
|
||||||
dirtyWallsByLevel.set(levelId, new Set())
|
dirtyWallsByLevel.set(levelId, new Set())
|
||||||
}
|
}
|
||||||
@@ -117,7 +116,6 @@ function updateWallGeometry(wallId: string, miterData: WallMiterData) {
|
|||||||
|
|
||||||
mesh.geometry.dispose()
|
mesh.geometry.dispose()
|
||||||
mesh.geometry = newGeo
|
mesh.geometry = newGeo
|
||||||
|
|
||||||
// Update collision mesh
|
// Update collision mesh
|
||||||
const collisionMesh = mesh.getObjectByName('collision-mesh') as THREE.Mesh
|
const collisionMesh = mesh.getObjectByName('collision-mesh') as THREE.Mesh
|
||||||
if (collisionMesh) {
|
if (collisionMesh) {
|
||||||
|
|||||||
Reference in New Issue
Block a user