duplicate item
This commit is contained in:
@@ -24,13 +24,28 @@ function getInitialState(node: {
|
||||
function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {
|
||||
const draftNode = useDraftNode()
|
||||
|
||||
const meta = (typeof movingNode.metadata === 'object' && movingNode.metadata !== null)
|
||||
? movingNode.metadata as Record<string, unknown>
|
||||
: {}
|
||||
const isNew = !!meta.isNew
|
||||
|
||||
const cursor = usePlacementCoordinator({
|
||||
asset: movingNode.asset,
|
||||
draftNode,
|
||||
initialState: getInitialState(movingNode),
|
||||
// Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft
|
||||
initialState: isNew ? { surface: 'floor', wallId: null, ceilingId: null, surfaceItemId: null } : getInitialState(movingNode),
|
||||
initDraft: (gridPosition) => {
|
||||
draftNode.adopt(movingNode)
|
||||
gridPosition.copy(new Vector3(...movingNode.position))
|
||||
if (isNew) {
|
||||
// Duplicate: use the same create() path as ItemTool so ghost rendering works correctly.
|
||||
// Floor items get a draft immediately; wall/ceiling items are created lazily on surface entry.
|
||||
gridPosition.copy(new Vector3(...movingNode.position))
|
||||
if (!movingNode.asset.attachTo) {
|
||||
draftNode.create(gridPosition, movingNode.asset, movingNode.rotation)
|
||||
}
|
||||
} else {
|
||||
draftNode.adopt(movingNode)
|
||||
gridPosition.copy(new Vector3(...movingNode.position))
|
||||
}
|
||||
},
|
||||
onCommitted: () => {
|
||||
sfxEmitter.emit('sfx:item-place')
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { type AnyNode, type ItemNode, useScene } from '@pascal-app/core'
|
||||
import { type AnyNode, ItemNode, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { Move, Trash2, X } from 'lucide-react'
|
||||
import { Copy, Move, Trash2, X } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useCallback } from 'react'
|
||||
import useEditor from '@/store/use-editor'
|
||||
@@ -51,6 +51,24 @@ export function ItemPanel() {
|
||||
}
|
||||
}, [node, setMovingNode, setSelection])
|
||||
|
||||
const handleDuplicate = useCallback(() => {
|
||||
if (!node) return
|
||||
sfxEmitter.emit('sfx:item-pick')
|
||||
// Create a proto node (not added to scene) as a carrier for asset/position info.
|
||||
// MoveItemContent detects metadata.isNew and uses draftNode.create() so ghost rendering works correctly.
|
||||
const proto = ItemNode.parse({
|
||||
position: [...node.position] as [number, number, number],
|
||||
rotation: [...node.rotation] as [number, number, number],
|
||||
name: node.name,
|
||||
asset: node.asset,
|
||||
parentId: node.parentId,
|
||||
side: node.side,
|
||||
metadata: { isNew: true },
|
||||
})
|
||||
setMovingNode(proto)
|
||||
setSelection({ selectedIds: [] })
|
||||
}, [node, setMovingNode, setSelection])
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!selectedId) return
|
||||
sfxEmitter.emit('sfx:item-delete')
|
||||
@@ -193,6 +211,14 @@ export function ItemPanel() {
|
||||
<Move className="h-3.5 w-3.5" />
|
||||
<span>Move</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 flex items-center justify-center gap-1.5 rounded border border-border px-2 py-1.5 text-xs hover:bg-accent cursor-pointer"
|
||||
onClick={handleDuplicate}
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
<span>Duplicate</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex-1 flex items-center justify-center gap-1.5 rounded border border-border px-2 py-1.5 text-xs hover:bg-accent cursor-pointer"
|
||||
|
||||
Reference in New Issue
Block a user