editor: structure build-start/end cues + menu hover/click SFX (#378)

* wip sfx

* wip audios

* feat(editor): structure build-start/end cues + menu hover/click SFX

Sound design pass for the build/items experience:

- Split structure-build into a start cue (draft begins) and end cue
  (commit). Wall/fence fire start on the first click; slab/ceiling/zone
  tick start on every non-closing vertex; roof on the first corner.
- Doors and windows now use the structure-end cue on place + move;
  shelves use item-place instead.
- New menu_hover (round-robin retired in favour of a single sample) and
  menu_click cues wired to the Build/Items main categories, item-catalog
  tiles, the icon rail / tab bar, and the bottom action menu (via the
  shared ActionButton).
- Refresh item_pick / item_place / item_rotate samples; drop the
  now-unused numbered menu_hover variations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(editor): silence forEach return lint in updateSFXVolumes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(editor): convert SFX wavs to mp3 to shrink assets

Re-encode all SFX samples as 128kbps mp3 (~94% smaller, transparent for
short UI cues) and point the player at the .mp3 sources. Drops the .wav
originals; the freshly converted item_pick/place/rotate.mp3 override the
older committed mp3s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-06 20:15:24 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 8f2e5678e8
commit 1419709131
31 changed files with 148 additions and 34 deletions
+3
View File
@@ -201,6 +201,9 @@ export const CeilingTool: React.FC = () => {
setPoints([])
useAlignmentGuides.getState().clear()
} else {
// Every non-closing vertex is a "start" tick; the closing click above
// fires the structure-build (end) cue.
triggerSFX('sfx:structure-build-start')
setPoints([...points, clickPoint])
}
}
+1 -1
View File
@@ -349,7 +349,7 @@ const MoveDoorTool: React.FC<{ node: DoorNode }> = ({ node: movingDoorNode }) =>
useLiveTransforms.getState().clear(movingDoorNode.id)
useScene.temporal.getState().pause()
triggerSFX('sfx:item-place')
triggerSFX('sfx:structure-build')
hideCursor()
useViewer.getState().setSelection({ selectedIds: [placedId] })
exitMoveMode()
+1 -1
View File
@@ -323,7 +323,7 @@ const DoorTool: React.FC = () => {
useScene.getState().createNode(node, event.node.id as AnyNodeId)
useViewer.getState().setSelection({ selectedIds: [node.id] })
useScene.temporal.getState().pause()
triggerSFX('sfx:item-place')
triggerSFX('sfx:structure-build')
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '')
useAlignmentGuides.getState().clear()
+1
View File
@@ -569,6 +569,7 @@ export const FenceTool: React.FC = () => {
startingPoint.current.set(snappedStart[0], event.localPosition[1], snappedStart[1])
endingPoint.current.copy(startingPoint.current)
buildingState.current = 1
triggerSFX('sfx:structure-build-start')
previewRef.current.visible = true
setDraftMeasurement(null)
} else {
+1 -1
View File
@@ -181,7 +181,7 @@ const ShelfTool = () => {
})
useScene.getState().createNode(shelf, activeLevelId)
useViewer.getState().setSelection({ selectedIds: [shelf.id] })
triggerSFX('sfx:structure-build')
triggerSFX('sfx:item-place')
// The placed shelf is now a valid alignment target for the next one;
// refresh the candidate pool and drop the guide from this drop.
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, previewNode.id)
+3
View File
@@ -178,6 +178,9 @@ export const SlabTool: React.FC = () => {
setPoints([])
useAlignmentGuides.getState().clear()
} else {
// Every non-closing vertex is a "start" tick; the closing click above
// fires the structure-build (end) cue.
triggerSFX('sfx:structure-build-start')
setPoints([...points, clickPoint])
}
}
+1
View File
@@ -538,6 +538,7 @@ export const WallTool: React.FC = () => {
startingPoint.current.set(snappedStart[0], event.localPosition[1], snappedStart[1])
endingPoint.current.copy(startingPoint.current)
buildingState.current = 1
triggerSFX('sfx:structure-build-start')
// Visibility is owned by `updateWallPreview` — it flips
// `mesh.visible` based on segment length. Setting it here
// (before any geometry data has been written) draws the
+1 -1
View File
@@ -381,7 +381,7 @@ const MoveWindowTool: React.FC<{ node: WindowNode }> = ({ node: movingWindowNode
useLiveTransforms.getState().clear(movingWindowNode.id)
useScene.temporal.getState().pause()
triggerSFX('sfx:item-place')
triggerSFX('sfx:structure-build')
hideCursor()
useViewer.getState().setSelection({ selectedIds: [placedId] })
exitMoveMode()
+1 -1
View File
@@ -331,7 +331,7 @@ const WindowTool: React.FC = () => {
useScene.getState().createNode(node, event.node.id as AnyNodeId)
useViewer.getState().setSelection({ selectedIds: [node.id] })
useScene.temporal.getState().pause()
triggerSFX('sfx:item-place')
triggerSFX('sfx:structure-build')
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '')
useAlignmentGuides.getState().clear()