@pascal-app/editor 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +11 -11
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1981
- package/src/components/editor/floating-action-menu.tsx +1 -1
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +6 -2
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +26 -22
- package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
- package/src/components/editor/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +24 -14
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/room-controls.tsx +7 -4
- package/src/components/editor/room-handles-2d.tsx +2 -3
- package/src/components/editor/room-handles.tsx +1 -2
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +40 -1671
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +1 -1
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
- package/src/components/systems/roof/roof-edit-system.tsx +4 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +1 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
- package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +1 -1
- package/src/components/ui/panels/panel-wrapper.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +6 -6
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-panel.tsx +6 -8
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +180 -182
- package/src/i18n/locales/es/messages.po +180 -182
- package/src/i18n/locales/fr/messages.po +180 -182
- package/src/i18n/locales/ja/messages.po +180 -182
- package/src/i18n/locales/zh-CN/messages.po +180 -182
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +109 -131
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/floor-footprints.test.ts +2 -2
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurements.test.ts +2 -1
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-handle-drag.ts +2 -12
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/store/use-editor.tsx +17 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- package/src/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurements.ts +0 -8
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -327
- package/src/lib/scene-bounds.ts +0 -233
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
-
import
|
|
2
|
+
import { emitter } from '@pascal-app/core'
|
|
3
|
+
import { configureViewer } from '@pascal-app/viewer'
|
|
3
4
|
|
|
4
5
|
type FakeContext = { id: string }
|
|
5
6
|
|
|
@@ -93,12 +94,18 @@ const fakeHowler = {
|
|
|
93
94
|
|
|
94
95
|
mock.module('howler', () => ({ Howl: FakeHowl, Howler: fakeHowler }))
|
|
95
96
|
|
|
97
|
+
const sharedSounds = await import('@pascal-app/viewer/sfx')
|
|
98
|
+
const howlsOnImport = instances.length
|
|
99
|
+
|
|
100
|
+
const { useAudioSettings: useAudio } = sharedSounds
|
|
101
|
+
|
|
96
102
|
const { disposeSFX, playSFX, preloadSFX, startLoopSFX, stopLoopSFX, updateSFXVolumes } =
|
|
97
103
|
await import('./sfx-player')
|
|
98
104
|
const { disposeSFXBus, initSFXBus, sfxEmitter, triggerSFX } = await import('./sfx-bus')
|
|
99
105
|
|
|
100
106
|
beforeEach(() => {
|
|
101
107
|
disposeSFXBus()
|
|
108
|
+
configureViewer({ sfxBaseUrl: '/audios/sfx' })
|
|
102
109
|
activeContext = { id: 'first' }
|
|
103
110
|
initialState = 'loaded'
|
|
104
111
|
useAudio.setState({ masterVolume: 100, muted: false, sfxVolume: 100 })
|
|
@@ -108,6 +115,7 @@ beforeEach(() => {
|
|
|
108
115
|
|
|
109
116
|
afterAll(() => {
|
|
110
117
|
disposeSFXBus()
|
|
118
|
+
configureViewer({})
|
|
111
119
|
mock.restore()
|
|
112
120
|
})
|
|
113
121
|
|
|
@@ -325,3 +333,66 @@ describe('SFX audio context lifecycle', () => {
|
|
|
325
333
|
}
|
|
326
334
|
})
|
|
327
335
|
})
|
|
336
|
+
|
|
337
|
+
describe('public viewer SFX entry', () => {
|
|
338
|
+
test('importing the public entry creates no Howl instances', () => {
|
|
339
|
+
expect(howlsOnImport).toBe(0)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test('resolves the default and configured SFX origins at playback time', () => {
|
|
343
|
+
configureViewer({})
|
|
344
|
+
sharedSounds.playSound('menuClick')
|
|
345
|
+
expect(
|
|
346
|
+
instances.some((s) => s.src === 'https://editor.pascal.app/audios/sfx/menu_click.mp3'),
|
|
347
|
+
).toBe(true)
|
|
348
|
+
const previous = [...instances]
|
|
349
|
+
configureViewer({ sfxBaseUrl: 'https://sounds.example/custom/' })
|
|
350
|
+
sharedSounds.playSound('switchOn')
|
|
351
|
+
expect(previous.every((s) => s.unloadCount === 1)).toBe(true)
|
|
352
|
+
expect(instances.find((s) => s.playCount === 1 && s.src?.endsWith('switch_on.mp3'))?.src).toBe(
|
|
353
|
+
'https://sounds.example/custom/switch_on.mp3',
|
|
354
|
+
)
|
|
355
|
+
configureViewer({ sfxBaseUrl: '/audios/sfx' })
|
|
356
|
+
sharedSounds.playSound('switchOff')
|
|
357
|
+
expect(
|
|
358
|
+
instances.find((s) => s.playCount === 1 && s.src === '/audios/sfx/switch_off.mp3'),
|
|
359
|
+
).toBeDefined()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('interaction subscriptions share delivery and detach independently and idempotently', () => {
|
|
363
|
+
const first = sharedSounds.initInteractionSounds()
|
|
364
|
+
const second = sharedSounds.initInteractionSounds()
|
|
365
|
+
emitter.emit('interaction:sound', 'switch-on')
|
|
366
|
+
expect(instances.filter((s) => s.playCount === 1)).toHaveLength(1)
|
|
367
|
+
first()
|
|
368
|
+
first()
|
|
369
|
+
emitter.emit('interaction:sound', 'switch-off')
|
|
370
|
+
expect(instances.filter((s) => s.playCount === 1)).toHaveLength(2)
|
|
371
|
+
second()
|
|
372
|
+
emitter.emit('interaction:sound', 'garage')
|
|
373
|
+
expect(instances.filter((s) => s.playCount === 1)).toHaveLength(2)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test('interaction playback uses window and panel gains and the shared mute', () => {
|
|
377
|
+
const stop = sharedSounds.initInteractionSounds()
|
|
378
|
+
emitter.emit('interaction:sound', 'window-swing')
|
|
379
|
+
const swing = instances.find((s) => s.src === '/audios/sfx/door_swing.mp3')
|
|
380
|
+
expect(swing?.volumeCalls.find(([, id]) => id === 1)?.[0]).toBeCloseTo(0.6)
|
|
381
|
+
emitter.emit('interaction:sound', 'panel-open')
|
|
382
|
+
const panel = instances.find((s) => s.src === '/audios/sfx/shutters_open.mp3')
|
|
383
|
+
expect(panel?.volumeCalls.find(([, id]) => id === 1)?.[0]).toBeCloseTo(0.75)
|
|
384
|
+
useAudio.getState().setMuted(true)
|
|
385
|
+
emitter.emit('interaction:sound', 'switch-on')
|
|
386
|
+
expect(instances.find((s) => s.src === '/audios/sfx/switch_on.mp3')?.playCount).toBe(0)
|
|
387
|
+
stop()
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test('viewer and editor UI cues reuse the same Howl cache and settings store', () => {
|
|
391
|
+
preloadSFX()
|
|
392
|
+
const count = instances.length
|
|
393
|
+
sharedSounds.playSound('menuHover')
|
|
394
|
+
playSFX('menuHover')
|
|
395
|
+
expect(instances).toHaveLength(count)
|
|
396
|
+
expect(instances.find((s) => s.src === '/audios/sfx/menu_hover.mp3')?.playCount).toBe(2)
|
|
397
|
+
})
|
|
398
|
+
})
|
package/src/lib/sfx-player.ts
CHANGED
|
@@ -1,414 +1,126 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// are silently dropped so bursty sequences don't phase-stack into noise.
|
|
16
|
-
minIntervalMs?: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type LoopSFXConfig = {
|
|
20
|
-
src: string
|
|
21
|
-
volumeMultiplier: number
|
|
22
|
-
fadeInMs?: number
|
|
23
|
-
fadeOutMs?: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const DEFAULT_MIN_INTERVAL_MS = 30
|
|
27
|
-
const SFX_FAILURE_BACKOFF_MS = 5_000
|
|
28
|
-
const DEFAULT_LOOP_FADE_MS = 90
|
|
29
|
-
|
|
30
|
-
// SFX sound definitions
|
|
31
|
-
export const SFX: Record<string, SFXConfig> = {
|
|
1
|
+
import {
|
|
2
|
+
disposeSounds,
|
|
3
|
+
playSound,
|
|
4
|
+
preloadSounds,
|
|
5
|
+
SOUNDS,
|
|
6
|
+
type SoundDefinition,
|
|
7
|
+
type SoundPlaybackOptions,
|
|
8
|
+
startSoundLoop,
|
|
9
|
+
stopSoundLoop,
|
|
10
|
+
updateSoundVolumes,
|
|
11
|
+
} from '@pascal-app/viewer/sfx'
|
|
12
|
+
|
|
13
|
+
export const SFX = {
|
|
14
|
+
...SOUNDS,
|
|
32
15
|
gridSnap: {
|
|
33
|
-
src: [
|
|
34
|
-
'/audios/sfx/grid_snap_0.mp3',
|
|
35
|
-
'/audios/sfx/grid_snap_1.mp3',
|
|
36
|
-
'/audios/sfx/grid_snap_2.mp3',
|
|
37
|
-
],
|
|
16
|
+
src: ['grid_snap_0.mp3', 'grid_snap_1.mp3', 'grid_snap_2.mp3'],
|
|
38
17
|
rateRange: [0.98, 1.02],
|
|
39
18
|
volumeRange: [0.5, 0.6],
|
|
40
19
|
minIntervalMs: 50,
|
|
41
20
|
},
|
|
42
21
|
itemDelete: {
|
|
43
|
-
src: '
|
|
22
|
+
src: 'item_delete.mp3',
|
|
44
23
|
rateRange: [0.9, 1.1],
|
|
45
24
|
volumeRange: [0.9, 1.0],
|
|
46
25
|
},
|
|
47
26
|
itemPick: {
|
|
48
|
-
src: '
|
|
27
|
+
src: 'item_pick.mp3',
|
|
49
28
|
rateRange: [0.95, 1.05],
|
|
50
29
|
volumeRange: [0.92, 1.0],
|
|
51
30
|
},
|
|
52
31
|
itemPlace: {
|
|
53
|
-
src: '
|
|
32
|
+
src: 'item_place.mp3',
|
|
54
33
|
rateRange: [0.98, 1.02],
|
|
55
34
|
volumeRange: [0.9, 1.0],
|
|
56
35
|
},
|
|
57
36
|
itemRotate: {
|
|
58
|
-
src: '
|
|
37
|
+
src: 'item_rotate.mp3',
|
|
59
38
|
rateRange: [0.94, 1.06],
|
|
60
39
|
volumeRange: [0.92, 1.0],
|
|
61
40
|
},
|
|
62
|
-
// Ticks as a resize handle is dragged across snap steps. Fires in rapid
|
|
63
|
-
// succession, so it mirrors gridSnap: three variations cycled round-robin
|
|
64
|
-
// with pitch jitter and a gap so the run reads as texture, not a tone.
|
|
65
41
|
resize: {
|
|
66
|
-
src: ['
|
|
42
|
+
src: ['resize_0.mp3', 'resize_1.mp3', 'resize_2.mp3'],
|
|
67
43
|
rateRange: [0.98, 1.02],
|
|
68
44
|
volumeRange: [0.26, 0.34],
|
|
69
45
|
minIntervalMs: 80,
|
|
70
46
|
},
|
|
71
|
-
// Fired when a structure draft begins (first click of a wall/slab/etc).
|
|
72
47
|
structureBuildStart: {
|
|
73
|
-
src: '
|
|
48
|
+
src: 'structure_build_start.mp3',
|
|
74
49
|
rateRange: [0.95, 1.05],
|
|
75
50
|
volumeRange: [0.88, 1.0],
|
|
76
51
|
},
|
|
77
|
-
// Fired when a structure is committed (segment placed / polygon closed).
|
|
78
52
|
structureBuildEnd: {
|
|
79
|
-
src: '
|
|
53
|
+
src: 'structure_build_end.mp3',
|
|
80
54
|
rateRange: [0.95, 1.05],
|
|
81
55
|
volumeRange: [0.88, 1.0],
|
|
82
56
|
},
|
|
83
57
|
structureDelete: {
|
|
84
|
-
src: '
|
|
58
|
+
src: 'structure_delete.mp3',
|
|
85
59
|
rateRange: [0.9, 1.1],
|
|
86
60
|
volumeRange: [0.9, 1.0],
|
|
87
61
|
},
|
|
88
62
|
snapshotCapture: {
|
|
89
63
|
// Shutter should sound consistent — no variation.
|
|
90
|
-
src: '
|
|
91
|
-
},
|
|
92
|
-
// Soft tick when hovering a main category in the Build / Items panels.
|
|
93
|
-
// Kept quiet and rate-locked so sweeping across the grid reads as texture,
|
|
94
|
-
// not a melody.
|
|
95
|
-
menuHover: {
|
|
96
|
-
src: '/audios/sfx/menu_hover.mp3',
|
|
97
|
-
rateRange: [0.98, 1.02],
|
|
98
|
-
volumeRange: [0.2, 0.3],
|
|
99
|
-
minIntervalMs: 0,
|
|
100
|
-
},
|
|
101
|
-
// Fired when a main category in the Build / Items panels is clicked.
|
|
102
|
-
menuClick: {
|
|
103
|
-
src: '/audios/sfx/menu_click.mp3',
|
|
104
|
-
rateRange: [0.98, 1.02],
|
|
105
|
-
volumeRange: [0.5, 0.6],
|
|
64
|
+
src: 'snapshot_capture.mp3',
|
|
106
65
|
},
|
|
107
|
-
// Fired when a material is applied to a surface in paint mode. Painting can
|
|
108
|
-
// fire in quick succession across faces, so keep variation + a small gap.
|
|
109
66
|
paintApply: {
|
|
110
|
-
src: '
|
|
67
|
+
src: 'paint_apply.mp3',
|
|
111
68
|
rateRange: [0.95, 1.05],
|
|
112
69
|
volumeRange: [0.85, 1.0],
|
|
113
70
|
minIntervalMs: 60,
|
|
114
71
|
},
|
|
115
|
-
// A three-second jingle for finishing something, not a click cue. No pitch or
|
|
116
|
-
// volume jitter — a fanfare that lands a semitone off reads as broken rather
|
|
117
|
-
// than as varied — and a gap longer than the sound itself, so two milestones
|
|
118
|
-
// that land together play once instead of phasing over each other.
|
|
119
72
|
success: {
|
|
120
|
-
src: '
|
|
73
|
+
src: 'success.mp3',
|
|
121
74
|
minIntervalMs: 3_000,
|
|
122
75
|
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
turnFailed: { src: '/audios/sfx/turn_failed.mp3' },
|
|
128
|
-
turnNeedsInput: { src: '/audios/sfx/turn_needs_input.mp3' },
|
|
129
|
-
// Operating the building from a viewer's callout or a walkthrough: recorded
|
|
130
|
-
// door and shutter movements; the switches and the viewer's hover, select and
|
|
131
|
-
// deselect ticks are the marketing house's synth cues (`scripts/synth-viewer-cues.ts`),
|
|
132
|
-
// already mixed against the doors, so they play at full volume without jitter.
|
|
133
|
-
doorOpen: { src: '/audios/sfx/door_open.mp3', rateRange: [0.96, 1.04], volumeRange: [0.9, 1] },
|
|
134
|
-
doorClose: { src: '/audios/sfx/door_close.mp3', rateRange: [0.96, 1.04], volumeRange: [0.9, 1] },
|
|
135
|
-
// Air moving as a leaf swings shut, ahead of the latch (`scripts/synth-viewer-cues.ts`).
|
|
136
|
-
doorSwing: { src: '/audios/sfx/door_swing.mp3', rateRange: [0.97, 1.03] },
|
|
137
|
-
garageDoor: { src: '/audios/sfx/garage_door.mp3', volumeRange: [0.9, 1] },
|
|
138
|
-
shuttersOpen: { src: '/audios/sfx/shutters_open.mp3', rateRange: [0.96, 1.04] },
|
|
139
|
-
shuttersClose: { src: '/audios/sfx/shutters_close.mp3', rateRange: [0.96, 1.04] },
|
|
140
|
-
switchOn: { src: '/audios/sfx/switch_on.mp3' },
|
|
141
|
-
switchOff: { src: '/audios/sfx/switch_off.mp3' },
|
|
142
|
-
viewerHover: { src: '/audios/sfx/viewer_hover.mp3', minIntervalMs: 95 },
|
|
143
|
-
viewerSelect: { src: '/audios/sfx/viewer_select.mp3', minIntervalMs: 90 },
|
|
144
|
-
viewerDeselect: { src: '/audios/sfx/viewer_deselect.mp3' },
|
|
145
|
-
} as const
|
|
76
|
+
turnDone: { src: 'turn_done.mp3' },
|
|
77
|
+
turnFailed: { src: 'turn_failed.mp3' },
|
|
78
|
+
turnNeedsInput: { src: 'turn_needs_input.mp3' },
|
|
79
|
+
} satisfies Record<string, SoundDefinition>
|
|
146
80
|
|
|
147
81
|
export type SFXName = keyof typeof SFX
|
|
148
82
|
|
|
149
83
|
export const LOOP_SFX = {
|
|
150
84
|
terrainRaise: {
|
|
151
|
-
src: '
|
|
85
|
+
src: 'terrain_raise.mp3',
|
|
86
|
+
loop: true,
|
|
152
87
|
volumeMultiplier: 0.2,
|
|
153
88
|
},
|
|
154
89
|
terrainLower: {
|
|
155
|
-
src: '
|
|
90
|
+
src: 'terrain_lower.mp3',
|
|
91
|
+
loop: true,
|
|
156
92
|
volumeMultiplier: 0.2,
|
|
157
93
|
},
|
|
158
94
|
terrainFlatten: {
|
|
159
|
-
src: '
|
|
95
|
+
src: 'terrain_flatten.mp3',
|
|
96
|
+
loop: true,
|
|
160
97
|
volumeMultiplier: 0.2,
|
|
161
98
|
},
|
|
162
99
|
terrainSmooth: {
|
|
163
|
-
src: '
|
|
100
|
+
src: 'terrain_smooth.mp3',
|
|
101
|
+
loop: true,
|
|
164
102
|
volumeMultiplier: 0.2,
|
|
165
103
|
},
|
|
166
|
-
} as const satisfies Record<string,
|
|
104
|
+
} as const satisfies Record<string, SoundDefinition>
|
|
167
105
|
|
|
168
106
|
export type LoopSFXName = keyof typeof LOOP_SFX
|
|
169
|
-
type CachedSFXName = SFXName | LoopSFXName
|
|
170
|
-
|
|
171
|
-
function loopConfig(name: LoopSFXName): LoopSFXConfig {
|
|
172
|
-
return LOOP_SFX[name]
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export type SFXPlaybackOptions = {
|
|
176
|
-
source?: 'local' | 'remote'
|
|
177
|
-
stereo?: number
|
|
178
|
-
volumeMultiplier?: number
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function randomInRange([min, max]: [number, number]): number {
|
|
182
|
-
return min + Math.random() * (max - min)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
let sfxCache = new Map<CachedSFXName, Howl[]>()
|
|
186
|
-
let sfxAudioContext: AudioContext | null = null
|
|
187
|
-
let sfxRetryAfter = 0
|
|
188
|
-
const lastPlayedAt = new Map<SFXName, number>()
|
|
189
|
-
const lastVariation = new Map<SFXName, number>()
|
|
190
|
-
let activeLoop: {
|
|
191
|
-
id: number
|
|
192
|
-
name: LoopSFXName
|
|
193
|
-
sound: Howl
|
|
194
|
-
volume: number
|
|
195
|
-
} | null = null
|
|
196
|
-
let pendingLoop: { name: LoopSFXName; sound: Howl } | null = null
|
|
197
|
-
let requestedLoop: LoopSFXName | null = null
|
|
198
|
-
|
|
199
|
-
function loopVolume(name: LoopSFXName): number {
|
|
200
|
-
const { masterVolume, sfxVolume } = useAudio.getState()
|
|
201
|
-
return (masterVolume / 100) * (sfxVolume / 100) * LOOP_SFX[name].volumeMultiplier
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function stopActiveLoop(fadeMs: number) {
|
|
205
|
-
const active = activeLoop
|
|
206
|
-
if (!active) return
|
|
207
|
-
activeLoop = null
|
|
208
107
|
|
|
209
|
-
|
|
210
|
-
active.sound.stop(active.id)
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
active.sound.once('fade', () => active.sound.stop(active.id), active.id)
|
|
215
|
-
active.sound.fade(active.volume, 0, fadeMs, active.id)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function unloadCachedSounds(resetPlaybackState: boolean) {
|
|
219
|
-
requestedLoop = null
|
|
220
|
-
pendingLoop = null
|
|
221
|
-
stopActiveLoop(0)
|
|
222
|
-
for (const sounds of sfxCache.values()) {
|
|
223
|
-
for (const sound of sounds) {
|
|
224
|
-
try {
|
|
225
|
-
sound.unload()
|
|
226
|
-
} catch {}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
sfxCache.clear()
|
|
230
|
-
sfxAudioContext = null
|
|
231
|
-
if (resetPlaybackState) {
|
|
232
|
-
sfxRetryAfter = 0
|
|
233
|
-
lastPlayedAt.clear()
|
|
234
|
-
lastVariation.clear()
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function cacheNeedsRebuild(): boolean {
|
|
239
|
-
if (sfxCache.size === 0) return true
|
|
240
|
-
if (sfxAudioContext !== Howler.ctx) return true
|
|
241
|
-
for (const sounds of sfxCache.values()) {
|
|
242
|
-
if (sounds.some((sound) => sound.state() === 'unloaded')) return true
|
|
243
|
-
}
|
|
244
|
-
return false
|
|
245
|
-
}
|
|
108
|
+
export type SFXPlaybackOptions = SoundPlaybackOptions
|
|
246
109
|
|
|
247
110
|
export function preloadSFX() {
|
|
248
|
-
|
|
249
|
-
unloadCachedSounds(false)
|
|
250
|
-
|
|
251
|
-
const definitions = { ...SFX, ...LOOP_SFX }
|
|
252
|
-
for (const [name, config] of Object.entries(definitions)) {
|
|
253
|
-
const sources = Array.isArray(config.src) ? config.src : [config.src]
|
|
254
|
-
sfxCache.set(
|
|
255
|
-
name as CachedSFXName,
|
|
256
|
-
sources.map(
|
|
257
|
-
(src) =>
|
|
258
|
-
new Howl({
|
|
259
|
-
loop: name in LOOP_SFX,
|
|
260
|
-
src: [src],
|
|
261
|
-
preload: true,
|
|
262
|
-
volume: 0.5,
|
|
263
|
-
}),
|
|
264
|
-
),
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
sfxAudioContext = Howler.ctx ?? null
|
|
111
|
+
preloadSounds([...Object.values(SFX), ...Object.values(LOOP_SFX)])
|
|
268
112
|
}
|
|
269
113
|
|
|
270
|
-
export function
|
|
271
|
-
|
|
114
|
+
export function playSFX(name: SFXName, options?: SFXPlaybackOptions) {
|
|
115
|
+
preloadSFX()
|
|
116
|
+
playSound(SFX[name], options)
|
|
272
117
|
}
|
|
273
118
|
|
|
274
119
|
export function startLoopSFX(name: LoopSFXName) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
stopLoopSFX()
|
|
278
|
-
return
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
requestedLoop = name
|
|
282
|
-
const current = activeLoop
|
|
283
|
-
if (current?.name === name && current.sound.playing(current.id)) return
|
|
284
|
-
if (current) stopActiveLoop(loopConfig(current.name).fadeOutMs ?? DEFAULT_LOOP_FADE_MS)
|
|
285
|
-
|
|
286
|
-
const now = performance.now()
|
|
287
|
-
if (now < sfxRetryAfter) return
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
preloadSFX()
|
|
291
|
-
// A fresh Howler context rebuilds the cache and clears stale playback
|
|
292
|
-
// state. Restore this current press after that cleanup so a first-use loop
|
|
293
|
-
// can still begin when its asset finishes loading.
|
|
294
|
-
requestedLoop = name
|
|
295
|
-
const sound = sfxCache.get(name)?.[0]
|
|
296
|
-
if (!sound) return
|
|
297
|
-
if (sound.state() !== 'loaded') {
|
|
298
|
-
if (pendingLoop?.name === name && pendingLoop.sound === sound) return
|
|
299
|
-
pendingLoop = { name, sound }
|
|
300
|
-
sound.once('load', () => {
|
|
301
|
-
if (pendingLoop?.sound === sound) pendingLoop = null
|
|
302
|
-
if (requestedLoop === name) startLoopSFX(name)
|
|
303
|
-
})
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const config = loopConfig(name)
|
|
308
|
-
const volume = loopVolume(name)
|
|
309
|
-
const id = sound.play()
|
|
310
|
-
sound.volume(0, id)
|
|
311
|
-
activeLoop = { id, name, sound, volume }
|
|
312
|
-
sound.fade(0, volume, config.fadeInMs ?? DEFAULT_LOOP_FADE_MS, id)
|
|
313
|
-
} catch {
|
|
314
|
-
unloadCachedSounds(false)
|
|
315
|
-
sfxRetryAfter = now + SFX_FAILURE_BACKOFF_MS
|
|
316
|
-
}
|
|
120
|
+
preloadSFX()
|
|
121
|
+
startSoundLoop(LOOP_SFX[name])
|
|
317
122
|
}
|
|
318
123
|
|
|
319
|
-
export
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const fadeMs = activeLoop ? (loopConfig(activeLoop.name).fadeOutMs ?? DEFAULT_LOOP_FADE_MS) : 0
|
|
323
|
-
stopActiveLoop(fadeMs)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Play a sound effect with volume based on audio settings
|
|
328
|
-
*/
|
|
329
|
-
export function playSFX(name: SFXName, options: SFXPlaybackOptions = {}) {
|
|
330
|
-
const config = SFX[name]!
|
|
331
|
-
const { masterVolume, sfxVolume, muted } = useAudio.getState()
|
|
332
|
-
|
|
333
|
-
if (muted) return
|
|
334
|
-
|
|
335
|
-
// Drop rapid repeats — two plays of the same SFX within minIntervalMs just
|
|
336
|
-
// smear into noise, they don't add useful information.
|
|
337
|
-
const now = performance.now()
|
|
338
|
-
if (now < sfxRetryAfter) return
|
|
339
|
-
const minInterval = config.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS
|
|
340
|
-
const source = options.source ?? 'local'
|
|
341
|
-
const playbackKey = `${source}:${name}`
|
|
342
|
-
const last = lastPlayedAt.get(playbackKey)
|
|
343
|
-
if (last !== undefined && now - last < minInterval) return
|
|
344
|
-
// Local feedback stays legible when a collaborator makes the same kind of
|
|
345
|
-
// change at nearly the same time; the quieter remote cue yields instead.
|
|
346
|
-
const lastLocal = lastPlayedAt.get(`local:${name}`)
|
|
347
|
-
if (source === 'remote' && lastLocal !== undefined && now - lastLocal < 120) return
|
|
348
|
-
lastPlayedAt.set(playbackKey, now)
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
preloadSFX()
|
|
352
|
-
const sounds = sfxCache.get(name)
|
|
353
|
-
if (!sounds || sounds.length === 0) return
|
|
354
|
-
|
|
355
|
-
// Pick a random variation, avoiding an immediate repeat of the last one so
|
|
356
|
-
// consecutive plays don't land on the same file.
|
|
357
|
-
let index = Math.floor(Math.random() * sounds.length)
|
|
358
|
-
if (sounds.length > 1 && index === lastVariation.get(name)) {
|
|
359
|
-
index = (index + 1) % sounds.length
|
|
360
|
-
}
|
|
361
|
-
lastVariation.set(name, index)
|
|
362
|
-
const sound = sounds[index]!
|
|
363
|
-
// Howler queues per-play mutations while a sound is loading. If its global
|
|
364
|
-
// AudioContext is replaced before that queue drains, stereo setup can try
|
|
365
|
-
// to connect nodes from different contexts and throw asynchronously.
|
|
366
|
-
if (sound.state() !== 'loaded') return
|
|
367
|
-
const baseVolume = (masterVolume / 100) * (sfxVolume / 100)
|
|
368
|
-
const volumeJitter = config.volumeRange ? randomInRange(config.volumeRange) : 1
|
|
369
|
-
const volumeMultiplier = Number.isFinite(options.volumeMultiplier)
|
|
370
|
-
? Math.max(0, Math.min(2, options.volumeMultiplier!))
|
|
371
|
-
: 1
|
|
372
|
-
const rate = config.rateRange ? randomInRange(config.rateRange) : 1
|
|
373
|
-
const id = sound.play()
|
|
374
|
-
sound.volume(baseVolume * volumeJitter * volumeMultiplier, id)
|
|
375
|
-
if (Number.isFinite(options.stereo)) {
|
|
376
|
-
sound.stereo(Math.max(-1, Math.min(1, options.stereo!)), id)
|
|
377
|
-
}
|
|
378
|
-
if (rate !== 1) sound.rate(rate, id)
|
|
379
|
-
} catch {
|
|
380
|
-
// Optional audio must never abort an editor input callback. Rebuild from
|
|
381
|
-
// the current Howler context after a backoff instead of retrying every pointer cue.
|
|
382
|
-
unloadCachedSounds(false)
|
|
383
|
-
sfxRetryAfter = now + SFX_FAILURE_BACKOFF_MS
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Update all cached SFX volumes (useful when settings change)
|
|
389
|
-
*/
|
|
390
|
-
export function updateSFXVolumes() {
|
|
391
|
-
const { masterVolume, muted, sfxVolume } = useAudio.getState()
|
|
392
|
-
if (muted) {
|
|
393
|
-
stopLoopSFX()
|
|
394
|
-
return
|
|
395
|
-
}
|
|
396
|
-
const finalVolume = (masterVolume / 100) * (sfxVolume / 100)
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
if (performance.now() < sfxRetryAfter) return
|
|
400
|
-
preloadSFX()
|
|
401
|
-
sfxCache.forEach((sounds) => {
|
|
402
|
-
sounds.forEach((sound) => {
|
|
403
|
-
sound.volume(finalVolume)
|
|
404
|
-
})
|
|
405
|
-
})
|
|
406
|
-
if (activeLoop) {
|
|
407
|
-
activeLoop.volume = loopVolume(activeLoop.name)
|
|
408
|
-
activeLoop.sound.volume(activeLoop.volume, activeLoop.id)
|
|
409
|
-
}
|
|
410
|
-
} catch {
|
|
411
|
-
unloadCachedSounds(false)
|
|
412
|
-
sfxRetryAfter = performance.now() + SFX_FAILURE_BACKOFF_MS
|
|
413
|
-
}
|
|
414
|
-
}
|
|
124
|
+
export const stopLoopSFX = stopSoundLoop
|
|
125
|
+
export const updateSFXVolumes = updateSoundVolumes
|
|
126
|
+
export const disposeSFX = disposeSounds
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
ItemNode,
|
|
10
10
|
LevelNode,
|
|
11
11
|
type NodeEvent,
|
|
12
|
+
RoofNode,
|
|
13
|
+
RoofSegmentNode,
|
|
12
14
|
SlabNode,
|
|
13
15
|
sceneRegistry,
|
|
14
16
|
UnitNode,
|
|
@@ -18,25 +20,25 @@ import {
|
|
|
18
20
|
} from '@pascal-app/core'
|
|
19
21
|
import { migrateRoomZones } from '@pascal-app/core/scene-migrations'
|
|
20
22
|
import { resolveGlbPick, useViewer } from '@pascal-app/viewer'
|
|
21
|
-
import { CameraControlsImpl } from '@react-three/drei'
|
|
22
|
-
import { createElement } from 'react'
|
|
23
|
-
import * as THREE from 'three'
|
|
24
|
-
import { Group, Vector3 } from 'three'
|
|
25
|
-
import { viewerHoverLabel } from '../components/viewer/viewer-hover-label'
|
|
26
|
-
import { ViewerSelectionManager } from '../components/viewer/viewer-selection-manager'
|
|
27
|
-
import { publishCameraPose } from '../store/camera-pose-store'
|
|
28
|
-
import useEditor, { takePreviewCameraRestore } from '../store/use-editor'
|
|
29
|
-
import { withSelectionHarness } from '../test-utils/selection-harness'
|
|
30
23
|
import {
|
|
31
24
|
frameViewerCamera,
|
|
25
|
+
publishCameraPose,
|
|
32
26
|
resolveViewerClick,
|
|
33
27
|
resolveViewerHover,
|
|
34
28
|
selectViewerRoom,
|
|
35
29
|
type ViewerSelection,
|
|
30
|
+
ViewerSelectionManager,
|
|
36
31
|
viewerBreadcrumb,
|
|
32
|
+
viewerHoverLabel,
|
|
37
33
|
viewerPickFromNodeEvent,
|
|
38
34
|
viewerSidebarSections,
|
|
39
|
-
} from '
|
|
35
|
+
} from '@pascal-app/viewer/ui'
|
|
36
|
+
import { CameraControlsImpl } from '@react-three/drei'
|
|
37
|
+
import { createElement } from 'react'
|
|
38
|
+
import * as THREE from 'three'
|
|
39
|
+
import { Group, Vector3 } from 'three'
|
|
40
|
+
import useEditor, { takePreviewCameraRestore } from '../store/use-editor'
|
|
41
|
+
import { withSelectionHarness } from '../test-utils/selection-harness'
|
|
40
42
|
|
|
41
43
|
const english = {
|
|
42
44
|
_: (descriptor: { id: string; message?: string }) => descriptor.message ?? descriptor.id,
|
|
@@ -164,6 +166,32 @@ describe('viewer selection rules', () => {
|
|
|
164
166
|
}
|
|
165
167
|
})
|
|
166
168
|
|
|
169
|
+
test("a roof's segment or its face of another kind selects the whole roof", () => {
|
|
170
|
+
const segment = RoofSegmentNode.parse({ id: 'rseg_main', parentId: 'roof_main' })
|
|
171
|
+
// A plugin's roof plane (`capabilities.roofs`): the click picks its roof as a segment's does.
|
|
172
|
+
const plane = {
|
|
173
|
+
object: 'node',
|
|
174
|
+
id: 'roofplane_south',
|
|
175
|
+
type: 'test:roof-plane',
|
|
176
|
+
parentId: 'roof_main',
|
|
177
|
+
visible: true,
|
|
178
|
+
metadata: {},
|
|
179
|
+
} as unknown as AnyNode
|
|
180
|
+
const roof = RoofNode.parse({
|
|
181
|
+
id: 'roof_main',
|
|
182
|
+
parentId: levelId,
|
|
183
|
+
children: [segment.id, plane.id],
|
|
184
|
+
})
|
|
185
|
+
const { nodes } = scene([roof])
|
|
186
|
+
nodes[segment.id] = segment
|
|
187
|
+
nodes[plane.id] = plane
|
|
188
|
+
for (const nodeId of [segment.id, plane.id])
|
|
189
|
+
expect(resolveViewerClick(nothing, { nodeId, point: [2, 2] }, nodes)).toMatchObject({
|
|
190
|
+
levelId,
|
|
191
|
+
selectedIds: [roof.id],
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
167
195
|
test('empty space clears the room and the element and keeps the floor', () => {
|
|
168
196
|
const { nodes, kitchen } = scene()
|
|
169
197
|
const drilled: ViewerSelection = {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
WallNode as WallSchema,
|
|
13
13
|
wallAssemblyPatch,
|
|
14
14
|
wallClosesRoom,
|
|
15
|
+
wallPullSeed,
|
|
15
16
|
} from '@pascal-app/core'
|
|
16
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
17
18
|
import { createWallOnCurrentLevel } from '../components/tools/wall/wall-drafting'
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
startWallPull,
|
|
24
25
|
useWallPullRun,
|
|
25
26
|
wallDraftIsRun,
|
|
26
|
-
wallPullSeed,
|
|
27
27
|
wallPullTypePatch,
|
|
28
28
|
} from './wall-pull'
|
|
29
29
|
import { isWallRunFree, snapWallRunDraftPoint, wallRunDirection } from './wall-run-snap'
|