move + item corrections
This commit is contained in:
@@ -46,6 +46,11 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
||||
structure: {
|
||||
types: ["wall", "item", "zone", "slab", "ceiling"],
|
||||
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();
|
||||
if (node.type === 'zone') {
|
||||
setSelection({ zoneId: node.id });
|
||||
@@ -88,6 +93,11 @@ const SELECTION_STRATEGIES: Record<string, SelectionStrategy> = {
|
||||
furnish: {
|
||||
types: ["item"],
|
||||
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 nextIds = isShift
|
||||
? selection.selectedIds.includes(node.id)
|
||||
@@ -112,8 +122,11 @@ export const SelectionManager = () => {
|
||||
const phase = useEditor((s) => s.phase);
|
||||
const mode = useEditor((s) => s.mode);
|
||||
|
||||
const movingNode = useEditor((s) => s.movingNode);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "select") return;
|
||||
if (movingNode) return;
|
||||
|
||||
const strategy = SELECTION_STRATEGIES[phase];
|
||||
if (!strategy) return;
|
||||
@@ -158,7 +171,7 @@ export const SelectionManager = () => {
|
||||
});
|
||||
emitter.off("grid:click", onGridClick);
|
||||
};
|
||||
}, [phase, mode]);
|
||||
}, [phase, mode, movingNode]);
|
||||
|
||||
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 { 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 { usePlacementCoordinator } from './use-placement-coordinator'
|
||||
|
||||
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 { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling } = useSpatialQuery()
|
||||
const draftNode = useDraftNode()
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedItem) return
|
||||
|
||||
useScene.temporal.getState().pause()
|
||||
|
||||
const validators = { canPlaceOnFloor, canPlaceOnWall, canPlaceOnCeiling }
|
||||
|
||||
// Reset placement state for new item
|
||||
placementState.current = { surface: 'floor', wallId: null, ceilingId: null }
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
const getContext = () => ({
|
||||
asset: selectedItem,
|
||||
levelId: useViewer.getState().selection.levelId,
|
||||
draftItem: draftNode.current,
|
||||
gridPosition: gridPosition.current,
|
||||
state: { ...placementState.current },
|
||||
const cursor = usePlacementCoordinator({
|
||||
asset: selectedItem!,
|
||||
draftNode,
|
||||
initDraft: (gridPosition) => {
|
||||
if (!selectedItem?.attachTo) {
|
||||
draftNode.create(gridPosition, selectedItem!)
|
||||
}
|
||||
},
|
||||
onCommitted: () => true,
|
||||
})
|
||||
|
||||
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)
|
||||
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>
|
||||
)
|
||||
if (!selectedItem) return null
|
||||
return <>{cursor}</>
|
||||
}
|
||||
|
||||
@@ -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 { 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 {
|
||||
/** Current draft item, or 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: (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
|
||||
/** 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that manages the lifecycle of a transient (draft) item node.
|
||||
* 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 {
|
||||
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 currentLevelId = useViewer.getState().selection.levelId
|
||||
@@ -33,17 +51,77 @@ export function useDraftNode(): DraftNodeHandle {
|
||||
asset,
|
||||
metadata: { isTransient: true },
|
||||
})
|
||||
console.log('create node', node)
|
||||
|
||||
useScene.getState().createNode(node, currentLevelId)
|
||||
draftRef.current = node
|
||||
adoptedRef.current = false
|
||||
originalStateRef.current = null
|
||||
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 draft = draftRef.current
|
||||
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 ?? useViewer.getState().selection.levelId) as AnyNodeId
|
||||
if (!parentId) return null
|
||||
@@ -68,14 +146,33 @@ export function useDraftNode(): DraftNodeHandle {
|
||||
// Re-pause for next draft cycle
|
||||
useScene.temporal.getState().pause()
|
||||
|
||||
adoptedRef.current = false
|
||||
originalStateRef.current = null
|
||||
return finalNode.id
|
||||
}, [])
|
||||
|
||||
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)
|
||||
draftRef.current = null
|
||||
}
|
||||
|
||||
draftRef.current = null
|
||||
adoptedRef.current = false
|
||||
originalStateRef.current = null
|
||||
}, [])
|
||||
|
||||
return useMemo(
|
||||
@@ -83,10 +180,14 @@ export function useDraftNode(): DraftNodeHandle {
|
||||
get current() {
|
||||
return draftRef.current
|
||||
},
|
||||
get isAdopted() {
|
||||
return adoptedRef.current
|
||||
},
|
||||
create,
|
||||
adopt,
|
||||
commit,
|
||||
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 { CeilingTool } from "./ceiling/ceiling-tool";
|
||||
import { ItemTool } from "./item/item-tool";
|
||||
import { MoveTool } from "./item/move-tool";
|
||||
import { SlabTool } from "./slab/slab-tool";
|
||||
import { WallTool } from "./wall/wall-tool";
|
||||
import { ZoneBoundaryEditor } from "./zone/zone-boundary-editor";
|
||||
@@ -25,6 +26,7 @@ export const ToolManager: React.FC = () => {
|
||||
const phase = useEditor((state) => state.phase);
|
||||
const mode = useEditor((state) => state.mode);
|
||||
const tool = useEditor((state) => state.tool);
|
||||
const movingNode = useEditor((state) => state.movingNode);
|
||||
const selectedZoneId = useViewer((state) => state.selection.zoneId);
|
||||
|
||||
// Show zone boundary editor when in structure/select mode with a zone selected
|
||||
@@ -39,7 +41,8 @@ export const ToolManager: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
{showZoneBoundaryEditor && <ZoneBoundaryEditor />}
|
||||
{BuildToolComponent && <BuildToolComponent />}
|
||||
{movingNode && <MoveTool />}
|
||||
{!movingNode && BuildToolComponent && <BuildToolComponent />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "hydrant",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "hydrant",
|
||||
"thumbnail": "/items/hydrant/thumbnail.webp",
|
||||
"src": "/items/hydrant/model.glb",
|
||||
@@ -58,7 +58,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "sunbed",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "sunbed",
|
||||
"thumbnail": "/items/sunbed/thumbnail.webp",
|
||||
"src": "/items/sunbed/model.glb",
|
||||
@@ -86,19 +86,19 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "palm",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "palm",
|
||||
"thumbnail": "/items/palm/thumbnail.webp",
|
||||
"src": "/items/palm/model.glb",
|
||||
"scale": [
|
||||
0.37,
|
||||
0.37000000000000005,
|
||||
0.37,
|
||||
0.37
|
||||
],
|
||||
"offset": [
|
||||
0.13999999999999999,
|
||||
0,
|
||||
-0.20000000000000007
|
||||
0,
|
||||
0.02
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
@@ -106,15 +106,15 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
3.500000000000003,
|
||||
4.500000000000007,
|
||||
3.500000000000004
|
||||
1,
|
||||
4.5,
|
||||
1
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "fence",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "fence",
|
||||
"thumbnail": "/items/fence/thumbnail.webp",
|
||||
"src": "/items/fence/model.glb",
|
||||
@@ -142,7 +142,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "patio-umbrella",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "patio-umbrella",
|
||||
"thumbnail": "/items/patio-umbrella/thumbnail.webp",
|
||||
"src": "/items/patio-umbrella/model.glb",
|
||||
@@ -154,7 +154,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
"offset": [
|
||||
0,
|
||||
0,
|
||||
0.09
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
@@ -162,9 +162,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
3.500000000000003,
|
||||
3.0000000000000013,
|
||||
3.500000000000002
|
||||
1,
|
||||
3,
|
||||
1
|
||||
]
|
||||
},
|
||||
|
||||
@@ -198,7 +198,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "smoke-detector",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "smoke-detector",
|
||||
"thumbnail": "/items/smoke-detector/thumbnail.webp",
|
||||
"src": "/items/smoke-detector/model.glb",
|
||||
@@ -256,7 +256,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "sprinkler",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "sprinkler",
|
||||
"thumbnail": "/items/sprinkler/thumbnail.webp",
|
||||
"src": "/items/sprinkler/model.glb",
|
||||
@@ -314,7 +314,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "ceiling-fan",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "ceiling-fan",
|
||||
"thumbnail": "/items/ceiling-fan/thumbnail.webp",
|
||||
"src": "/items/ceiling-fan/model.glb",
|
||||
@@ -363,9 +363,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
2,
|
||||
2,
|
||||
0.4
|
||||
1.9999999999999991,
|
||||
2.0000000000000004,
|
||||
0.39999999999999936
|
||||
],
|
||||
"attachTo": "wall"
|
||||
},
|
||||
@@ -401,7 +401,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "glass-door",
|
||||
"category": "furniture",
|
||||
"category": "door",
|
||||
"name": "glass-door",
|
||||
"thumbnail": "/items/glass-door/thumbnail.webp",
|
||||
"src": "/items/glass-door/model.glb",
|
||||
@@ -430,7 +430,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "door",
|
||||
"category": "furniture",
|
||||
"category": "door",
|
||||
"name": "door",
|
||||
"thumbnail": "/items/door/thumbnail.webp",
|
||||
"src": "/items/door/model.glb",
|
||||
@@ -459,7 +459,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "thermostat",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "thermostat",
|
||||
"thumbnail": "/items/thermostat/thumbnail.webp",
|
||||
"src": "/items/thermostat/model.glb",
|
||||
@@ -489,7 +489,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "parking-spot",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "parking-spot",
|
||||
"thumbnail": "/items/parking-spot/thumbnail.webp",
|
||||
"src": "/items/parking-spot/model.glb",
|
||||
@@ -517,7 +517,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "ac-block",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "ac-block",
|
||||
"thumbnail": "/items/ac-block/thumbnail.webp",
|
||||
"src": "/items/ac-block/model.glb",
|
||||
@@ -545,7 +545,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "fire-detector",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "fire-detector",
|
||||
"thumbnail": "/items/fire-detector/thumbnail.webp",
|
||||
"src": "/items/fire-detector/model.glb",
|
||||
@@ -574,7 +574,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "exit-sign",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "exit-sign",
|
||||
"thumbnail": "/items/exit-sign/thumbnail.webp",
|
||||
"src": "/items/exit-sign/model.glb",
|
||||
@@ -594,28 +594,28 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
0.9999999999999999,
|
||||
1,
|
||||
0.5,
|
||||
0.30000000000000004
|
||||
0.3
|
||||
],
|
||||
"attachTo": "wall"
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "electric-panel",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "electric-panel",
|
||||
"thumbnail": "/items/electric-panel/thumbnail.webp",
|
||||
"src": "/items/electric-panel/model.glb",
|
||||
"scale": [
|
||||
0.6100000000000002,
|
||||
0.7399999999999999,
|
||||
0.61,
|
||||
0.74,
|
||||
0.7
|
||||
],
|
||||
"offset": [
|
||||
0,
|
||||
0,
|
||||
0.060000000000000005
|
||||
0.06
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
@@ -623,17 +623,17 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
0.5000000000000001,
|
||||
0.9999999999999999,
|
||||
0.30000000000000004
|
||||
0.5,
|
||||
1,
|
||||
0.3
|
||||
],
|
||||
"attachTo": "wall"
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"id": "alarm-keypad",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "alarm-keypad",
|
||||
"thumbnail": "/items/alarm-keypad/thumbnail.webp",
|
||||
"src": "/items/alarm-keypad/model.glb",
|
||||
@@ -671,9 +671,9 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
1
|
||||
],
|
||||
"offset": [
|
||||
-0.08,
|
||||
0,
|
||||
0.020000000000000004
|
||||
0.1,
|
||||
0.01
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
@@ -681,10 +681,11 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
0.500000000000001,
|
||||
0.30000000000000004,
|
||||
0.5000000000000003
|
||||
]
|
||||
1,
|
||||
0.5,
|
||||
0.7
|
||||
],
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
{
|
||||
@@ -745,7 +746,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "skate",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "skate",
|
||||
"thumbnail": "/items/skate/thumbnail.webp",
|
||||
"src": "/items/skate/model.glb",
|
||||
@@ -801,7 +802,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "outdoor-playhouse",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "outdoor-playhouse",
|
||||
"thumbnail": "/items/outdoor-playhouse/thumbnail.webp",
|
||||
"src": "/items/outdoor-playhouse/model.glb",
|
||||
@@ -829,7 +830,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "scooter",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "scooter",
|
||||
"thumbnail": "/items/scooter/thumbnail.webp",
|
||||
"src": "/items/scooter/model.glb",
|
||||
@@ -857,7 +858,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "basket-hoop",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "basket-hoop",
|
||||
"thumbnail": "/items/basket-hoop/thumbnail.webp",
|
||||
"src": "/items/basket-hoop/model.glb",
|
||||
@@ -941,7 +942,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "hood",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "hood",
|
||||
"thumbnail": "/items/hood/thumbnail.webp",
|
||||
"src": "/items/hood/model.glb",
|
||||
@@ -952,8 +953,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"offset": [
|
||||
0,
|
||||
0.5200000000000002,
|
||||
0.00999999999999999
|
||||
0.52,
|
||||
0.01
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
@@ -961,16 +962,16 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
1.5000000000000016,
|
||||
1.5,
|
||||
1,
|
||||
1.1000000000000003
|
||||
1.1
|
||||
],
|
||||
"attachTo": "wall"
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "kitchen-shelf",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "kitchen-shelf",
|
||||
"thumbnail": "/items/kitchen-shelf/thumbnail.webp",
|
||||
"src": "/items/kitchen-shelf/model.glb",
|
||||
@@ -981,8 +982,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"offset": [
|
||||
0,
|
||||
0.5200000000000002,
|
||||
0.00999999999999999
|
||||
0.52,
|
||||
0.01
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
@@ -990,11 +991,11 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
2.500000000000002,
|
||||
2.5,
|
||||
1,
|
||||
1.1000000000000003
|
||||
1.1
|
||||
],
|
||||
"attachTo": "wall"
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
{
|
||||
@@ -1027,7 +1028,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "drying-rack",
|
||||
"category": "furniture",
|
||||
"category": "bathroom",
|
||||
"name": "drying-rack",
|
||||
"thumbnail": "/items/drying-rack/thumbnail.webp",
|
||||
"src": "/items/drying-rack/model.glb",
|
||||
@@ -1066,8 +1067,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"offset": [
|
||||
0,
|
||||
0.19000000000000003,
|
||||
0.11999999999999998
|
||||
0.19,
|
||||
0.12
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
@@ -1075,16 +1076,16 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
0.5000000000000008,
|
||||
0.5,
|
||||
0.5000000000000003
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "cutting-board",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "cutting-board",
|
||||
"thumbnail": "/items/cutting-board/thumbnail.webp",
|
||||
"src": "/items/cutting-board/model.glb",
|
||||
@@ -1112,7 +1113,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "frying-pan",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "frying-pan",
|
||||
"thumbnail": "/items/frying-pan/thumbnail.webp",
|
||||
"src": "/items/frying-pan/model.glb",
|
||||
@@ -1140,7 +1141,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "kitchen-utensils",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "kitchen-utensils",
|
||||
"thumbnail": "/items/kitchen-utensils/thumbnail.webp",
|
||||
"src": "/items/kitchen-utensils/model.glb",
|
||||
@@ -1168,7 +1169,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "kitchen-counter",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "kitchen-counter",
|
||||
"thumbnail": "/items/kitchen-counter/thumbnail.webp",
|
||||
"src": "/items/kitchen-counter/model.glb",
|
||||
@@ -1196,7 +1197,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "kitchen-cabinet",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "kitchen-cabinet",
|
||||
"thumbnail": "/items/kitchen-cabinet/thumbnail.webp",
|
||||
"src": "/items/kitchen-cabinet/model.glb",
|
||||
@@ -1336,7 +1337,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "television",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "television",
|
||||
"thumbnail": "/items/television/thumbnail.webp",
|
||||
"src": "/items/television/model.glb",
|
||||
@@ -1392,7 +1393,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "air-conditioning",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "air-conditioning",
|
||||
"thumbnail": "/items/air-conditioning/thumbnail.webp",
|
||||
"src": "/items/air-conditioning/model.glb",
|
||||
@@ -1403,8 +1404,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"offset": [
|
||||
0,
|
||||
0.37000000000000016,
|
||||
0.21000000000000005
|
||||
0.37,
|
||||
0.21
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
@@ -1414,14 +1415,14 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
"dimensions": [
|
||||
2,
|
||||
1,
|
||||
0.8999999999999999
|
||||
0.9
|
||||
],
|
||||
"attachTo": "wall"
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "sewing-machine",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "sewing-machine",
|
||||
"thumbnail": "/items/sewing-machine/thumbnail.webp",
|
||||
"src": "/items/sewing-machine/model.glb",
|
||||
@@ -1533,7 +1534,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "toaster",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "toaster",
|
||||
"thumbnail": "/items/toaster/thumbnail.webp",
|
||||
"src": "/items/toaster/model.glb",
|
||||
@@ -1618,7 +1619,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "kettle",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "kettle",
|
||||
"thumbnail": "/items/kettle/thumbnail.webp",
|
||||
"src": "/items/kettle/model.glb",
|
||||
@@ -1646,7 +1647,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "iron",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "iron",
|
||||
"thumbnail": "/items/iron/thumbnail.webp",
|
||||
"src": "/items/iron/model.glb",
|
||||
@@ -1760,7 +1761,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "coffee-machine",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "coffee-machine",
|
||||
"thumbnail": "/items/coffee-machine/thumbnail.webp",
|
||||
"src": "/items/coffee-machine/model.glb",
|
||||
@@ -1788,7 +1789,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "wine-bottle",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "wine-bottle",
|
||||
"thumbnail": "/items/wine-bottle/thumbnail.webp",
|
||||
"src": "/items/wine-bottle/model.glb",
|
||||
@@ -1816,7 +1817,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "fruits",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "fruits",
|
||||
"thumbnail": "/items/fruits/thumbnail.webp",
|
||||
"src": "/items/fruits/model.glb",
|
||||
@@ -1868,7 +1869,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
1,
|
||||
0.1
|
||||
],
|
||||
"attachTo": "wall"
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
{
|
||||
@@ -1901,7 +1902,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "ball",
|
||||
"category": "furniture",
|
||||
"category": "outdoor",
|
||||
"name": "ball",
|
||||
"thumbnail": "/items/ball/thumbnail.webp",
|
||||
"src": "/items/ball/model.glb",
|
||||
@@ -1950,7 +1951,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"dimensions": [
|
||||
1.5,
|
||||
1,
|
||||
0.8,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -2037,12 +2038,12 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
1,
|
||||
0.2
|
||||
],
|
||||
"attachTo": "wall"
|
||||
"attachTo": "wall-side"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "stereo-speaker",
|
||||
"category": "furniture",
|
||||
"category": "appliance",
|
||||
"name": "stereo-speaker",
|
||||
"thumbnail": "/items/stereo-speaker/thumbnail.webp",
|
||||
"src": "/items/stereo-speaker/model.glb",
|
||||
@@ -2098,7 +2099,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "stove",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "stove",
|
||||
"thumbnail": "/items/stove/thumbnail.webp",
|
||||
"src": "/items/stove/model.glb",
|
||||
@@ -2126,7 +2127,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "fridge",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "fridge",
|
||||
"thumbnail": "/items/fridge/thumbnail.webp",
|
||||
"src": "/items/fridge/model.glb",
|
||||
@@ -2154,7 +2155,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "kitchen",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "kitchen",
|
||||
"thumbnail": "/items/kitchen/thumbnail.webp",
|
||||
"src": "/items/kitchen/model.glb",
|
||||
@@ -2182,7 +2183,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "microwave",
|
||||
"category": "furniture",
|
||||
"category": "kitchen",
|
||||
"name": "microwave",
|
||||
"thumbnail": "/items/microwave/thumbnail.webp",
|
||||
"src": "/items/microwave/model.glb",
|
||||
@@ -2322,7 +2323,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "washing-machine",
|
||||
"category": "furniture",
|
||||
"category": "bathroom",
|
||||
"name": "washing-machine",
|
||||
"thumbnail": "/items/washing-machine/thumbnail.webp",
|
||||
"src": "/items/washing-machine/model.glb",
|
||||
@@ -2350,7 +2351,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "laundry-bag",
|
||||
"category": "furniture",
|
||||
"category": "bathroom",
|
||||
"name": "laundry-bag",
|
||||
"thumbnail": "/items/laundry-bag/thumbnail.webp",
|
||||
"src": "/items/laundry-bag/model.glb",
|
||||
@@ -2378,7 +2379,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "shower-angle",
|
||||
"category": "furniture",
|
||||
"category": "bathroom",
|
||||
"name": "shower-angle",
|
||||
"thumbnail": "/items/shower-angle/thumbnail.webp",
|
||||
"src": "/items/shower-angle/model.glb",
|
||||
@@ -2406,7 +2407,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "bathtub",
|
||||
"category": "furniture",
|
||||
"category": "bathroom",
|
||||
"name": "bathtub",
|
||||
"thumbnail": "/items/bathtub/thumbnail.webp",
|
||||
"src": "/items/bathtub/model.glb",
|
||||
@@ -2427,14 +2428,14 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"dimensions": [
|
||||
2.5,
|
||||
1,
|
||||
0.8,
|
||||
1.5
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "toilet",
|
||||
"category": "furniture",
|
||||
"category": "bathroom",
|
||||
"name": "toilet",
|
||||
"thumbnail": "/items/toilet/thumbnail.webp",
|
||||
"src": "/items/toilet/model.glb",
|
||||
@@ -2455,7 +2456,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"dimensions": [
|
||||
1,
|
||||
1,
|
||||
0.9,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -2483,14 +2484,14 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"dimensions": [
|
||||
2.5,
|
||||
1,
|
||||
0.8,
|
||||
1.5
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "shower-square",
|
||||
"category": "furniture",
|
||||
"category": "bathroom",
|
||||
"name": "shower-square",
|
||||
"thumbnail": "/items/shower-square/thumbnail.webp",
|
||||
"src": "/items/shower-square/model.glb",
|
||||
@@ -2518,7 +2519,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
|
||||
{
|
||||
"id": "bathroom-sink",
|
||||
"category": "furniture",
|
||||
"category": "bathroom",
|
||||
"name": "bathroom-sink",
|
||||
"thumbnail": "/items/bathroom-sink/thumbnail.webp",
|
||||
"src": "/items/bathroom-sink/model.glb",
|
||||
@@ -2547,7 +2548,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
{
|
||||
"id": "coffee-table",
|
||||
"category": "furniture",
|
||||
"name": "Coffee-table",
|
||||
"name": "coffee-table",
|
||||
"thumbnail": "/items/coffee-table/thumbnail.webp",
|
||||
"src": "/items/coffee-table/model.glb",
|
||||
"scale": [
|
||||
@@ -2566,16 +2567,16 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0
|
||||
],
|
||||
"dimensions": [
|
||||
2.0,
|
||||
0.5,
|
||||
2,
|
||||
0.4,
|
||||
1.5
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "computer",
|
||||
"category": "furniture",
|
||||
"name": "Computer",
|
||||
"category": "appliance",
|
||||
"name": "computer",
|
||||
"thumbnail": "/items/computer/thumbnail.webp",
|
||||
"src": "/items/computer/model.glb",
|
||||
"scale": [
|
||||
@@ -2624,7 +2625,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"dimensions": [
|
||||
1.5,
|
||||
1,
|
||||
0.8,
|
||||
1.5
|
||||
]
|
||||
},
|
||||
@@ -2652,7 +2653,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"dimensions": [
|
||||
1,
|
||||
1.5,
|
||||
1.2,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -2708,7 +2709,7 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
],
|
||||
"dimensions": [
|
||||
2.5,
|
||||
1,
|
||||
0.8,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -2796,32 +2797,5 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
||||
0.30000000000000004
|
||||
],
|
||||
"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'
|
||||
|
||||
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 { create } from 'zustand'
|
||||
import type { Asset } from '../../../packages/core/src/schema/nodes/item'
|
||||
@@ -56,6 +56,8 @@ type EditorState = {
|
||||
setCatalogCategory: (category: CatalogCategory | null) => void
|
||||
selectedItem: Asset | null
|
||||
setSelectedItem: (item: Asset) => void
|
||||
movingNode: ItemNode | null
|
||||
setMovingNode: (node: ItemNode | null) => void
|
||||
}
|
||||
|
||||
const useEditor = create<EditorState>()((set, get) => ({
|
||||
@@ -169,6 +171,8 @@ const useEditor = create<EditorState>()((set, get) => ({
|
||||
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
||||
selectedItem: null,
|
||||
setSelectedItem: (item) => set({ selectedItem: item }),
|
||||
movingNode: null,
|
||||
setMovingNode: (node) => set({ movingNode: node }),
|
||||
}))
|
||||
|
||||
export default useEditor
|
||||
|
||||
Reference in New Issue
Block a user