duplicate item
This commit is contained in:
@@ -24,13 +24,28 @@ function getInitialState(node: {
|
|||||||
function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {
|
function MoveItemContent({ movingNode }: { movingNode: ItemNode }) {
|
||||||
const draftNode = useDraftNode()
|
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({
|
const cursor = usePlacementCoordinator({
|
||||||
asset: movingNode.asset,
|
asset: movingNode.asset,
|
||||||
draftNode,
|
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) => {
|
initDraft: (gridPosition) => {
|
||||||
|
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)
|
draftNode.adopt(movingNode)
|
||||||
gridPosition.copy(new Vector3(...movingNode.position))
|
gridPosition.copy(new Vector3(...movingNode.position))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onCommitted: () => {
|
onCommitted: () => {
|
||||||
sfxEmitter.emit('sfx:item-place')
|
sfxEmitter.emit('sfx:item-place')
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client'
|
'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 { 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 Image from 'next/image'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import useEditor from '@/store/use-editor'
|
import useEditor from '@/store/use-editor'
|
||||||
@@ -51,6 +51,24 @@ export function ItemPanel() {
|
|||||||
}
|
}
|
||||||
}, [node, setMovingNode, setSelection])
|
}, [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(() => {
|
const handleDelete = useCallback(() => {
|
||||||
if (!selectedId) return
|
if (!selectedId) return
|
||||||
sfxEmitter.emit('sfx:item-delete')
|
sfxEmitter.emit('sfx:item-delete')
|
||||||
@@ -193,6 +211,14 @@ export function ItemPanel() {
|
|||||||
<Move className="h-3.5 w-3.5" />
|
<Move className="h-3.5 w-3.5" />
|
||||||
<span>Move</span>
|
<span>Move</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
type="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"
|
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