move + item corrections
This commit is contained in:
@@ -46,6 +46,11 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|||||||
structure: {
|
structure: {
|
||||||
types: ["wall", "item", "zone", "slab", "ceiling"],
|
types: ["wall", "item", "zone", "slab", "ceiling"],
|
||||||
handleSelect: (node, isShift) => {
|
handleSelect: (node, isShift) => {
|
||||||
|
// Single click on item (door/window) → enter move mode
|
||||||
|
if (!isShift && node.type === 'item') {
|
||||||
|
useEditor.getState().setMovingNode(node as ItemNode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { selection, setSelection } = useViewer.getState();
|
const { selection, setSelection } = useViewer.getState();
|
||||||
if (node.type === 'zone') {
|
if (node.type === 'zone') {
|
||||||
setSelection({ zoneId: node.id });
|
setSelection({ zoneId: node.id });
|
||||||
@@ -88,6 +93,11 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
|||||||
furnish: {
|
furnish: {
|
||||||
types: ["item"],
|
types: ["item"],
|
||||||
handleSelect: (node, isShift) => {
|
handleSelect: (node, isShift) => {
|
||||||
|
// Single click on item → enter move mode
|
||||||
|
if (!isShift && node.type === 'item') {
|
||||||
|
useEditor.getState().setMovingNode(node as ItemNode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { selection, setSelection } = useViewer.getState();
|
const { selection, setSelection } = useViewer.getState();
|
||||||
const nextIds = isShift
|
const nextIds = isShift
|
||||||
? selection.selectedIds.includes(node.id)
|
? selection.selectedIds.includes(node.id)
|
||||||
@@ -112,8 +122,11 @@ export const SelectionManager = () => {
|
|||||||
const phase = useEditor((s) => s.phase);
|
const phase = useEditor((s) => s.phase);
|
||||||
const mode = useEditor((s) => s.mode);
|
const mode = useEditor((s) => s.mode);
|
||||||
|
|
||||||
|
const movingNode = useEditor((s) => s.movingNode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== "select") return;
|
if (mode !== "select") return;
|
||||||
|
if (movingNode) return;
|
||||||
|
|
||||||
const strategy = SELECTION_STRATEGIES[phase];
|
const strategy = SELECTION_STRATEGIES[phase];
|
||||||
if (!strategy) return;
|
if (!strategy) return;
|
||||||
@@ -158,7 +171,7 @@ export const SelectionManager = () => {
|
|||||||
});
|
});
|
||||||
emitter.off("grid:click", onGridClick);
|
emitter.off("grid:click", onGridClick);
|
||||||
};
|
};
|
||||||
}, [phase, mode]);
|
}, [phase, mode, movingNode]);
|
||||||
|
|
||||||
return <EditorOutlinerSync />;
|
return <EditorOutlinerSync />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,413 +1,22 @@
|
|||||||
import {
|
|
||||||
type CeilingEvent,
|
|
||||||
emitter,
|
|
||||||
type GridEvent,
|
|
||||||
sceneRegistry,
|
|
||||||
useScene,
|
|
||||||
useSpatialQuery,
|
|
||||||
type WallEvent,
|
|
||||||
} from '@pascal-app/core'
|
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
|
||||||
import { useFrame } from '@react-three/fiber'
|
|
||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import { BoxGeometry, type Mesh, type MeshStandardMaterial, Vector3 } from 'three'
|
|
||||||
import useEditor from '@/store/use-editor'
|
import useEditor from '@/store/use-editor'
|
||||||
import { resolveLevelId } from '../../../../../packages/core/src/hooks/spatial-grid/spatial-grid-sync'
|
|
||||||
import {
|
|
||||||
ceilingStrategy,
|
|
||||||
checkCanPlace,
|
|
||||||
floorStrategy,
|
|
||||||
wallStrategy,
|
|
||||||
} from './placement-strategies'
|
|
||||||
import type { PlacementState, TransitionResult } from './placement-types'
|
|
||||||
import { useDraftNode } from './use-draft-node'
|
import { useDraftNode } from './use-draft-node'
|
||||||
|
import { usePlacementCoordinator } from './use-placement-coordinator'
|
||||||
|
|
||||||
export const ItemTool: React.FC = () => {
|
export const ItemTool: React.FC = () => {
|
||||||
const cursorRef = useRef<Mesh>(null!)
|
|
||||||
const gridPosition = useRef(new Vector3(0, 0, 0))
|
|
||||||
const placementState = useRef<PlacementState>({
|
|
||||||
surface: 'floor',
|
|
||||||
wallId: null,
|
|
||||||
ceilingId: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedItem = useEditor((state) => state.selectedItem)
|
const selectedItem = useEditor((state) => state.selectedItem)
|
||||||
const { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling } = useSpatialQuery()
|
|
||||||
const draftNode = useDraftNode()
|
const draftNode = useDraftNode()
|
||||||
|
|
||||||
useEffect(() => {
|
const cursor = usePlacementCoordinator({
|
||||||
if (!selectedItem) return
|
asset: selectedItem!,
|
||||||
|
draftNode,
|
||||||
useScene.temporal.getState().pause()
|
initDraft: (gridPosition) => {
|
||||||
|
if (!selectedItem?.attachTo) {
|
||||||
const validators = { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
|
draftNode.create(gridPosition, selectedItem!)
|
||||||
|
}
|
||||||
// Reset placement state for new item
|
},
|
||||||
placementState.current = { surface: 'floor', wallId: null, ceilingId: null }
|
onCommitted: () => true,
|
||||||
|
|
||||||
// ---- Helpers ----
|
|
||||||
|
|
||||||
const getContext = () => ({
|
|
||||||
asset: selectedItem,
|
|
||||||
levelId: useViewer.getState().selection.levelId,
|
|
||||||
draftItem: draftNode.current,
|
|
||||||
gridPosition: gridPosition.current,
|
|
||||||
state: { ...placementState.current },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const revalidate = (): boolean => {
|
if (!selectedItem) return null
|
||||||
const placeable = checkCanPlace(getContext(), validators)
|
return <>{cursor}</>
|
||||||
;(cursorRef.current.material as MeshStandardMaterial).color.set(
|
|
||||||
placeable ? 'green' : 'red',
|
|
||||||
)
|
|
||||||
return placeable
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyTransition = (result: TransitionResult) => {
|
|
||||||
Object.assign(placementState.current, result.stateUpdate)
|
|
||||||
gridPosition.current.set(...result.gridPosition)
|
|
||||||
cursorRef.current.position.set(...result.cursorPosition)
|
|
||||||
cursorRef.current.rotation.y = result.cursorRotationY
|
|
||||||
|
|
||||||
const draft = draftNode.current
|
|
||||||
if (draft) {
|
|
||||||
Object.assign(draft, result.nodeUpdate)
|
|
||||||
useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
|
||||||
}
|
|
||||||
revalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a draft from a transition result on the first valid move.
|
|
||||||
* If placement is invalid at this position, the draft is immediately destroyed
|
|
||||||
* so no item appears in the scene until the cursor reaches a valid spot.
|
|
||||||
*/
|
|
||||||
const ensureDraft = (result: TransitionResult) => {
|
|
||||||
gridPosition.current.set(...result.gridPosition)
|
|
||||||
cursorRef.current.position.set(...result.cursorPosition)
|
|
||||||
cursorRef.current.rotation.y = result.cursorRotationY
|
|
||||||
|
|
||||||
draftNode.create(gridPosition.current, selectedItem)
|
|
||||||
|
|
||||||
const draft = draftNode.current
|
|
||||||
if (draft) {
|
|
||||||
Object.assign(draft, result.nodeUpdate)
|
|
||||||
useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!revalidate()) {
|
|
||||||
draftNode.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Create initial draft (floor items only) ----
|
|
||||||
// Wall/ceiling items are created on surface enter to avoid floating items.
|
|
||||||
|
|
||||||
if (!selectedItem.attachTo) {
|
|
||||||
draftNode.create(gridPosition.current, selectedItem)
|
|
||||||
}
|
|
||||||
revalidate()
|
|
||||||
|
|
||||||
// ---- Floor Handlers ----
|
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
|
||||||
const result = floorStrategy.move(getContext(), event)
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
gridPosition.current.set(...result.gridPosition)
|
|
||||||
cursorRef.current.position.set(...result.cursorPosition)
|
|
||||||
|
|
||||||
const draft = draftNode.current
|
|
||||||
if (draft) draft.position = result.gridPosition
|
|
||||||
|
|
||||||
revalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onGridClick = (event: GridEvent) => {
|
|
||||||
const result = floorStrategy.click(getContext(), event, validators)
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
draftNode.commit(result.nodeUpdate)
|
|
||||||
draftNode.create(gridPosition.current, selectedItem)
|
|
||||||
revalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Wall Handlers ----
|
|
||||||
|
|
||||||
const onWallEnter = (event: WallEvent) => {
|
|
||||||
const nodes = useScene.getState().nodes
|
|
||||||
const result = wallStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
applyTransition(result)
|
|
||||||
|
|
||||||
// Try to create draft immediately if placement is valid
|
|
||||||
if (!draftNode.current) {
|
|
||||||
ensureDraft(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onWallMove = (event: WallEvent) => {
|
|
||||||
const ctx = getContext()
|
|
||||||
|
|
||||||
// If not yet on wall surface (e.g. entered via invalid top face),
|
|
||||||
// promote this move to an enter when hitting a valid side face.
|
|
||||||
if (ctx.state.surface !== 'wall') {
|
|
||||||
const nodes = useScene.getState().nodes
|
|
||||||
const enterResult = wallStrategy.enter(ctx, event, resolveLevelId, nodes)
|
|
||||||
if (!enterResult) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
applyTransition(enterResult)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// No draft yet (first move after enter) — create at current position if valid
|
|
||||||
if (!draftNode.current) {
|
|
||||||
const nodes = useScene.getState().nodes
|
|
||||||
const setup = wallStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
|
||||||
if (!setup) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
ensureDraft(setup)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = wallStrategy.move(ctx, event)
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
gridPosition.current.set(...result.gridPosition)
|
|
||||||
cursorRef.current.position.set(...result.cursorPosition)
|
|
||||||
cursorRef.current.rotation.y = result.cursorRotationY
|
|
||||||
|
|
||||||
// Sync side/rotation on draft ref (needed by checkCanPlace)
|
|
||||||
const draft = draftNode.current
|
|
||||||
if (draft && result.nodeUpdate) {
|
|
||||||
if ('side' in result.nodeUpdate) draft.side = result.nodeUpdate.side
|
|
||||||
if ('rotation' in result.nodeUpdate)
|
|
||||||
draft.rotation = result.nodeUpdate.rotation as [number, number, number]
|
|
||||||
}
|
|
||||||
|
|
||||||
const placeable = revalidate()
|
|
||||||
|
|
||||||
// Only update mesh + store when placement is valid
|
|
||||||
if (draft && placeable) {
|
|
||||||
draft.position = result.gridPosition
|
|
||||||
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
||||||
if (mesh) {
|
|
||||||
mesh.position.copy(gridPosition.current)
|
|
||||||
const rot = result.nodeUpdate?.rotation
|
|
||||||
if (rot) mesh.rotation.y = rot[1]
|
|
||||||
}
|
|
||||||
if (result.nodeUpdate) {
|
|
||||||
useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
|
||||||
}
|
|
||||||
if (result.dirtyNodeId) {
|
|
||||||
useScene.getState().dirtyNodes.add(result.dirtyNodeId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onWallClick = (event: WallEvent) => {
|
|
||||||
const result = wallStrategy.click(getContext(), event, validators)
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
draftNode.commit(result.nodeUpdate)
|
|
||||||
if (result.dirtyNodeId) {
|
|
||||||
useScene.getState().dirtyNodes.add(result.dirtyNodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-enter the wall — applyTransition creates the next draft at the correct position
|
|
||||||
const nodes = useScene.getState().nodes
|
|
||||||
const enterResult = wallStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
|
||||||
if (enterResult) {
|
|
||||||
applyTransition(enterResult)
|
|
||||||
} else {
|
|
||||||
revalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onWallLeave = (event: WallEvent) => {
|
|
||||||
const result = wallStrategy.leave(getContext())
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
// Wall/ceiling items: destroy draft so it doesn't float on the floor
|
|
||||||
if (selectedItem.attachTo) {
|
|
||||||
draftNode.destroy()
|
|
||||||
Object.assign(placementState.current, result.stateUpdate)
|
|
||||||
} else {
|
|
||||||
applyTransition(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Ceiling Handlers ----
|
|
||||||
|
|
||||||
const onCeilingEnter = (event: CeilingEvent) => {
|
|
||||||
const nodes = useScene.getState().nodes
|
|
||||||
const result = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
applyTransition(result)
|
|
||||||
|
|
||||||
// Try to create draft immediately if placement is valid
|
|
||||||
if (!draftNode.current) {
|
|
||||||
ensureDraft(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCeilingMove = (event: CeilingEvent) => {
|
|
||||||
// No draft yet (first move after enter) — create at current position if valid
|
|
||||||
if (!draftNode.current && placementState.current.surface === 'ceiling') {
|
|
||||||
const nodes = useScene.getState().nodes
|
|
||||||
const setup = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
|
||||||
if (!setup) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
ensureDraft(setup)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = ceilingStrategy.move(getContext(), event)
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
gridPosition.current.set(...result.gridPosition)
|
|
||||||
cursorRef.current.position.set(...result.cursorPosition)
|
|
||||||
|
|
||||||
revalidate()
|
|
||||||
|
|
||||||
const draft = draftNode.current
|
|
||||||
if (draft) {
|
|
||||||
draft.position = result.gridPosition
|
|
||||||
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
||||||
if (mesh) mesh.position.copy(gridPosition.current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCeilingClick = (event: CeilingEvent) => {
|
|
||||||
const result = ceilingStrategy.click(getContext(), event, validators)
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
draftNode.commit(result.nodeUpdate)
|
|
||||||
|
|
||||||
// Re-enter the ceiling — applyTransition creates the next draft at the correct position
|
|
||||||
const nodes = useScene.getState().nodes
|
|
||||||
const enterResult = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
|
||||||
if (enterResult) {
|
|
||||||
applyTransition(enterResult)
|
|
||||||
} else {
|
|
||||||
revalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCeilingLeave = (event: CeilingEvent) => {
|
|
||||||
const result = ceilingStrategy.leave(getContext())
|
|
||||||
if (!result) return
|
|
||||||
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
// Wall/ceiling items: destroy draft so it doesn't float on the floor
|
|
||||||
if (selectedItem.attachTo) {
|
|
||||||
draftNode.destroy()
|
|
||||||
Object.assign(placementState.current, result.stateUpdate)
|
|
||||||
} else {
|
|
||||||
applyTransition(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Keyboard rotation ----
|
|
||||||
|
|
||||||
const ROTATION_STEP = Math.PI / 2
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
|
||||||
const draft = draftNode.current
|
|
||||||
if (!draft) return
|
|
||||||
|
|
||||||
let rotationDelta = 0
|
|
||||||
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
||||||
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
||||||
|
|
||||||
if (rotationDelta !== 0) {
|
|
||||||
event.preventDefault()
|
|
||||||
const currentRotation = draft.rotation
|
|
||||||
const newRotationY = (currentRotation[1] ?? 0) + rotationDelta
|
|
||||||
draft.rotation = [currentRotation[0], newRotationY, currentRotation[2]]
|
|
||||||
|
|
||||||
useScene.getState().updateNode(draft.id, { rotation: draft.rotation })
|
|
||||||
cursorRef.current.rotation.y = newRotationY
|
|
||||||
revalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('keydown', onKeyDown)
|
|
||||||
|
|
||||||
// ---- Bounding box geometry ----
|
|
||||||
|
|
||||||
const boxGeometry = new BoxGeometry(
|
|
||||||
selectedItem.dimensions[0],
|
|
||||||
selectedItem.dimensions[1],
|
|
||||||
selectedItem.dimensions[2],
|
|
||||||
)
|
|
||||||
boxGeometry.translate(0, selectedItem.dimensions[1] / 2, 0)
|
|
||||||
cursorRef.current.geometry = boxGeometry
|
|
||||||
|
|
||||||
// ---- Subscribe ----
|
|
||||||
|
|
||||||
emitter.on('grid:move', onGridMove)
|
|
||||||
emitter.on('grid:click', onGridClick)
|
|
||||||
emitter.on('wall:enter', onWallEnter)
|
|
||||||
emitter.on('wall:move', onWallMove)
|
|
||||||
emitter.on('wall:click', onWallClick)
|
|
||||||
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)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
draftNode.destroy()
|
|
||||||
useScene.temporal.getState().resume()
|
|
||||||
emitter.off('grid:move', onGridMove)
|
|
||||||
emitter.off('grid:click', onGridClick)
|
|
||||||
emitter.off('wall:enter', onWallEnter)
|
|
||||||
emitter.off('wall:move', onWallMove)
|
|
||||||
emitter.off('wall:click', onWallClick)
|
|
||||||
emitter.off('wall:leave', onWallLeave)
|
|
||||||
emitter.off('ceiling:enter', onCeilingEnter)
|
|
||||||
emitter.off('ceiling:move', onCeilingMove)
|
|
||||||
emitter.off('ceiling:click', onCeilingClick)
|
|
||||||
emitter.off('ceiling:leave', onCeilingLeave)
|
|
||||||
window.removeEventListener('keydown', onKeyDown)
|
|
||||||
}
|
|
||||||
}, [selectedItem, canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling, draftNode])
|
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
|
||||||
if (draftNode.current && placementState.current.surface === 'floor') {
|
|
||||||
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
|
||||||
if (mesh) {
|
|
||||||
// If distance is large, snap immediately
|
|
||||||
const distance = mesh.position.distanceToSquared(gridPosition.current)
|
|
||||||
if (distance > 1) {
|
|
||||||
mesh.position.copy(gridPosition.current)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Otherwise, lerp smoothly
|
|
||||||
mesh.position.lerp(gridPosition.current, delta * 20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group>
|
|
||||||
<mesh ref={cursorRef}>
|
|
||||||
<boxGeometry args={[0.1, 0.1, 0.1]} />
|
|
||||||
<meshStandardMaterial color="red" wireframe />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import useEditor from '@/store/use-editor'
|
||||||
|
import { Vector3 } from 'three'
|
||||||
|
import { useDraftNode } from './use-draft-node'
|
||||||
|
import { usePlacementCoordinator } from './use-placement-coordinator'
|
||||||
|
import type { PlacementState } from './placement-types'
|
||||||
|
|
||||||
|
function getInitialState(node: { asset: { attachTo?: string }; parentId: string | null }): PlacementState {
|
||||||
|
const attachTo = node.asset.attachTo
|
||||||
|
if (attachTo === 'wall' || attachTo === 'wall-side') {
|
||||||
|
return { surface: 'wall', wallId: node.parentId, ceilingId: null }
|
||||||
|
}
|
||||||
|
if (attachTo === 'ceiling') {
|
||||||
|
return { surface: 'ceiling', wallId: null, ceilingId: node.parentId }
|
||||||
|
}
|
||||||
|
return { surface: 'floor', wallId: null, ceilingId: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MoveTool: React.FC = () => {
|
||||||
|
const movingNode = useEditor((state) => state.movingNode)
|
||||||
|
const draftNode = useDraftNode()
|
||||||
|
|
||||||
|
const exitMoveMode = () => {
|
||||||
|
useEditor.getState().setMovingNode(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursor = usePlacementCoordinator({
|
||||||
|
asset: movingNode!.asset,
|
||||||
|
draftNode,
|
||||||
|
initialState: movingNode ? getInitialState(movingNode) : undefined,
|
||||||
|
initDraft: (gridPosition) => {
|
||||||
|
if (!movingNode) return
|
||||||
|
draftNode.adopt(movingNode)
|
||||||
|
gridPosition.copy(new Vector3(...movingNode.position))
|
||||||
|
},
|
||||||
|
onCommitted: () => {
|
||||||
|
exitMoveMode()
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
draftNode.destroy()
|
||||||
|
exitMoveMode()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!movingNode) return null
|
||||||
|
return <>{cursor}</>
|
||||||
|
}
|
||||||
@@ -5,23 +5,41 @@ import type { Vector3 } from 'three'
|
|||||||
import type { Asset } from '../../../../../packages/core/src/schema/nodes/item'
|
import type { Asset } from '../../../../../packages/core/src/schema/nodes/item'
|
||||||
import { stripTransient } from './placement-math'
|
import { stripTransient } from './placement-math'
|
||||||
|
|
||||||
|
interface OriginalState {
|
||||||
|
position: [number, number, number]
|
||||||
|
rotation: [number, number, number]
|
||||||
|
side: ItemNode['side']
|
||||||
|
parentId: string | null
|
||||||
|
metadata: ItemNode['metadata']
|
||||||
|
}
|
||||||
|
|
||||||
export interface DraftNodeHandle {
|
export interface DraftNodeHandle {
|
||||||
/** Current draft item, or null */
|
/** Current draft item, or null */
|
||||||
readonly current: ItemNode | null
|
readonly current: ItemNode | null
|
||||||
|
/** Whether the current draft was adopted (move mode) vs created (create mode) */
|
||||||
|
readonly isAdopted: boolean
|
||||||
/** Create a new draft item at the given position. Returns the created node or null. */
|
/** Create a new draft item at the given position. Returns the created node or null. */
|
||||||
create: (gridPosition: Vector3, asset: Asset) => ItemNode | null
|
create: (gridPosition: Vector3, asset: Asset) => ItemNode | null
|
||||||
/** Commit the current draft: delete draft (paused), resume, create fresh node (tracked), re-pause. */
|
/** Take ownership of an existing scene node as the draft (for move mode). */
|
||||||
|
adopt: (node: ItemNode) => void
|
||||||
|
/** Commit the current draft. Create mode: delete+recreate. Move mode: update in place. */
|
||||||
commit: (finalUpdate: Partial<ItemNode>) => string | null
|
commit: (finalUpdate: Partial<ItemNode>) => string | null
|
||||||
/** Destroy the current draft: delete node (stays paused, no undo entry). */
|
/** Destroy the current draft. Create mode: delete node. Move mode: restore original state. */
|
||||||
destroy: () => void
|
destroy: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that manages the lifecycle of a transient (draft) item node.
|
* Hook that manages the lifecycle of a transient (draft) item node.
|
||||||
* Handles temporal pause/resume for undo/redo isolation.
|
* Handles temporal pause/resume for undo/redo isolation.
|
||||||
|
*
|
||||||
|
* Supports two modes:
|
||||||
|
* - Create mode (via `create()`): draft is a new transient node. Commit = delete+recreate (undo removes node).
|
||||||
|
* - Move mode (via `adopt()`): draft is an existing node. Commit = update in place (undo reverts position).
|
||||||
*/
|
*/
|
||||||
export function useDraftNode(): DraftNodeHandle {
|
export function useDraftNode(): DraftNodeHandle {
|
||||||
const draftRef = useRef<ItemNode | null>(null)
|
const draftRef = useRef<ItemNode | null>(null)
|
||||||
|
const adoptedRef = useRef(false)
|
||||||
|
const originalStateRef = useRef<OriginalState | null>(null)
|
||||||
|
|
||||||
const create = useCallback((gridPosition: Vector3, asset: Asset): ItemNode | null => {
|
const create = useCallback((gridPosition: Vector3, asset: Asset): ItemNode | null => {
|
||||||
const currentLevelId = useViewer.getState().selection.levelId
|
const currentLevelId = useViewer.getState().selection.levelId
|
||||||
@@ -33,17 +51,77 @@ export function useDraftNode(): DraftNodeHandle {
|
|||||||
asset,
|
asset,
|
||||||
metadata: { isTransient: true },
|
metadata: { isTransient: true },
|
||||||
})
|
})
|
||||||
console.log('create node', node)
|
|
||||||
|
|
||||||
useScene.getState().createNode(node, currentLevelId)
|
useScene.getState().createNode(node, currentLevelId)
|
||||||
draftRef.current = node
|
draftRef.current = node
|
||||||
|
adoptedRef.current = false
|
||||||
|
originalStateRef.current = null
|
||||||
return node
|
return node
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const adopt = useCallback((node: ItemNode): void => {
|
||||||
|
// Save original state so destroy() can restore it
|
||||||
|
const meta = (typeof node.metadata === 'object' && node.metadata !== null && !Array.isArray(node.metadata))
|
||||||
|
? node.metadata as Record<string, unknown>
|
||||||
|
: {}
|
||||||
|
|
||||||
|
originalStateRef.current = {
|
||||||
|
position: [...node.position] as [number, number, number],
|
||||||
|
rotation: [...node.rotation] as [number, number, number],
|
||||||
|
side: node.side,
|
||||||
|
parentId: node.parentId,
|
||||||
|
metadata: node.metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
draftRef.current = node
|
||||||
|
adoptedRef.current = true
|
||||||
|
|
||||||
|
// Mark as transient so it renders as a draft
|
||||||
|
useScene.getState().updateNode(node.id, {
|
||||||
|
metadata: { ...meta, isTransient: true },
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const commit = useCallback((finalUpdate: Partial<ItemNode>): string | null => {
|
const commit = useCallback((finalUpdate: Partial<ItemNode>): string | null => {
|
||||||
const draft = draftRef.current
|
const draft = draftRef.current
|
||||||
if (!draft) return null
|
if (!draft) return null
|
||||||
|
|
||||||
|
if (adoptedRef.current) {
|
||||||
|
// Move mode: update in place (single undoable action)
|
||||||
|
const { parentId: newParentId, ...updateProps } = finalUpdate
|
||||||
|
const parentId = newParentId ?? originalStateRef.current?.parentId ?? useViewer.getState().selection.levelId
|
||||||
|
const original = originalStateRef.current!
|
||||||
|
|
||||||
|
// Restore original state while paused — so the undo baseline is clean
|
||||||
|
useScene.getState().updateNode(draft.id, {
|
||||||
|
position: original.position,
|
||||||
|
rotation: original.rotation,
|
||||||
|
side: original.side,
|
||||||
|
parentId: original.parentId,
|
||||||
|
metadata: original.metadata,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Resume → tracked update (undo reverts to original)
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
|
|
||||||
|
useScene.getState().updateNode(draft.id, {
|
||||||
|
position: updateProps.position ?? draft.position,
|
||||||
|
rotation: updateProps.rotation ?? draft.rotation,
|
||||||
|
side: updateProps.side ?? draft.side,
|
||||||
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
||||||
|
parentId: parentId as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
useScene.temporal.getState().pause()
|
||||||
|
|
||||||
|
const id = draft.id
|
||||||
|
draftRef.current = null
|
||||||
|
adoptedRef.current = false
|
||||||
|
originalStateRef.current = null
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mode: delete draft (paused), resume, create fresh node (tracked), re-pause
|
||||||
const { parentId: newParentId, ...updateProps } = finalUpdate
|
const { parentId: newParentId, ...updateProps } = finalUpdate
|
||||||
const parentId = (newParentId ?? useViewer.getState().selection.levelId) as AnyNodeId
|
const parentId = (newParentId ?? useViewer.getState().selection.levelId) as AnyNodeId
|
||||||
if (!parentId) return null
|
if (!parentId) return null
|
||||||
@@ -68,14 +146,33 @@ export function useDraftNode(): DraftNodeHandle {
|
|||||||
// Re-pause for next draft cycle
|
// Re-pause for next draft cycle
|
||||||
useScene.temporal.getState().pause()
|
useScene.temporal.getState().pause()
|
||||||
|
|
||||||
|
adoptedRef.current = false
|
||||||
|
originalStateRef.current = null
|
||||||
return finalNode.id
|
return finalNode.id
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const destroy = useCallback(() => {
|
const destroy = useCallback(() => {
|
||||||
if (draftRef.current) {
|
if (!draftRef.current) return
|
||||||
|
|
||||||
|
if (adoptedRef.current && originalStateRef.current) {
|
||||||
|
// Move mode: restore original state instead of deleting
|
||||||
|
const original = originalStateRef.current
|
||||||
|
|
||||||
|
useScene.getState().updateNode(draftRef.current.id, {
|
||||||
|
position: original.position,
|
||||||
|
rotation: original.rotation,
|
||||||
|
side: original.side,
|
||||||
|
parentId: original.parentId,
|
||||||
|
metadata: original.metadata,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Create mode: delete the transient node
|
||||||
useScene.getState().deleteNode(draftRef.current.id)
|
useScene.getState().deleteNode(draftRef.current.id)
|
||||||
draftRef.current = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draftRef.current = null
|
||||||
|
adoptedRef.current = false
|
||||||
|
originalStateRef.current = null
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
@@ -83,10 +180,14 @@ export function useDraftNode(): DraftNodeHandle {
|
|||||||
get current() {
|
get current() {
|
||||||
return draftRef.current
|
return draftRef.current
|
||||||
},
|
},
|
||||||
|
get isAdopted() {
|
||||||
|
return adoptedRef.current
|
||||||
|
},
|
||||||
create,
|
create,
|
||||||
|
adopt,
|
||||||
commit,
|
commit,
|
||||||
destroy,
|
destroy,
|
||||||
}),
|
}),
|
||||||
[create, commit, destroy],
|
[create, adopt, commit, destroy],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
import {
|
||||||
|
type CeilingEvent,
|
||||||
|
emitter,
|
||||||
|
type GridEvent,
|
||||||
|
sceneRegistry,
|
||||||
|
useScene,
|
||||||
|
useSpatialQuery,
|
||||||
|
type WallEvent,
|
||||||
|
} from '@pascal-app/core'
|
||||||
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
|
import { useFrame } from '@react-three/fiber'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { BoxGeometry, type Mesh, type MeshStandardMaterial, Vector3 } from 'three'
|
||||||
|
import { resolveLevelId } from '../../../../../packages/core/src/hooks/spatial-grid/spatial-grid-sync'
|
||||||
|
import {
|
||||||
|
ceilingStrategy,
|
||||||
|
checkCanPlace,
|
||||||
|
floorStrategy,
|
||||||
|
wallStrategy,
|
||||||
|
} from './placement-strategies'
|
||||||
|
import type { PlacementState, TransitionResult } from './placement-types'
|
||||||
|
import type { DraftNodeHandle } from './use-draft-node'
|
||||||
|
import type { Asset } from '../../../../../packages/core/src/schema/nodes/item'
|
||||||
|
|
||||||
|
export interface PlacementCoordinatorConfig {
|
||||||
|
asset: Asset
|
||||||
|
draftNode: DraftNodeHandle
|
||||||
|
initDraft: (gridPosition: Vector3) => void
|
||||||
|
onCommitted: () => boolean
|
||||||
|
onCancel?: () => void
|
||||||
|
initialState?: PlacementState
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlacementCoordinator(config: PlacementCoordinatorConfig): React.ReactNode {
|
||||||
|
const cursorRef = useRef<Mesh>(null!)
|
||||||
|
const gridPosition = useRef(new Vector3(0, 0, 0))
|
||||||
|
const placementState = useRef<PlacementState>(
|
||||||
|
config.initialState ?? { surface: 'floor', wallId: null, ceilingId: null },
|
||||||
|
)
|
||||||
|
|
||||||
|
const { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling } = useSpatialQuery()
|
||||||
|
const { asset, draftNode } = config
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
useScene.temporal.getState().pause()
|
||||||
|
|
||||||
|
const validators = { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
|
||||||
|
|
||||||
|
// Reset placement state
|
||||||
|
placementState.current = config.initialState ?? {
|
||||||
|
surface: 'floor',
|
||||||
|
wallId: null,
|
||||||
|
ceilingId: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Helpers ----
|
||||||
|
|
||||||
|
const getContext = () => ({
|
||||||
|
asset,
|
||||||
|
levelId: useViewer.getState().selection.levelId,
|
||||||
|
draftItem: draftNode.current,
|
||||||
|
gridPosition: gridPosition.current,
|
||||||
|
state: { ...placementState.current },
|
||||||
|
})
|
||||||
|
|
||||||
|
const revalidate = (): boolean => {
|
||||||
|
const placeable = checkCanPlace(getContext(), validators)
|
||||||
|
;(cursorRef.current.material as MeshStandardMaterial).color.set(
|
||||||
|
placeable ? 'green' : 'red',
|
||||||
|
)
|
||||||
|
return placeable
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyTransition = (result: TransitionResult) => {
|
||||||
|
Object.assign(placementState.current, result.stateUpdate)
|
||||||
|
gridPosition.current.set(...result.gridPosition)
|
||||||
|
cursorRef.current.position.set(...result.cursorPosition)
|
||||||
|
cursorRef.current.rotation.y = result.cursorRotationY
|
||||||
|
|
||||||
|
const draft = draftNode.current
|
||||||
|
if (draft) {
|
||||||
|
Object.assign(draft, result.nodeUpdate)
|
||||||
|
// In move mode, skip store updates — only the ref + mesh matter during drag
|
||||||
|
if (!draftNode.isAdopted) {
|
||||||
|
useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
revalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureDraft = (result: TransitionResult) => {
|
||||||
|
gridPosition.current.set(...result.gridPosition)
|
||||||
|
cursorRef.current.position.set(...result.cursorPosition)
|
||||||
|
cursorRef.current.rotation.y = result.cursorRotationY
|
||||||
|
|
||||||
|
draftNode.create(gridPosition.current, asset)
|
||||||
|
|
||||||
|
const draft = draftNode.current
|
||||||
|
if (draft) {
|
||||||
|
Object.assign(draft, result.nodeUpdate)
|
||||||
|
if (!draftNode.isAdopted) {
|
||||||
|
useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!revalidate()) {
|
||||||
|
draftNode.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Init draft ----
|
||||||
|
config.initDraft(gridPosition.current)
|
||||||
|
|
||||||
|
// Sync cursor to the draft mesh's world position (handles floor + wall/ceiling items)
|
||||||
|
if (draftNode.current) {
|
||||||
|
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
||||||
|
if (mesh) {
|
||||||
|
mesh.getWorldPosition(cursorRef.current.position)
|
||||||
|
} else {
|
||||||
|
cursorRef.current.position.copy(gridPosition.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidate()
|
||||||
|
|
||||||
|
// ---- Floor Handlers ----
|
||||||
|
|
||||||
|
const onGridMove = (event: GridEvent) => {
|
||||||
|
const result = floorStrategy.move(getContext(), event)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
gridPosition.current.set(...result.gridPosition)
|
||||||
|
cursorRef.current.position.set(...result.cursorPosition)
|
||||||
|
|
||||||
|
const draft = draftNode.current
|
||||||
|
if (draft) draft.position = result.gridPosition
|
||||||
|
|
||||||
|
revalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGridClick = (event: GridEvent) => {
|
||||||
|
const result = floorStrategy.click(getContext(), event, validators)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
draftNode.commit(result.nodeUpdate)
|
||||||
|
if (config.onCommitted()) {
|
||||||
|
draftNode.create(gridPosition.current, asset)
|
||||||
|
revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Wall Handlers ----
|
||||||
|
|
||||||
|
const onWallEnter = (event: WallEvent) => {
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
const result = wallStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
applyTransition(result)
|
||||||
|
|
||||||
|
if (!draftNode.current) {
|
||||||
|
ensureDraft(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWallMove = (event: WallEvent) => {
|
||||||
|
const ctx = getContext()
|
||||||
|
|
||||||
|
if (ctx.state.surface !== 'wall') {
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
const enterResult = wallStrategy.enter(ctx, event, resolveLevelId, nodes)
|
||||||
|
if (!enterResult) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
applyTransition(enterResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!draftNode.current) {
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
const setup = wallStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
||||||
|
if (!setup) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
ensureDraft(setup)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = wallStrategy.move(ctx, event)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
gridPosition.current.set(...result.gridPosition)
|
||||||
|
cursorRef.current.position.set(...result.cursorPosition)
|
||||||
|
cursorRef.current.rotation.y = result.cursorRotationY
|
||||||
|
|
||||||
|
const draft = draftNode.current
|
||||||
|
if (draft && result.nodeUpdate) {
|
||||||
|
if ('side' in result.nodeUpdate) draft.side = result.nodeUpdate.side
|
||||||
|
if ('rotation' in result.nodeUpdate)
|
||||||
|
draft.rotation = result.nodeUpdate.rotation as [number, number, number]
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeable = revalidate()
|
||||||
|
|
||||||
|
if (draft && placeable) {
|
||||||
|
draft.position = result.gridPosition
|
||||||
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
||||||
|
if (mesh) {
|
||||||
|
mesh.position.copy(gridPosition.current)
|
||||||
|
const rot = result.nodeUpdate?.rotation
|
||||||
|
if (rot) mesh.rotation.y = rot[1]
|
||||||
|
}
|
||||||
|
// In move mode, skip store updates — only the ref + mesh matter during drag
|
||||||
|
if (!draftNode.isAdopted) {
|
||||||
|
if (result.nodeUpdate) {
|
||||||
|
useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
||||||
|
}
|
||||||
|
if (result.dirtyNodeId) {
|
||||||
|
useScene.getState().dirtyNodes.add(result.dirtyNodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWallClick = (event: WallEvent) => {
|
||||||
|
const result = wallStrategy.click(getContext(), event, validators)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
draftNode.commit(result.nodeUpdate)
|
||||||
|
if (result.dirtyNodeId) {
|
||||||
|
useScene.getState().dirtyNodes.add(result.dirtyNodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.onCommitted()) {
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
const enterResult = wallStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
||||||
|
if (enterResult) {
|
||||||
|
applyTransition(enterResult)
|
||||||
|
} else {
|
||||||
|
revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWallLeave = (event: WallEvent) => {
|
||||||
|
const result = wallStrategy.leave(getContext())
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (asset.attachTo) {
|
||||||
|
draftNode.destroy()
|
||||||
|
Object.assign(placementState.current, result.stateUpdate)
|
||||||
|
} else {
|
||||||
|
applyTransition(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Ceiling Handlers ----
|
||||||
|
|
||||||
|
const onCeilingEnter = (event: CeilingEvent) => {
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
const result = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
applyTransition(result)
|
||||||
|
|
||||||
|
if (!draftNode.current) {
|
||||||
|
ensureDraft(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCeilingMove = (event: CeilingEvent) => {
|
||||||
|
if (!draftNode.current && placementState.current.surface === 'ceiling') {
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
const setup = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
||||||
|
if (!setup) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
ensureDraft(setup)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = ceilingStrategy.move(getContext(), event)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
gridPosition.current.set(...result.gridPosition)
|
||||||
|
cursorRef.current.position.set(...result.cursorPosition)
|
||||||
|
|
||||||
|
revalidate()
|
||||||
|
|
||||||
|
const draft = draftNode.current
|
||||||
|
if (draft) {
|
||||||
|
draft.position = result.gridPosition
|
||||||
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
||||||
|
if (mesh) mesh.position.copy(gridPosition.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCeilingClick = (event: CeilingEvent) => {
|
||||||
|
const result = ceilingStrategy.click(getContext(), event, validators)
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
draftNode.commit(result.nodeUpdate)
|
||||||
|
|
||||||
|
if (config.onCommitted()) {
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
const enterResult = ceilingStrategy.enter(getContext(), event, resolveLevelId, nodes)
|
||||||
|
if (enterResult) {
|
||||||
|
applyTransition(enterResult)
|
||||||
|
} else {
|
||||||
|
revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCeilingLeave = (event: CeilingEvent) => {
|
||||||
|
const result = ceilingStrategy.leave(getContext())
|
||||||
|
if (!result) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
if (asset.attachTo) {
|
||||||
|
draftNode.destroy()
|
||||||
|
Object.assign(placementState.current, result.stateUpdate)
|
||||||
|
} else {
|
||||||
|
applyTransition(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Keyboard rotation ----
|
||||||
|
|
||||||
|
const ROTATION_STEP = Math.PI / 2
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// Escape / right-click → cancel
|
||||||
|
if (event.key === 'Escape' && config.onCancel) {
|
||||||
|
event.preventDefault()
|
||||||
|
config.onCancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = draftNode.current
|
||||||
|
if (!draft) return
|
||||||
|
|
||||||
|
let rotationDelta = 0
|
||||||
|
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
||||||
|
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
||||||
|
|
||||||
|
if (rotationDelta !== 0) {
|
||||||
|
event.preventDefault()
|
||||||
|
const currentRotation = draft.rotation
|
||||||
|
const newRotationY = (currentRotation[1] ?? 0) + rotationDelta
|
||||||
|
draft.rotation = [currentRotation[0], newRotationY, currentRotation[2]]
|
||||||
|
|
||||||
|
// In move mode, skip store update — only the ref + cursor mesh matter during drag
|
||||||
|
if (!draftNode.isAdopted) {
|
||||||
|
useScene.getState().updateNode(draft.id, { rotation: draft.rotation })
|
||||||
|
}
|
||||||
|
cursorRef.current.rotation.y = newRotationY
|
||||||
|
revalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKeyDown)
|
||||||
|
|
||||||
|
// ---- Right-click cancel ----
|
||||||
|
const onContextMenu = (event: MouseEvent) => {
|
||||||
|
if (config.onCancel) {
|
||||||
|
event.preventDefault()
|
||||||
|
config.onCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('contextmenu', onContextMenu)
|
||||||
|
|
||||||
|
// ---- Bounding box geometry ----
|
||||||
|
|
||||||
|
const boxGeometry = new BoxGeometry(
|
||||||
|
asset.dimensions[0],
|
||||||
|
asset.dimensions[1],
|
||||||
|
asset.dimensions[2],
|
||||||
|
)
|
||||||
|
boxGeometry.translate(0, asset.dimensions[1] / 2, 0)
|
||||||
|
cursorRef.current.geometry = boxGeometry
|
||||||
|
|
||||||
|
// ---- Subscribe ----
|
||||||
|
|
||||||
|
emitter.on('grid:move', onGridMove)
|
||||||
|
emitter.on('grid:click', onGridClick)
|
||||||
|
emitter.on('wall:enter', onWallEnter)
|
||||||
|
emitter.on('wall:move', onWallMove)
|
||||||
|
emitter.on('wall:click', onWallClick)
|
||||||
|
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)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
draftNode.destroy()
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
|
emitter.off('grid:move', onGridMove)
|
||||||
|
emitter.off('grid:click', onGridClick)
|
||||||
|
emitter.off('wall:enter', onWallEnter)
|
||||||
|
emitter.off('wall:move', onWallMove)
|
||||||
|
emitter.off('wall:click', onWallClick)
|
||||||
|
emitter.off('wall:leave', onWallLeave)
|
||||||
|
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('contextmenu', onContextMenu)
|
||||||
|
}
|
||||||
|
}, [asset, canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling, draftNode])
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
if (draftNode.current && placementState.current.surface === 'floor') {
|
||||||
|
const mesh = sceneRegistry.nodes.get(draftNode.current.id)
|
||||||
|
if (mesh) {
|
||||||
|
const distance = mesh.position.distanceToSquared(gridPosition.current)
|
||||||
|
if (distance > 1) {
|
||||||
|
mesh.position.copy(gridPosition.current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mesh.position.lerp(gridPosition.current, delta * 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
<mesh ref={cursorRef}>
|
||||||
|
<boxGeometry args={[0.1, 0.1, 0.1]} />
|
||||||
|
<meshStandardMaterial color="red" wireframe />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import useEditor, { type Phase, type Tool } from "@/store/use-editor";
|
|||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
import { CeilingTool } from "./ceiling/ceiling-tool";
|
import { CeilingTool } from "./ceiling/ceiling-tool";
|
||||||
import { ItemTool } from "./item/item-tool";
|
import { ItemTool } from "./item/item-tool";
|
||||||
|
import { MoveTool } from "./item/move-tool";
|
||||||
import { SlabTool } from "./slab/slab-tool";
|
import { SlabTool } from "./slab/slab-tool";
|
||||||
import { WallTool } from "./wall/wall-tool";
|
import { WallTool } from "./wall/wall-tool";
|
||||||
import { ZoneBoundaryEditor } from "./zone/zone-boundary-editor";
|
import { ZoneBoundaryEditor } from "./zone/zone-boundary-editor";
|
||||||
@@ -25,6 +26,7 @@ export const ToolManager: React.FC = () => {
|
|||||||
const phase = useEditor((state) => state.phase);
|
const phase = useEditor((state) => state.phase);
|
||||||
const mode = useEditor((state) => state.mode);
|
const mode = useEditor((state) => state.mode);
|
||||||
const tool = useEditor((state) => state.tool);
|
const tool = useEditor((state) => state.tool);
|
||||||
|
const movingNode = useEditor((state) => state.movingNode);
|
||||||
const selectedZoneId = useViewer((state) => state.selection.zoneId);
|
const selectedZoneId = useViewer((state) => state.selection.zoneId);
|
||||||
|
|
||||||
// Show zone boundary editor when in structure/select mode with a zone selected
|
// Show zone boundary editor when in structure/select mode with a zone selected
|
||||||
@@ -39,7 +41,8 @@ export const ToolManager: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showZoneBoundaryEditor && <ZoneBoundaryEditor />}
|
{showZoneBoundaryEditor && <ZoneBoundaryEditor />}
|
||||||
{BuildToolComponent && <BuildToolComponent />}
|
{movingNode && <MoveTool />}
|
||||||
|
{!movingNode && BuildToolComponent && <BuildToolComponent />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "hydrant",
|
"id": "hydrant",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "hydrant",
|
"name": "hydrant",
|
||||||
"thumbnail": "/items/hydrant/thumbnail.webp",
|
"thumbnail": "/items/hydrant/thumbnail.webp",
|
||||||
"src": "/items/hydrant/model.glb",
|
"src": "/items/hydrant/model.glb",
|
||||||
@@ -58,7 +58,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "sunbed",
|
"id": "sunbed",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "sunbed",
|
"name": "sunbed",
|
||||||
"thumbnail": "/items/sunbed/thumbnail.webp",
|
"thumbnail": "/items/sunbed/thumbnail.webp",
|
||||||
"src": "/items/sunbed/model.glb",
|
"src": "/items/sunbed/model.glb",
|
||||||
@@ -86,19 +86,19 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "palm",
|
"id": "palm",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "palm",
|
"name": "palm",
|
||||||
"thumbnail": "/items/palm/thumbnail.webp",
|
"thumbnail": "/items/palm/thumbnail.webp",
|
||||||
"src": "/items/palm/model.glb",
|
"src": "/items/palm/model.glb",
|
||||||
"scale": [
|
"scale": [
|
||||||
0.37,
|
0.37,
|
||||||
0.37000000000000005,
|
0.37,
|
||||||
0.37
|
0.37
|
||||||
],
|
],
|
||||||
"offset": [
|
"offset": [
|
||||||
0.13999999999999999,
|
|
||||||
0,
|
0,
|
||||||
-0.20000000000000007
|
0,
|
||||||
|
0.02
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
@@ -106,15 +106,15 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
3.500000000000003,
|
1,
|
||||||
4.500000000000007,
|
4.5,
|
||||||
3.500000000000004
|
1
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "fence",
|
"id": "fence",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "fence",
|
"name": "fence",
|
||||||
"thumbnail": "/items/fence/thumbnail.webp",
|
"thumbnail": "/items/fence/thumbnail.webp",
|
||||||
"src": "/items/fence/model.glb",
|
"src": "/items/fence/model.glb",
|
||||||
@@ -142,7 +142,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "patio-umbrella",
|
"id": "patio-umbrella",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "patio-umbrella",
|
"name": "patio-umbrella",
|
||||||
"thumbnail": "/items/patio-umbrella/thumbnail.webp",
|
"thumbnail": "/items/patio-umbrella/thumbnail.webp",
|
||||||
"src": "/items/patio-umbrella/model.glb",
|
"src": "/items/patio-umbrella/model.glb",
|
||||||
@@ -154,7 +154,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
"offset": [
|
"offset": [
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0.09
|
0
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
@@ -162,9 +162,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
3.500000000000003,
|
1,
|
||||||
3.0000000000000013,
|
3,
|
||||||
3.500000000000002
|
1
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "smoke-detector",
|
"id": "smoke-detector",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "smoke-detector",
|
"name": "smoke-detector",
|
||||||
"thumbnail": "/items/smoke-detector/thumbnail.webp",
|
"thumbnail": "/items/smoke-detector/thumbnail.webp",
|
||||||
"src": "/items/smoke-detector/model.glb",
|
"src": "/items/smoke-detector/model.glb",
|
||||||
@@ -256,7 +256,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "sprinkler",
|
"id": "sprinkler",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "sprinkler",
|
"name": "sprinkler",
|
||||||
"thumbnail": "/items/sprinkler/thumbnail.webp",
|
"thumbnail": "/items/sprinkler/thumbnail.webp",
|
||||||
"src": "/items/sprinkler/model.glb",
|
"src": "/items/sprinkler/model.glb",
|
||||||
@@ -314,7 +314,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "ceiling-fan",
|
"id": "ceiling-fan",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "ceiling-fan",
|
"name": "ceiling-fan",
|
||||||
"thumbnail": "/items/ceiling-fan/thumbnail.webp",
|
"thumbnail": "/items/ceiling-fan/thumbnail.webp",
|
||||||
"src": "/items/ceiling-fan/model.glb",
|
"src": "/items/ceiling-fan/model.glb",
|
||||||
@@ -363,9 +363,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
2,
|
1.9999999999999991,
|
||||||
2,
|
2.0000000000000004,
|
||||||
0.4
|
0.39999999999999936
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall"
|
||||||
},
|
},
|
||||||
@@ -401,7 +401,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "glass-door",
|
"id": "glass-door",
|
||||||
"category": "furniture",
|
"category": "door",
|
||||||
"name": "glass-door",
|
"name": "glass-door",
|
||||||
"thumbnail": "/items/glass-door/thumbnail.webp",
|
"thumbnail": "/items/glass-door/thumbnail.webp",
|
||||||
"src": "/items/glass-door/model.glb",
|
"src": "/items/glass-door/model.glb",
|
||||||
@@ -430,7 +430,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "door",
|
"id": "door",
|
||||||
"category": "furniture",
|
"category": "door",
|
||||||
"name": "door",
|
"name": "door",
|
||||||
"thumbnail": "/items/door/thumbnail.webp",
|
"thumbnail": "/items/door/thumbnail.webp",
|
||||||
"src": "/items/door/model.glb",
|
"src": "/items/door/model.glb",
|
||||||
@@ -459,7 +459,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "thermostat",
|
"id": "thermostat",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "thermostat",
|
"name": "thermostat",
|
||||||
"thumbnail": "/items/thermostat/thumbnail.webp",
|
"thumbnail": "/items/thermostat/thumbnail.webp",
|
||||||
"src": "/items/thermostat/model.glb",
|
"src": "/items/thermostat/model.glb",
|
||||||
@@ -489,7 +489,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "parking-spot",
|
"id": "parking-spot",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "parking-spot",
|
"name": "parking-spot",
|
||||||
"thumbnail": "/items/parking-spot/thumbnail.webp",
|
"thumbnail": "/items/parking-spot/thumbnail.webp",
|
||||||
"src": "/items/parking-spot/model.glb",
|
"src": "/items/parking-spot/model.glb",
|
||||||
@@ -517,7 +517,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "ac-block",
|
"id": "ac-block",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "ac-block",
|
"name": "ac-block",
|
||||||
"thumbnail": "/items/ac-block/thumbnail.webp",
|
"thumbnail": "/items/ac-block/thumbnail.webp",
|
||||||
"src": "/items/ac-block/model.glb",
|
"src": "/items/ac-block/model.glb",
|
||||||
@@ -545,7 +545,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "fire-detector",
|
"id": "fire-detector",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "fire-detector",
|
"name": "fire-detector",
|
||||||
"thumbnail": "/items/fire-detector/thumbnail.webp",
|
"thumbnail": "/items/fire-detector/thumbnail.webp",
|
||||||
"src": "/items/fire-detector/model.glb",
|
"src": "/items/fire-detector/model.glb",
|
||||||
@@ -574,7 +574,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "exit-sign",
|
"id": "exit-sign",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "exit-sign",
|
"name": "exit-sign",
|
||||||
"thumbnail": "/items/exit-sign/thumbnail.webp",
|
"thumbnail": "/items/exit-sign/thumbnail.webp",
|
||||||
"src": "/items/exit-sign/model.glb",
|
"src": "/items/exit-sign/model.glb",
|
||||||
@@ -594,28 +594,28 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
0.9999999999999999,
|
1,
|
||||||
0.5,
|
0.5,
|
||||||
0.30000000000000004
|
0.3
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "electric-panel",
|
"id": "electric-panel",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "electric-panel",
|
"name": "electric-panel",
|
||||||
"thumbnail": "/items/electric-panel/thumbnail.webp",
|
"thumbnail": "/items/electric-panel/thumbnail.webp",
|
||||||
"src": "/items/electric-panel/model.glb",
|
"src": "/items/electric-panel/model.glb",
|
||||||
"scale": [
|
"scale": [
|
||||||
0.6100000000000002,
|
0.61,
|
||||||
0.7399999999999999,
|
0.74,
|
||||||
0.7
|
0.7
|
||||||
],
|
],
|
||||||
"offset": [
|
"offset": [
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0.060000000000000005
|
0.06
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
@@ -623,17 +623,17 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
0.5000000000000001,
|
0.5,
|
||||||
0.9999999999999999,
|
1,
|
||||||
0.30000000000000004
|
0.3
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "alarm-keypad",
|
"id": "alarm-keypad",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "alarm-keypad",
|
"name": "alarm-keypad",
|
||||||
"thumbnail": "/items/alarm-keypad/thumbnail.webp",
|
"thumbnail": "/items/alarm-keypad/thumbnail.webp",
|
||||||
"src": "/items/alarm-keypad/model.glb",
|
"src": "/items/alarm-keypad/model.glb",
|
||||||
@@ -671,9 +671,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
1
|
1
|
||||||
],
|
],
|
||||||
"offset": [
|
"offset": [
|
||||||
-0.08,
|
|
||||||
0,
|
0,
|
||||||
0.020000000000000004
|
0.1,
|
||||||
|
0.01
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
@@ -681,10 +681,11 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
0.500000000000001,
|
1,
|
||||||
0.30000000000000004,
|
0.5,
|
||||||
0.5000000000000003
|
0.7
|
||||||
]
|
],
|
||||||
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -745,7 +746,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "skate",
|
"id": "skate",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "skate",
|
"name": "skate",
|
||||||
"thumbnail": "/items/skate/thumbnail.webp",
|
"thumbnail": "/items/skate/thumbnail.webp",
|
||||||
"src": "/items/skate/model.glb",
|
"src": "/items/skate/model.glb",
|
||||||
@@ -801,7 +802,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "outdoor-playhouse",
|
"id": "outdoor-playhouse",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "outdoor-playhouse",
|
"name": "outdoor-playhouse",
|
||||||
"thumbnail": "/items/outdoor-playhouse/thumbnail.webp",
|
"thumbnail": "/items/outdoor-playhouse/thumbnail.webp",
|
||||||
"src": "/items/outdoor-playhouse/model.glb",
|
"src": "/items/outdoor-playhouse/model.glb",
|
||||||
@@ -829,7 +830,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "scooter",
|
"id": "scooter",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "scooter",
|
"name": "scooter",
|
||||||
"thumbnail": "/items/scooter/thumbnail.webp",
|
"thumbnail": "/items/scooter/thumbnail.webp",
|
||||||
"src": "/items/scooter/model.glb",
|
"src": "/items/scooter/model.glb",
|
||||||
@@ -857,7 +858,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "basket-hoop",
|
"id": "basket-hoop",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "basket-hoop",
|
"name": "basket-hoop",
|
||||||
"thumbnail": "/items/basket-hoop/thumbnail.webp",
|
"thumbnail": "/items/basket-hoop/thumbnail.webp",
|
||||||
"src": "/items/basket-hoop/model.glb",
|
"src": "/items/basket-hoop/model.glb",
|
||||||
@@ -941,7 +942,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "hood",
|
"id": "hood",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "hood",
|
"name": "hood",
|
||||||
"thumbnail": "/items/hood/thumbnail.webp",
|
"thumbnail": "/items/hood/thumbnail.webp",
|
||||||
"src": "/items/hood/model.glb",
|
"src": "/items/hood/model.glb",
|
||||||
@@ -952,8 +953,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"offset": [
|
"offset": [
|
||||||
0,
|
0,
|
||||||
0.5200000000000002,
|
0.52,
|
||||||
0.00999999999999999
|
0.01
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
@@ -961,16 +962,16 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
1.5000000000000016,
|
1.5,
|
||||||
1,
|
1,
|
||||||
1.1000000000000003
|
1.1
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "kitchen-shelf",
|
"id": "kitchen-shelf",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "kitchen-shelf",
|
"name": "kitchen-shelf",
|
||||||
"thumbnail": "/items/kitchen-shelf/thumbnail.webp",
|
"thumbnail": "/items/kitchen-shelf/thumbnail.webp",
|
||||||
"src": "/items/kitchen-shelf/model.glb",
|
"src": "/items/kitchen-shelf/model.glb",
|
||||||
@@ -981,8 +982,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"offset": [
|
"offset": [
|
||||||
0,
|
0,
|
||||||
0.5200000000000002,
|
0.52,
|
||||||
0.00999999999999999
|
0.01
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
@@ -990,11 +991,11 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
2.500000000000002,
|
2.5,
|
||||||
1,
|
1,
|
||||||
1.1000000000000003
|
1.1
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -1027,7 +1028,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "drying-rack",
|
"id": "drying-rack",
|
||||||
"category": "furniture",
|
"category": "bathroom",
|
||||||
"name": "drying-rack",
|
"name": "drying-rack",
|
||||||
"thumbnail": "/items/drying-rack/thumbnail.webp",
|
"thumbnail": "/items/drying-rack/thumbnail.webp",
|
||||||
"src": "/items/drying-rack/model.glb",
|
"src": "/items/drying-rack/model.glb",
|
||||||
@@ -1066,8 +1067,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"offset": [
|
"offset": [
|
||||||
0,
|
0,
|
||||||
0.19000000000000003,
|
0.19,
|
||||||
0.11999999999999998
|
0.12
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
@@ -1075,16 +1076,16 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
0.5000000000000008,
|
|
||||||
0.5,
|
0.5,
|
||||||
0.5000000000000003
|
0.5,
|
||||||
|
0.5
|
||||||
],
|
],
|
||||||
"attachTo": "wall-side"
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "cutting-board",
|
"id": "cutting-board",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "cutting-board",
|
"name": "cutting-board",
|
||||||
"thumbnail": "/items/cutting-board/thumbnail.webp",
|
"thumbnail": "/items/cutting-board/thumbnail.webp",
|
||||||
"src": "/items/cutting-board/model.glb",
|
"src": "/items/cutting-board/model.glb",
|
||||||
@@ -1112,7 +1113,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "frying-pan",
|
"id": "frying-pan",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "frying-pan",
|
"name": "frying-pan",
|
||||||
"thumbnail": "/items/frying-pan/thumbnail.webp",
|
"thumbnail": "/items/frying-pan/thumbnail.webp",
|
||||||
"src": "/items/frying-pan/model.glb",
|
"src": "/items/frying-pan/model.glb",
|
||||||
@@ -1140,7 +1141,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "kitchen-utensils",
|
"id": "kitchen-utensils",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "kitchen-utensils",
|
"name": "kitchen-utensils",
|
||||||
"thumbnail": "/items/kitchen-utensils/thumbnail.webp",
|
"thumbnail": "/items/kitchen-utensils/thumbnail.webp",
|
||||||
"src": "/items/kitchen-utensils/model.glb",
|
"src": "/items/kitchen-utensils/model.glb",
|
||||||
@@ -1168,7 +1169,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "kitchen-counter",
|
"id": "kitchen-counter",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "kitchen-counter",
|
"name": "kitchen-counter",
|
||||||
"thumbnail": "/items/kitchen-counter/thumbnail.webp",
|
"thumbnail": "/items/kitchen-counter/thumbnail.webp",
|
||||||
"src": "/items/kitchen-counter/model.glb",
|
"src": "/items/kitchen-counter/model.glb",
|
||||||
@@ -1196,7 +1197,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "kitchen-cabinet",
|
"id": "kitchen-cabinet",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "kitchen-cabinet",
|
"name": "kitchen-cabinet",
|
||||||
"thumbnail": "/items/kitchen-cabinet/thumbnail.webp",
|
"thumbnail": "/items/kitchen-cabinet/thumbnail.webp",
|
||||||
"src": "/items/kitchen-cabinet/model.glb",
|
"src": "/items/kitchen-cabinet/model.glb",
|
||||||
@@ -1336,7 +1337,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "television",
|
"id": "television",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "television",
|
"name": "television",
|
||||||
"thumbnail": "/items/television/thumbnail.webp",
|
"thumbnail": "/items/television/thumbnail.webp",
|
||||||
"src": "/items/television/model.glb",
|
"src": "/items/television/model.glb",
|
||||||
@@ -1392,7 +1393,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "air-conditioning",
|
"id": "air-conditioning",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "air-conditioning",
|
"name": "air-conditioning",
|
||||||
"thumbnail": "/items/air-conditioning/thumbnail.webp",
|
"thumbnail": "/items/air-conditioning/thumbnail.webp",
|
||||||
"src": "/items/air-conditioning/model.glb",
|
"src": "/items/air-conditioning/model.glb",
|
||||||
@@ -1403,8 +1404,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"offset": [
|
"offset": [
|
||||||
0,
|
0,
|
||||||
0.37000000000000016,
|
0.37,
|
||||||
0.21000000000000005
|
0.21
|
||||||
],
|
],
|
||||||
"rotation": [
|
"rotation": [
|
||||||
0,
|
0,
|
||||||
@@ -1414,14 +1415,14 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
"dimensions": [
|
"dimensions": [
|
||||||
2,
|
2,
|
||||||
1,
|
1,
|
||||||
0.8999999999999999
|
0.9
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "sewing-machine",
|
"id": "sewing-machine",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "sewing-machine",
|
"name": "sewing-machine",
|
||||||
"thumbnail": "/items/sewing-machine/thumbnail.webp",
|
"thumbnail": "/items/sewing-machine/thumbnail.webp",
|
||||||
"src": "/items/sewing-machine/model.glb",
|
"src": "/items/sewing-machine/model.glb",
|
||||||
@@ -1533,7 +1534,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "toaster",
|
"id": "toaster",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "toaster",
|
"name": "toaster",
|
||||||
"thumbnail": "/items/toaster/thumbnail.webp",
|
"thumbnail": "/items/toaster/thumbnail.webp",
|
||||||
"src": "/items/toaster/model.glb",
|
"src": "/items/toaster/model.glb",
|
||||||
@@ -1618,7 +1619,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "kettle",
|
"id": "kettle",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "kettle",
|
"name": "kettle",
|
||||||
"thumbnail": "/items/kettle/thumbnail.webp",
|
"thumbnail": "/items/kettle/thumbnail.webp",
|
||||||
"src": "/items/kettle/model.glb",
|
"src": "/items/kettle/model.glb",
|
||||||
@@ -1646,7 +1647,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "iron",
|
"id": "iron",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "iron",
|
"name": "iron",
|
||||||
"thumbnail": "/items/iron/thumbnail.webp",
|
"thumbnail": "/items/iron/thumbnail.webp",
|
||||||
"src": "/items/iron/model.glb",
|
"src": "/items/iron/model.glb",
|
||||||
@@ -1760,7 +1761,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "coffee-machine",
|
"id": "coffee-machine",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "coffee-machine",
|
"name": "coffee-machine",
|
||||||
"thumbnail": "/items/coffee-machine/thumbnail.webp",
|
"thumbnail": "/items/coffee-machine/thumbnail.webp",
|
||||||
"src": "/items/coffee-machine/model.glb",
|
"src": "/items/coffee-machine/model.glb",
|
||||||
@@ -1788,7 +1789,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "wine-bottle",
|
"id": "wine-bottle",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "wine-bottle",
|
"name": "wine-bottle",
|
||||||
"thumbnail": "/items/wine-bottle/thumbnail.webp",
|
"thumbnail": "/items/wine-bottle/thumbnail.webp",
|
||||||
"src": "/items/wine-bottle/model.glb",
|
"src": "/items/wine-bottle/model.glb",
|
||||||
@@ -1816,7 +1817,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "fruits",
|
"id": "fruits",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "fruits",
|
"name": "fruits",
|
||||||
"thumbnail": "/items/fruits/thumbnail.webp",
|
"thumbnail": "/items/fruits/thumbnail.webp",
|
||||||
"src": "/items/fruits/model.glb",
|
"src": "/items/fruits/model.glb",
|
||||||
@@ -1868,7 +1869,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
1,
|
1,
|
||||||
0.1
|
0.1
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -1901,7 +1902,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "ball",
|
"id": "ball",
|
||||||
"category": "furniture",
|
"category": "outdoor",
|
||||||
"name": "ball",
|
"name": "ball",
|
||||||
"thumbnail": "/items/ball/thumbnail.webp",
|
"thumbnail": "/items/ball/thumbnail.webp",
|
||||||
"src": "/items/ball/model.glb",
|
"src": "/items/ball/model.glb",
|
||||||
@@ -1950,7 +1951,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
1.5,
|
1.5,
|
||||||
1,
|
0.8,
|
||||||
1
|
1
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2037,12 +2038,12 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
1,
|
1,
|
||||||
0.2
|
0.2
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall-side"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "stereo-speaker",
|
"id": "stereo-speaker",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "stereo-speaker",
|
"name": "stereo-speaker",
|
||||||
"thumbnail": "/items/stereo-speaker/thumbnail.webp",
|
"thumbnail": "/items/stereo-speaker/thumbnail.webp",
|
||||||
"src": "/items/stereo-speaker/model.glb",
|
"src": "/items/stereo-speaker/model.glb",
|
||||||
@@ -2098,7 +2099,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "stove",
|
"id": "stove",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "stove",
|
"name": "stove",
|
||||||
"thumbnail": "/items/stove/thumbnail.webp",
|
"thumbnail": "/items/stove/thumbnail.webp",
|
||||||
"src": "/items/stove/model.glb",
|
"src": "/items/stove/model.glb",
|
||||||
@@ -2126,7 +2127,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "fridge",
|
"id": "fridge",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "fridge",
|
"name": "fridge",
|
||||||
"thumbnail": "/items/fridge/thumbnail.webp",
|
"thumbnail": "/items/fridge/thumbnail.webp",
|
||||||
"src": "/items/fridge/model.glb",
|
"src": "/items/fridge/model.glb",
|
||||||
@@ -2154,7 +2155,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "kitchen",
|
"id": "kitchen",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "kitchen",
|
"name": "kitchen",
|
||||||
"thumbnail": "/items/kitchen/thumbnail.webp",
|
"thumbnail": "/items/kitchen/thumbnail.webp",
|
||||||
"src": "/items/kitchen/model.glb",
|
"src": "/items/kitchen/model.glb",
|
||||||
@@ -2182,7 +2183,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "microwave",
|
"id": "microwave",
|
||||||
"category": "furniture",
|
"category": "kitchen",
|
||||||
"name": "microwave",
|
"name": "microwave",
|
||||||
"thumbnail": "/items/microwave/thumbnail.webp",
|
"thumbnail": "/items/microwave/thumbnail.webp",
|
||||||
"src": "/items/microwave/model.glb",
|
"src": "/items/microwave/model.glb",
|
||||||
@@ -2322,7 +2323,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "washing-machine",
|
"id": "washing-machine",
|
||||||
"category": "furniture",
|
"category": "bathroom",
|
||||||
"name": "washing-machine",
|
"name": "washing-machine",
|
||||||
"thumbnail": "/items/washing-machine/thumbnail.webp",
|
"thumbnail": "/items/washing-machine/thumbnail.webp",
|
||||||
"src": "/items/washing-machine/model.glb",
|
"src": "/items/washing-machine/model.glb",
|
||||||
@@ -2350,7 +2351,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "laundry-bag",
|
"id": "laundry-bag",
|
||||||
"category": "furniture",
|
"category": "bathroom",
|
||||||
"name": "laundry-bag",
|
"name": "laundry-bag",
|
||||||
"thumbnail": "/items/laundry-bag/thumbnail.webp",
|
"thumbnail": "/items/laundry-bag/thumbnail.webp",
|
||||||
"src": "/items/laundry-bag/model.glb",
|
"src": "/items/laundry-bag/model.glb",
|
||||||
@@ -2378,7 +2379,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "shower-angle",
|
"id": "shower-angle",
|
||||||
"category": "furniture",
|
"category": "bathroom",
|
||||||
"name": "shower-angle",
|
"name": "shower-angle",
|
||||||
"thumbnail": "/items/shower-angle/thumbnail.webp",
|
"thumbnail": "/items/shower-angle/thumbnail.webp",
|
||||||
"src": "/items/shower-angle/model.glb",
|
"src": "/items/shower-angle/model.glb",
|
||||||
@@ -2406,7 +2407,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "bathtub",
|
"id": "bathtub",
|
||||||
"category": "furniture",
|
"category": "bathroom",
|
||||||
"name": "bathtub",
|
"name": "bathtub",
|
||||||
"thumbnail": "/items/bathtub/thumbnail.webp",
|
"thumbnail": "/items/bathtub/thumbnail.webp",
|
||||||
"src": "/items/bathtub/model.glb",
|
"src": "/items/bathtub/model.glb",
|
||||||
@@ -2427,14 +2428,14 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
2.5,
|
2.5,
|
||||||
1,
|
0.8,
|
||||||
1.5
|
1.5
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "toilet",
|
"id": "toilet",
|
||||||
"category": "furniture",
|
"category": "bathroom",
|
||||||
"name": "toilet",
|
"name": "toilet",
|
||||||
"thumbnail": "/items/toilet/thumbnail.webp",
|
"thumbnail": "/items/toilet/thumbnail.webp",
|
||||||
"src": "/items/toilet/model.glb",
|
"src": "/items/toilet/model.glb",
|
||||||
@@ -2455,7 +2456,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
1,
|
1,
|
||||||
1,
|
0.9,
|
||||||
1
|
1
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2483,14 +2484,14 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
2.5,
|
2.5,
|
||||||
1,
|
0.8,
|
||||||
1.5
|
1.5
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "shower-square",
|
"id": "shower-square",
|
||||||
"category": "furniture",
|
"category": "bathroom",
|
||||||
"name": "shower-square",
|
"name": "shower-square",
|
||||||
"thumbnail": "/items/shower-square/thumbnail.webp",
|
"thumbnail": "/items/shower-square/thumbnail.webp",
|
||||||
"src": "/items/shower-square/model.glb",
|
"src": "/items/shower-square/model.glb",
|
||||||
@@ -2518,7 +2519,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
"id": "bathroom-sink",
|
"id": "bathroom-sink",
|
||||||
"category": "furniture",
|
"category": "bathroom",
|
||||||
"name": "bathroom-sink",
|
"name": "bathroom-sink",
|
||||||
"thumbnail": "/items/bathroom-sink/thumbnail.webp",
|
"thumbnail": "/items/bathroom-sink/thumbnail.webp",
|
||||||
"src": "/items/bathroom-sink/model.glb",
|
"src": "/items/bathroom-sink/model.glb",
|
||||||
@@ -2547,7 +2548,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
{
|
{
|
||||||
"id": "coffee-table",
|
"id": "coffee-table",
|
||||||
"category": "furniture",
|
"category": "furniture",
|
||||||
"name": "Coffee-table",
|
"name": "coffee-table",
|
||||||
"thumbnail": "/items/coffee-table/thumbnail.webp",
|
"thumbnail": "/items/coffee-table/thumbnail.webp",
|
||||||
"src": "/items/coffee-table/model.glb",
|
"src": "/items/coffee-table/model.glb",
|
||||||
"scale": [
|
"scale": [
|
||||||
@@ -2566,16 +2567,16 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0
|
0
|
||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
2.0,
|
2,
|
||||||
0.5,
|
0.4,
|
||||||
1.5
|
1.5
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": "computer",
|
"id": "computer",
|
||||||
"category": "furniture",
|
"category": "appliance",
|
||||||
"name": "Computer",
|
"name": "computer",
|
||||||
"thumbnail": "/items/computer/thumbnail.webp",
|
"thumbnail": "/items/computer/thumbnail.webp",
|
||||||
"src": "/items/computer/model.glb",
|
"src": "/items/computer/model.glb",
|
||||||
"scale": [
|
"scale": [
|
||||||
@@ -2624,7 +2625,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
1.5,
|
1.5,
|
||||||
1,
|
0.8,
|
||||||
1.5
|
1.5
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2652,7 +2653,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
1,
|
1,
|
||||||
1.5,
|
1.2,
|
||||||
1
|
1
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2708,7 +2709,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
],
|
],
|
||||||
"dimensions": [
|
"dimensions": [
|
||||||
2.5,
|
2.5,
|
||||||
1,
|
0.8,
|
||||||
1
|
1
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2796,32 +2797,5 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
0.30000000000000004
|
0.30000000000000004
|
||||||
],
|
],
|
||||||
"attachTo": "wall"
|
"attachTo": "wall"
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"id": "couch-medium",
|
|
||||||
"category": "furniture",
|
|
||||||
"name": "Couch",
|
|
||||||
"thumbnail": "/items/couch-medium/thumbnail.webp",
|
|
||||||
"src": "/items/couch-medium/model.glb",
|
|
||||||
"scale": [
|
|
||||||
0.39,
|
|
||||||
0.39,
|
|
||||||
0.39
|
|
||||||
],
|
|
||||||
"offset": [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0.03
|
|
||||||
],
|
|
||||||
"rotation": [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
"dimensions": [
|
|
||||||
2,
|
|
||||||
0.8,
|
|
||||||
1
|
|
||||||
]
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type BuildingNode, type LevelNode, useScene } from '@pascal-app/core'
|
import { type BuildingNode, type ItemNode, type LevelNode, useScene } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import type { Asset } from '../../../packages/core/src/schema/nodes/item'
|
import type { Asset } from '../../../packages/core/src/schema/nodes/item'
|
||||||
@@ -56,6 +56,8 @@ type EditorState = {
|
|||||||
setCatalogCategory: (category: CatalogCategory | null) => void
|
setCatalogCategory: (category: CatalogCategory | null) => void
|
||||||
selectedItem: Asset | null
|
selectedItem: Asset | null
|
||||||
setSelectedItem: (item: Asset) => void
|
setSelectedItem: (item: Asset) => void
|
||||||
|
movingNode: ItemNode | null
|
||||||
|
setMovingNode: (node: ItemNode | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useEditor = create<EditorState>()((set, get) => ({
|
const useEditor = create<EditorState>()((set, get) => ({
|
||||||
@@ -169,6 +171,8 @@ const useEditor = create<EditorState>()((set, get) => ({
|
|||||||
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
||||||
selectedItem: null,
|
selectedItem: null,
|
||||||
setSelectedItem: (item) => set({ selectedItem: item }),
|
setSelectedItem: (item) => set({ selectedItem: item }),
|
||||||
|
movingNode: null,
|
||||||
|
setMovingNode: (node) => set({ movingNode: node }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export default useEditor
|
export default useEditor
|
||||||
|
|||||||
Reference in New Issue
Block a user