fix(item): duplicated items keep their painted slot materials
Duplicating an item drops to the catalog placement flow, which rebuilds the draft from the asset + transform and never carried node.slots — so the copy lost every painted slot override. Thread slots through the draft create path: useDraftNode.create seeds it onto the draft and commit() forwards it to the final node, the placement coordinator passes it to its lazy wall/ceiling draft creates, and the item move tool supplies node.slots for both the floor (direct create) and wall/ceiling (coordinator) duplicate paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bf9246cd36
commit
115a142d83
@@ -27,12 +27,14 @@ export interface DraftNodeHandle {
|
|||||||
readonly current: ItemNode | null
|
readonly current: ItemNode | null
|
||||||
/** Whether the current draft was adopted (move mode) vs created (create mode) */
|
/** Whether the current draft was adopted (move mode) vs created (create mode) */
|
||||||
readonly isAdopted: boolean
|
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.
|
||||||
|
* `slots` seeds painted slot overrides so duplicates keep their materials. */
|
||||||
create: (
|
create: (
|
||||||
gridPosition: Vector3,
|
gridPosition: Vector3,
|
||||||
asset: AssetInput,
|
asset: AssetInput,
|
||||||
rotation?: [number, number, number],
|
rotation?: [number, number, number],
|
||||||
scale?: [number, number, number],
|
scale?: [number, number, number],
|
||||||
|
slots?: ItemNode['slots'],
|
||||||
) => ItemNode | null
|
) => ItemNode | null
|
||||||
/** Take ownership of an existing scene node as the draft (for move mode). */
|
/** Take ownership of an existing scene node as the draft (for move mode). */
|
||||||
adopt: (node: ItemNode) => void
|
adopt: (node: ItemNode) => void
|
||||||
@@ -61,6 +63,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|||||||
asset: AssetInput,
|
asset: AssetInput,
|
||||||
rotation?: [number, number, number],
|
rotation?: [number, number, number],
|
||||||
scale?: [number, number, number],
|
scale?: [number, number, number],
|
||||||
|
slots?: ItemNode['slots'],
|
||||||
): ItemNode | null => {
|
): ItemNode | null => {
|
||||||
const currentLevelId = useViewer.getState().selection.levelId
|
const currentLevelId = useViewer.getState().selection.levelId
|
||||||
if (!currentLevelId) return null
|
if (!currentLevelId) return null
|
||||||
@@ -73,6 +76,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|||||||
asset,
|
asset,
|
||||||
parentId: currentLevelId,
|
parentId: currentLevelId,
|
||||||
metadata: { isTransient: true },
|
metadata: { isTransient: true },
|
||||||
|
...(slots ? { slots } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
useScene.getState().createNode(node, currentLevelId)
|
useScene.getState().createNode(node, currentLevelId)
|
||||||
@@ -180,6 +184,8 @@ export function useDraftNode(): DraftNodeHandle {
|
|||||||
rotation: updateProps.rotation ?? draft.rotation,
|
rotation: updateProps.rotation ?? draft.rotation,
|
||||||
scale: updateProps.scale ?? draft.scale,
|
scale: updateProps.scale ?? draft.scale,
|
||||||
side: updateProps.side ?? draft.side,
|
side: updateProps.side ?? draft.side,
|
||||||
|
// Carry painted slot overrides so a duplicated item keeps its materials.
|
||||||
|
...(draft.slots ? { slots: draft.slots } : {}),
|
||||||
// Roof host — see the move-mode commit above for why this must be
|
// Roof host — see the move-mode commit above for why this must be
|
||||||
// forwarded explicitly.
|
// forwarded explicitly.
|
||||||
roofSegmentId: updateProps.roofSegmentId,
|
roofSegmentId: updateProps.roofSegmentId,
|
||||||
|
|||||||
@@ -192,6 +192,9 @@ export interface PlacementCoordinatorConfig {
|
|||||||
initialState?: PlacementState
|
initialState?: PlacementState
|
||||||
/** Scale to use when lazily creating a draft (e.g. for wall/ceiling duplicates). Defaults to [1,1,1]. */
|
/** Scale to use when lazily creating a draft (e.g. for wall/ceiling duplicates). Defaults to [1,1,1]. */
|
||||||
defaultScale?: [number, number, number]
|
defaultScale?: [number, number, number]
|
||||||
|
/** Painted slot overrides to seed onto a lazily-created draft (wall/ceiling
|
||||||
|
* duplicates) so the duplicate keeps its materials. */
|
||||||
|
slots?: ItemNode['slots']
|
||||||
/** Move-mode sessions keep the grabbed item offset from the first surface hit
|
/** Move-mode sessions keep the grabbed item offset from the first surface hit
|
||||||
* (floor / wall / ceiling / item-surface / shelf) instead of snapping the
|
* (floor / wall / ceiling / item-surface / shelf) instead of snapping the
|
||||||
* item's origin under the cursor. */
|
* item's origin under the cursor. */
|
||||||
@@ -512,7 +515,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
0,
|
0,
|
||||||
]
|
]
|
||||||
|
|
||||||
draftNode.create(gridPosition.current, asset, initRotation, configRef.current.defaultScale)
|
draftNode.create(
|
||||||
|
gridPosition.current,
|
||||||
|
asset,
|
||||||
|
initRotation,
|
||||||
|
configRef.current.defaultScale,
|
||||||
|
configRef.current.slots,
|
||||||
|
)
|
||||||
|
|
||||||
const draft = draftNode.current
|
const draft = draftNode.current
|
||||||
if (draft) {
|
if (draft) {
|
||||||
@@ -857,7 +866,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|||||||
|
|
||||||
draftNode.commit(result.nodeUpdate)
|
draftNode.commit(result.nodeUpdate)
|
||||||
if (configRef.current.onCommitted()) {
|
if (configRef.current.onCommitted()) {
|
||||||
draftNode.create(gridPosition.current, asset, currentRotation)
|
draftNode.create(
|
||||||
|
gridPosition.current,
|
||||||
|
asset,
|
||||||
|
currentRotation,
|
||||||
|
configRef.current.defaultScale,
|
||||||
|
configRef.current.slots,
|
||||||
|
)
|
||||||
const previewBounds = expandBoundsToGrid(
|
const previewBounds = expandBoundsToGrid(
|
||||||
getFallbackPreviewBounds(draftNode.current, asset, asset.attachTo),
|
getFallbackPreviewBounds(draftNode.current, asset, asset.attachTo),
|
||||||
asset.attachTo,
|
asset.attachTo,
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ export function MoveItemTool({ node }: { node: ItemNode }) {
|
|||||||
const cursor = usePlacementCoordinator({
|
const cursor = usePlacementCoordinator({
|
||||||
asset: node.asset,
|
asset: node.asset,
|
||||||
draftNode,
|
draftNode,
|
||||||
|
// Carry painted slot overrides onto the duplicate's draft (wall/ceiling
|
||||||
|
// items create their draft lazily inside the coordinator).
|
||||||
|
slots: node.slots,
|
||||||
// Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft.
|
// Duplicates start fresh in floor mode; wall/ceiling draft is created lazily by ensureDraft.
|
||||||
initialState: isNew
|
initialState: isNew
|
||||||
? {
|
? {
|
||||||
@@ -135,7 +138,7 @@ export function MoveItemTool({ node }: { node: ItemNode }) {
|
|||||||
// items are created lazily on surface entry.
|
// items are created lazily on surface entry.
|
||||||
gridPosition.copy(new Vector3(...node.position))
|
gridPosition.copy(new Vector3(...node.position))
|
||||||
if (!node.asset.attachTo) {
|
if (!node.asset.attachTo) {
|
||||||
draftNode.create(gridPosition, node.asset, node.rotation, node.scale)
|
draftNode.create(gridPosition, node.asset, node.rotation, node.scale, node.slots)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
draftNode.adopt(node)
|
draftNode.adopt(node)
|
||||||
|
|||||||
Reference in New Issue
Block a user