diff --git a/apps/editor/public/audios/sfx/resize_0.mp3 b/apps/editor/public/audios/sfx/resize_0.mp3 new file mode 100644 index 000000000..7743b81ce Binary files /dev/null and b/apps/editor/public/audios/sfx/resize_0.mp3 differ diff --git a/apps/editor/public/audios/sfx/resize_1.mp3 b/apps/editor/public/audios/sfx/resize_1.mp3 new file mode 100644 index 000000000..5aae5ce5d Binary files /dev/null and b/apps/editor/public/audios/sfx/resize_1.mp3 differ diff --git a/apps/editor/public/audios/sfx/resize_2.mp3 b/apps/editor/public/audios/sfx/resize_2.mp3 new file mode 100644 index 000000000..aa86b4cbb Binary files /dev/null and b/apps/editor/public/audios/sfx/resize_2.mp3 differ diff --git a/packages/editor/src/components/editor/node-arrow-handles.tsx b/packages/editor/src/components/editor/node-arrow-handles.tsx index 515dc0480..622034cdc 100644 --- a/packages/editor/src/components/editor/node-arrow-handles.tsx +++ b/packages/editor/src/components/editor/node-arrow-handles.tsx @@ -668,6 +668,11 @@ function LinearArrow({ ? 1 : -1 + // Last value an emitted resize tick fired at — a new tick fires only + // when the (snapped + clamped) value actually changes, so the cue + // tracks real size steps instead of every sub-pixel pointer jitter. + let lastTickValue = initialValue + return { overrideId, onBegin: () => { @@ -695,6 +700,10 @@ function LinearArrow({ ? snapScalar(rawNext, gridSnapStep) : rawNext const next = Math.min(maxBound, Math.max(minBound, snappedNext)) + if (next !== lastTickValue) { + lastTickValue = next + sfxEmitter.emit('sfx:resize') + } const patch = descriptor.apply(initialNode as never, next, sceneApi) as Partial // Let the kind publish live guides for the edge being resized. onDrag?.({ ...(initialNode as object), ...patch } as AnyNode, sceneApi) diff --git a/packages/editor/src/lib/sfx-bus.ts b/packages/editor/src/lib/sfx-bus.ts index 262525d56..a4551410d 100644 --- a/packages/editor/src/lib/sfx-bus.ts +++ b/packages/editor/src/lib/sfx-bus.ts @@ -10,6 +10,7 @@ type SFXEvents = { 'sfx:item-pick': undefined 'sfx:item-place': undefined 'sfx:item-rotate': undefined + 'sfx:resize': undefined 'sfx:structure-build-start': undefined 'sfx:structure-build': undefined 'sfx:structure-delete': undefined @@ -40,6 +41,7 @@ export function initSFXBus() { sfxEmitter.on('sfx:item-pick', () => playSFX('itemPick')) sfxEmitter.on('sfx:item-place', () => playSFX('itemPlace')) sfxEmitter.on('sfx:item-rotate', () => playSFX('itemRotate')) + sfxEmitter.on('sfx:resize', () => playSFX('resize')) sfxEmitter.on('sfx:structure-build-start', () => playSFX('structureBuildStart')) sfxEmitter.on('sfx:structure-build', () => playSFX('structureBuildEnd')) sfxEmitter.on('sfx:structure-delete', () => playSFX('structureDelete')) diff --git a/packages/editor/src/lib/sfx-player.ts b/packages/editor/src/lib/sfx-player.ts index f4d12e65f..e50538d01 100644 --- a/packages/editor/src/lib/sfx-player.ts +++ b/packages/editor/src/lib/sfx-player.ts @@ -59,6 +59,16 @@ export const SFX: Record = { volumeRange: [0.92, 1.0], panJitter: 0.15, }, + // Ticks as a resize handle is dragged across snap steps. Fires in rapid + // succession, so it mirrors gridSnap: three variations cycled round-robin + // with pitch/pan jitter and a gap so the run reads as texture, not a tone. + resize: { + src: ['/audios/sfx/resize_0.mp3', '/audios/sfx/resize_1.mp3', '/audios/sfx/resize_2.mp3'], + rateRange: [0.98, 1.02], + volumeRange: [0.26, 0.34], + panJitter: 0.15, + minIntervalMs: 80, + }, // Fired when a structure draft begins (first click of a wall/slab/etc). structureBuildStart: { src: '/audios/sfx/structure_build_start.mp3',