Merge pull request #433 from pascalorg/chore/editor-polish
Editor polish: paint SFX, duplicate keeps slots, review-skill convention
This commit is contained in:
@@ -1157,6 +1157,7 @@ export const SelectionManager = () => {
|
||||
}
|
||||
|
||||
interaction.apply()
|
||||
sfxEmitter.emit('sfx:paint-apply')
|
||||
if (activePreview?.key === interaction.key) {
|
||||
activePreview = null
|
||||
} else {
|
||||
|
||||
@@ -27,12 +27,14 @@ export interface DraftNodeHandle {
|
||||
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.
|
||||
* `slots` seeds painted slot overrides so duplicates keep their materials. */
|
||||
create: (
|
||||
gridPosition: Vector3,
|
||||
asset: AssetInput,
|
||||
rotation?: [number, number, number],
|
||||
scale?: [number, number, number],
|
||||
slots?: ItemNode['slots'],
|
||||
) => ItemNode | null
|
||||
/** Take ownership of an existing scene node as the draft (for move mode). */
|
||||
adopt: (node: ItemNode) => void
|
||||
@@ -61,6 +63,7 @@ export function useDraftNode(): DraftNodeHandle {
|
||||
asset: AssetInput,
|
||||
rotation?: [number, number, number],
|
||||
scale?: [number, number, number],
|
||||
slots?: ItemNode['slots'],
|
||||
): ItemNode | null => {
|
||||
const currentLevelId = useViewer.getState().selection.levelId
|
||||
if (!currentLevelId) return null
|
||||
@@ -73,6 +76,7 @@ export function useDraftNode(): DraftNodeHandle {
|
||||
asset,
|
||||
parentId: currentLevelId,
|
||||
metadata: { isTransient: true },
|
||||
...(slots ? { slots } : {}),
|
||||
})
|
||||
|
||||
useScene.getState().createNode(node, currentLevelId)
|
||||
@@ -180,6 +184,8 @@ export function useDraftNode(): DraftNodeHandle {
|
||||
rotation: updateProps.rotation ?? draft.rotation,
|
||||
scale: updateProps.scale ?? draft.scale,
|
||||
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
|
||||
// forwarded explicitly.
|
||||
roofSegmentId: updateProps.roofSegmentId,
|
||||
|
||||
@@ -192,6 +192,9 @@ export interface PlacementCoordinatorConfig {
|
||||
initialState?: PlacementState
|
||||
/** Scale to use when lazily creating a draft (e.g. for wall/ceiling duplicates). Defaults to [1,1,1]. */
|
||||
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
|
||||
* (floor / wall / ceiling / item-surface / shelf) instead of snapping the
|
||||
* item's origin under the cursor. */
|
||||
@@ -512,7 +515,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
||||
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
|
||||
if (draft) {
|
||||
@@ -857,7 +866,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
||||
|
||||
draftNode.commit(result.nodeUpdate)
|
||||
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(
|
||||
getFallbackPreviewBounds(draftNode.current, asset, asset.attachTo),
|
||||
asset.attachTo,
|
||||
|
||||
@@ -16,6 +16,7 @@ type SFXEvents = {
|
||||
'sfx:snapshot-capture': undefined
|
||||
'sfx:menu-hover': undefined
|
||||
'sfx:menu-click': undefined
|
||||
'sfx:paint-apply': undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,6 +46,7 @@ export function initSFXBus() {
|
||||
sfxEmitter.on('sfx:snapshot-capture', () => playSFX('snapshotCapture'))
|
||||
sfxEmitter.on('sfx:menu-hover', () => playSFX('menuHover'))
|
||||
sfxEmitter.on('sfx:menu-click', () => playSFX('menuClick'))
|
||||
sfxEmitter.on('sfx:paint-apply', () => playSFX('paintApply'))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,6 +100,15 @@ export const SFX: Record<string, SFXConfig> = {
|
||||
volumeRange: [0.5, 0.6],
|
||||
panJitter: 0.1,
|
||||
},
|
||||
// Fired when a material is applied to a surface in paint mode. Painting can
|
||||
// fire in quick succession across faces, so keep variation + a small gap.
|
||||
paintApply: {
|
||||
src: '/audios/sfx/paint_apply.mp3',
|
||||
rateRange: [0.95, 1.05],
|
||||
volumeRange: [0.85, 1.0],
|
||||
panJitter: 0.12,
|
||||
minIntervalMs: 60,
|
||||
},
|
||||
} as const
|
||||
|
||||
export type SFXName = keyof typeof SFX
|
||||
|
||||
@@ -115,6 +115,9 @@ export function MoveItemTool({ node }: { node: ItemNode }) {
|
||||
const cursor = usePlacementCoordinator({
|
||||
asset: node.asset,
|
||||
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.
|
||||
initialState: isNew
|
||||
? {
|
||||
@@ -135,7 +138,7 @@ export function MoveItemTool({ node }: { node: ItemNode }) {
|
||||
// items are created lazily on surface entry.
|
||||
gridPosition.copy(new Vector3(...node.position))
|
||||
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 {
|
||||
draftNode.adopt(node)
|
||||
|
||||
Reference in New Issue
Block a user