@pascal-app/viewer 1.1.0 → 1.1.2
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/README.md +157 -0
- package/dist/components/viewer/drone-controller.d.ts.map +1 -1
- package/dist/components/viewer/drone-controller.js +1 -5
- package/dist/components/viewer/first-person/build-collider-world.d.ts +14 -0
- package/dist/components/viewer/first-person/build-collider-world.d.ts.map +1 -0
- package/dist/components/viewer/first-person/build-collider-world.js +426 -0
- package/dist/components/viewer/first-person/ground-holes.d.ts +22 -0
- package/dist/components/viewer/first-person/ground-holes.d.ts.map +1 -0
- package/dist/components/viewer/first-person/ground-holes.js +116 -0
- package/dist/components/viewer/first-person/mechanism-targets.d.ts +27 -0
- package/dist/components/viewer/first-person/mechanism-targets.d.ts.map +1 -0
- package/dist/components/viewer/first-person/mechanism-targets.js +79 -0
- package/dist/components/viewer/first-person/terrain-collider.d.ts +32 -0
- package/dist/components/viewer/first-person/terrain-collider.d.ts.map +1 -0
- package/dist/components/viewer/first-person/terrain-collider.js +175 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
- package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
- package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +22 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/lib/daylight/room-map.d.ts +29 -4
- package/dist/lib/daylight/room-map.d.ts.map +1 -1
- package/dist/lib/daylight/room-map.js +219 -36
- package/dist/lib/daylight/room-scene.d.ts.map +1 -1
- package/dist/lib/daylight/room-scene.js +9 -3
- package/dist/lib/daylight/rooms.d.ts +15 -6
- package/dist/lib/daylight/rooms.d.ts.map +1 -1
- package/dist/lib/daylight/rooms.js +49 -9
- package/dist/lib/measurement-presentation.d.ts +2 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -1
- package/dist/lib/measurement-presentation.js +6 -0
- package/dist/lib/typing-target.d.ts +2 -0
- package/dist/lib/typing-target.d.ts.map +1 -0
- package/dist/lib/typing-target.js +7 -0
- package/dist/lib/viewer-config.d.ts +6 -0
- package/dist/lib/viewer-config.d.ts.map +1 -1
- package/dist/lib/viewer-config.js +2 -0
- package/dist/sfx/audio-settings.d.ts +81 -0
- package/dist/sfx/audio-settings.d.ts.map +1 -0
- package/dist/sfx/audio-settings.js +166 -0
- package/dist/sfx/index.d.ts +6 -0
- package/dist/sfx/index.d.ts.map +1 -0
- package/dist/sfx/index.js +5 -0
- package/dist/sfx/interaction-sounds.d.ts +7 -0
- package/dist/sfx/interaction-sounds.d.ts.map +1 -0
- package/dist/sfx/interaction-sounds.js +39 -0
- package/dist/sfx/player.d.ts +31 -0
- package/dist/sfx/player.d.ts.map +1 -0
- package/dist/sfx/player.js +255 -0
- package/dist/sfx/sounds.d.ts +60 -0
- package/dist/sfx/sounds.d.ts.map +1 -0
- package/dist/sfx/sounds.js +25 -0
- package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
- package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
- package/dist/sfx/use-viewer-ui-sounds.js +42 -0
- package/dist/store/use-first-person-hud.d.ts +13 -0
- package/dist/store/use-first-person-hud.d.ts.map +1 -0
- package/dist/store/use-first-person-hud.js +21 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +27 -13
- package/dist/ui/components/action-button.d.ts +14 -0
- package/dist/ui/components/action-button.d.ts.map +1 -0
- package/dist/ui/components/action-button.js +21 -0
- package/dist/ui/components/action-menu-placement.d.ts +7 -0
- package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
- package/dist/ui/components/action-menu-placement.js +10 -0
- package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
- package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
- package/dist/ui/components/floorplan-compass-button.js +22 -0
- package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
- package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
- package/dist/ui/components/follow-pascal-toggle.js +21 -0
- package/dist/ui/components/localized-html.d.ts +9 -0
- package/dist/ui/components/localized-html.d.ts.map +1 -0
- package/dist/ui/components/localized-html.js +16 -0
- package/dist/ui/components/render-settings-menu.d.ts +9 -0
- package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
- package/dist/ui/components/render-settings-menu.js +145 -0
- package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
- package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
- package/dist/ui/components/rooms/room-highlight.js +56 -0
- package/dist/ui/components/rooms/room-labels.d.ts +278 -0
- package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
- package/dist/ui/components/rooms/room-labels.js +1295 -0
- package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
- package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
- package/dist/ui/components/rooms/viewer-rooms.js +123 -0
- package/dist/ui/components/scene-loader.d.ts +18 -0
- package/dist/ui/components/scene-loader.d.ts.map +1 -0
- package/dist/ui/components/scene-loader.js +29 -0
- package/dist/ui/components/sky-control.d.ts +37 -0
- package/dist/ui/components/sky-control.d.ts.map +1 -0
- package/dist/ui/components/sky-control.js +382 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
- package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
- package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
- package/dist/ui/components/viewer-controls-bar.js +130 -0
- package/dist/ui/components/viewer-hover-label.d.ts +14 -0
- package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
- package/dist/ui/components/viewer-hover-label.js +28 -0
- package/dist/ui/components/viewer-scene-header.d.ts +34 -0
- package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
- package/dist/ui/components/viewer-scene-header.js +92 -0
- package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
- package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
- package/dist/ui/components/viewer-selection-manager.js +116 -0
- package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
- package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage-modes.js +42 -0
- package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
- package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage-switcher.js +16 -0
- package/dist/ui/components/viewer-stage.d.ts +25 -0
- package/dist/ui/components/viewer-stage.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage.js +107 -0
- package/dist/ui/components/walkthrough-hud.d.ts +29 -0
- package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
- package/dist/ui/components/walkthrough-hud.js +55 -0
- package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
- package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
- package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
- package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
- package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
- package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview.js +700 -0
- package/dist/ui/hooks/use-mobile.d.ts +2 -0
- package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
- package/dist/ui/hooks/use-mobile.js +14 -0
- package/dist/ui/index.d.ts +49 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +48 -0
- package/dist/ui/lib/below-grade.d.ts +38 -0
- package/dist/ui/lib/below-grade.d.ts.map +1 -0
- package/dist/ui/lib/below-grade.js +102 -0
- package/dist/ui/lib/camera-pose.d.ts +19 -0
- package/dist/ui/lib/camera-pose.d.ts.map +1 -0
- package/dist/ui/lib/camera-pose.js +113 -0
- package/dist/ui/lib/cn.d.ts +3 -0
- package/dist/ui/lib/cn.d.ts.map +1 -0
- package/dist/ui/lib/cn.js +5 -0
- package/dist/ui/lib/level-name.d.ts +10 -0
- package/dist/ui/lib/level-name.d.ts.map +1 -0
- package/dist/ui/lib/level-name.js +33 -0
- package/dist/ui/lib/node-display-label.d.ts +8 -0
- package/dist/ui/lib/node-display-label.d.ts.map +1 -0
- package/dist/ui/lib/node-display-label.js +36 -0
- package/dist/ui/lib/room-area.d.ts +3 -0
- package/dist/ui/lib/room-area.d.ts.map +1 -0
- package/dist/ui/lib/room-area.js +8 -0
- package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
- package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
- package/dist/ui/lib/room-assembly-overlay.js +222 -0
- package/dist/ui/lib/room-name-messages.d.ts +28 -0
- package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
- package/dist/ui/lib/room-name-messages.js +112 -0
- package/dist/ui/lib/room-selection.d.ts +58 -0
- package/dist/ui/lib/room-selection.d.ts.map +1 -0
- package/dist/ui/lib/room-selection.js +297 -0
- package/dist/ui/lib/scene-theme-name.d.ts +4 -0
- package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
- package/dist/ui/lib/scene-theme-name.js +22 -0
- package/dist/ui/lib/selection-modifiers.d.ts +20 -0
- package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
- package/dist/ui/lib/selection-modifiers.js +15 -0
- package/dist/ui/lib/viewer-selection.d.ts +132 -0
- package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
- package/dist/ui/lib/viewer-selection.js +462 -0
- package/dist/ui/lib/zone-content.d.ts +6 -0
- package/dist/ui/lib/zone-content.d.ts.map +1 -0
- package/dist/ui/lib/zone-content.js +96 -0
- package/dist/ui/primitives/button.d.ts +11 -0
- package/dist/ui/primitives/button.d.ts.map +1 -0
- package/dist/ui/primitives/button.js +35 -0
- package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
- package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
- package/dist/ui/primitives/dropdown-menu.js +53 -0
- package/dist/ui/primitives/popover.d.ts +8 -0
- package/dist/ui/primitives/popover.d.ts.map +1 -0
- package/dist/ui/primitives/popover.js +17 -0
- package/dist/ui/primitives/switch.d.ts +5 -0
- package/dist/ui/primitives/switch.d.ts.map +1 -0
- package/dist/ui/primitives/switch.js +8 -0
- package/dist/ui/primitives/tooltip.d.ts +8 -0
- package/dist/ui/primitives/tooltip.d.ts.map +1 -0
- package/dist/ui/primitives/tooltip.js +17 -0
- package/dist/ui/store/camera-pose-store.d.ts +17 -0
- package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
- package/dist/ui/store/camera-pose-store.js +15 -0
- package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
- package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
- package/dist/ui/store/navigation-sync-pose-store.js +16 -0
- package/dist/ui/store/use-follow-pascal.d.ts +30 -0
- package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
- package/dist/ui/store/use-follow-pascal.js +28 -0
- package/dist/ui/theme.css +339 -0
- package/package.json +34 -7
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type SoundName } from './sounds';
|
|
2
|
+
/** A host cue with filenames relative to the configured SFX base URL. */
|
|
3
|
+
export type SoundDefinition = {
|
|
4
|
+
src: string | string[];
|
|
5
|
+
rateRange?: [number, number];
|
|
6
|
+
volumeRange?: [number, number];
|
|
7
|
+
minIntervalMs?: number;
|
|
8
|
+
loop?: boolean;
|
|
9
|
+
volumeMultiplier?: number;
|
|
10
|
+
fadeInMs?: number;
|
|
11
|
+
fadeOutMs?: number;
|
|
12
|
+
};
|
|
13
|
+
/** Per-play gain, stereo position and local/remote repeat suppression. */
|
|
14
|
+
export type SoundPlaybackOptions = {
|
|
15
|
+
source?: 'local' | 'remote';
|
|
16
|
+
stereo?: number;
|
|
17
|
+
volumeMultiplier?: number;
|
|
18
|
+
};
|
|
19
|
+
/** Warm built-in cues and optional host cues without playing them. */
|
|
20
|
+
export declare function preloadSounds(extraSounds?: readonly SoundDefinition[]): void;
|
|
21
|
+
/** Unload the shared sound cache and detach its settings subscription. */
|
|
22
|
+
export declare function disposeSounds(): void;
|
|
23
|
+
/** Start a host-defined loop, fading out the previously active loop. */
|
|
24
|
+
export declare function startSoundLoop(name: SoundDefinition): void;
|
|
25
|
+
/** Fade out the active loop and cancel any pending loop playback. */
|
|
26
|
+
export declare function stopSoundLoop(): void;
|
|
27
|
+
/** Play a built-in cue or host definition using the shared audio settings. */
|
|
28
|
+
export declare function playSound(sound: SoundName | SoundDefinition, options?: SoundPlaybackOptions): void;
|
|
29
|
+
/** Apply current volume and mute settings to cached sounds and the active loop. */
|
|
30
|
+
export declare function updateSoundVolumes(): void;
|
|
31
|
+
//# sourceMappingURL=player.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../src/sfx/player.ts"],"names":[],"mappings":"AAGA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AAEjD,yEAAyE;AACzE,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACtB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AA4ED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,WAAW,GAAE,SAAS,eAAe,EAAO,QA6BzE;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,SAK5B;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,QA4CnD;AAED,qEAAqE;AACrE,wBAAgB,aAAa,SAK5B;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,eAAe,EAAE,OAAO,GAAE,oBAAyB,QA2D/F;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,SAuBjC"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Howl, Howler } from 'howler';
|
|
2
|
+
import { getViewerConfig } from '../lib/viewer-config';
|
|
3
|
+
import { useAudioSettings } from './audio-settings';
|
|
4
|
+
import { SOUNDS } from './sounds';
|
|
5
|
+
const DEFAULT_MIN_INTERVAL_MS = 30;
|
|
6
|
+
const SFX_FAILURE_BACKOFF_MS = 5_000;
|
|
7
|
+
const DEFAULT_LOOP_FADE_MS = 90;
|
|
8
|
+
function randomInRange([min, max]) {
|
|
9
|
+
return min + Math.random() * (max - min);
|
|
10
|
+
}
|
|
11
|
+
const sfxCache = new Map();
|
|
12
|
+
let cachedBaseUrl = null;
|
|
13
|
+
let unsubscribeAudio = null;
|
|
14
|
+
const definitions = new Set();
|
|
15
|
+
let sfxAudioContext = null;
|
|
16
|
+
let sfxRetryAfter = 0;
|
|
17
|
+
const lastPlayedAt = new Map();
|
|
18
|
+
const lastVariation = new Map();
|
|
19
|
+
let activeLoop = null;
|
|
20
|
+
let pendingLoop = null;
|
|
21
|
+
let requestedLoop = null;
|
|
22
|
+
function loopVolume(name) {
|
|
23
|
+
const { masterVolume, sfxVolume } = useAudioSettings.getState();
|
|
24
|
+
return (masterVolume / 100) * (sfxVolume / 100) * (name.volumeMultiplier ?? 1);
|
|
25
|
+
}
|
|
26
|
+
function stopActiveLoop(fadeMs) {
|
|
27
|
+
const active = activeLoop;
|
|
28
|
+
if (!active)
|
|
29
|
+
return;
|
|
30
|
+
activeLoop = null;
|
|
31
|
+
if (fadeMs <= 0) {
|
|
32
|
+
active.sound.stop(active.id);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
active.sound.once('fade', () => active.sound.stop(active.id), active.id);
|
|
36
|
+
active.sound.fade(active.volume, 0, fadeMs, active.id);
|
|
37
|
+
}
|
|
38
|
+
function unloadCachedSounds(resetPlaybackState) {
|
|
39
|
+
requestedLoop = null;
|
|
40
|
+
pendingLoop = null;
|
|
41
|
+
stopActiveLoop(0);
|
|
42
|
+
for (const sounds of sfxCache.values()) {
|
|
43
|
+
for (const sound of sounds) {
|
|
44
|
+
try {
|
|
45
|
+
sound.unload();
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
sfxCache.clear();
|
|
51
|
+
sfxAudioContext = null;
|
|
52
|
+
if (resetPlaybackState) {
|
|
53
|
+
sfxRetryAfter = 0;
|
|
54
|
+
lastPlayedAt.clear();
|
|
55
|
+
lastVariation.clear();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function cacheNeedsRebuild() {
|
|
59
|
+
if (sfxCache.size !== definitions.size || sfxCache.size === 0)
|
|
60
|
+
return true;
|
|
61
|
+
if (cachedBaseUrl !== getViewerConfig().sfxBaseUrl)
|
|
62
|
+
return true;
|
|
63
|
+
if (sfxAudioContext !== Howler.ctx)
|
|
64
|
+
return true;
|
|
65
|
+
for (const sounds of sfxCache.values()) {
|
|
66
|
+
if (sounds.some((sound) => sound.state() === 'unloaded'))
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
/** Warm built-in cues and optional host cues without playing them. */
|
|
72
|
+
export function preloadSounds(extraSounds = []) {
|
|
73
|
+
for (const definition of [...Object.values(SOUNDS), ...extraSounds])
|
|
74
|
+
definitions.add(definition);
|
|
75
|
+
if (performance.now() < sfxRetryAfter)
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
if (!unsubscribeAudio)
|
|
79
|
+
unsubscribeAudio = useAudioSettings.subscribe(updateSoundVolumes);
|
|
80
|
+
if (!cacheNeedsRebuild())
|
|
81
|
+
return;
|
|
82
|
+
unloadCachedSounds(false);
|
|
83
|
+
const baseUrl = getViewerConfig().sfxBaseUrl.replace(/\/+$/, '');
|
|
84
|
+
cachedBaseUrl = getViewerConfig().sfxBaseUrl;
|
|
85
|
+
for (const config of definitions) {
|
|
86
|
+
const sources = Array.isArray(config.src) ? config.src : [config.src];
|
|
87
|
+
sfxCache.set(config, sources.map((src) => new Howl({
|
|
88
|
+
loop: config.loop ?? false,
|
|
89
|
+
src: [`${baseUrl}/${src}`],
|
|
90
|
+
preload: true,
|
|
91
|
+
volume: 0.5,
|
|
92
|
+
})));
|
|
93
|
+
}
|
|
94
|
+
sfxAudioContext = Howler.ctx ?? null;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
unloadCachedSounds(false);
|
|
98
|
+
sfxRetryAfter = performance.now() + SFX_FAILURE_BACKOFF_MS;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Unload the shared sound cache and detach its settings subscription. */
|
|
102
|
+
export function disposeSounds() {
|
|
103
|
+
unsubscribeAudio?.();
|
|
104
|
+
unsubscribeAudio = null;
|
|
105
|
+
unloadCachedSounds(true);
|
|
106
|
+
definitions.clear();
|
|
107
|
+
}
|
|
108
|
+
/** Start a host-defined loop, fading out the previously active loop. */
|
|
109
|
+
export function startSoundLoop(name) {
|
|
110
|
+
definitions.add(name);
|
|
111
|
+
const { muted } = useAudioSettings.getState();
|
|
112
|
+
if (muted) {
|
|
113
|
+
stopSoundLoop();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
requestedLoop = name;
|
|
117
|
+
const current = activeLoop;
|
|
118
|
+
if (current?.name === name && current.sound.playing(current.id))
|
|
119
|
+
return;
|
|
120
|
+
if (current)
|
|
121
|
+
stopActiveLoop(current.name.fadeOutMs ?? DEFAULT_LOOP_FADE_MS);
|
|
122
|
+
const now = performance.now();
|
|
123
|
+
if (now < sfxRetryAfter)
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
preloadSounds();
|
|
127
|
+
// A fresh Howler context rebuilds the cache and clears stale playback
|
|
128
|
+
// state. Restore this current press after that cleanup so a first-use loop
|
|
129
|
+
// can still begin when its asset finishes loading.
|
|
130
|
+
requestedLoop = name;
|
|
131
|
+
const sound = sfxCache.get(name)?.[0];
|
|
132
|
+
if (!sound)
|
|
133
|
+
return;
|
|
134
|
+
if (sound.state() !== 'loaded') {
|
|
135
|
+
if (pendingLoop?.name === name && pendingLoop.sound === sound)
|
|
136
|
+
return;
|
|
137
|
+
pendingLoop = { name, sound };
|
|
138
|
+
sound.once('load', () => {
|
|
139
|
+
if (pendingLoop?.sound === sound)
|
|
140
|
+
pendingLoop = null;
|
|
141
|
+
if (requestedLoop === name)
|
|
142
|
+
startSoundLoop(name);
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const config = name;
|
|
147
|
+
const volume = loopVolume(name);
|
|
148
|
+
const id = sound.play();
|
|
149
|
+
sound.volume(0, id);
|
|
150
|
+
activeLoop = { id, name, sound, volume };
|
|
151
|
+
sound.fade(0, volume, config.fadeInMs ?? DEFAULT_LOOP_FADE_MS, id);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
unloadCachedSounds(false);
|
|
155
|
+
sfxRetryAfter = now + SFX_FAILURE_BACKOFF_MS;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Fade out the active loop and cancel any pending loop playback. */
|
|
159
|
+
export function stopSoundLoop() {
|
|
160
|
+
requestedLoop = null;
|
|
161
|
+
pendingLoop = null;
|
|
162
|
+
const fadeMs = activeLoop ? (activeLoop.name.fadeOutMs ?? DEFAULT_LOOP_FADE_MS) : 0;
|
|
163
|
+
stopActiveLoop(fadeMs);
|
|
164
|
+
}
|
|
165
|
+
/** Play a built-in cue or host definition using the shared audio settings. */
|
|
166
|
+
export function playSound(sound, options = {}) {
|
|
167
|
+
const name = typeof sound === 'string' ? SOUNDS[sound] : sound;
|
|
168
|
+
const config = name;
|
|
169
|
+
definitions.add(config);
|
|
170
|
+
const { masterVolume, sfxVolume, muted } = useAudioSettings.getState();
|
|
171
|
+
if (muted)
|
|
172
|
+
return;
|
|
173
|
+
// Drop rapid repeats — two plays of the same SFX within minIntervalMs just
|
|
174
|
+
// smear into noise, they don't add useful information.
|
|
175
|
+
const now = performance.now();
|
|
176
|
+
if (now < sfxRetryAfter)
|
|
177
|
+
return;
|
|
178
|
+
const minInterval = config.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
179
|
+
const source = options.source ?? 'local';
|
|
180
|
+
const times = lastPlayedAt.get(name) ?? {};
|
|
181
|
+
const last = times[source];
|
|
182
|
+
if (last !== undefined && now - last < minInterval)
|
|
183
|
+
return;
|
|
184
|
+
// Local feedback stays legible when a collaborator makes the same kind of
|
|
185
|
+
// change at nearly the same time; the quieter remote cue yields instead.
|
|
186
|
+
const lastLocal = times.local;
|
|
187
|
+
if (source === 'remote' && lastLocal !== undefined && now - lastLocal < 120)
|
|
188
|
+
return;
|
|
189
|
+
times[source] = now;
|
|
190
|
+
lastPlayedAt.set(name, times);
|
|
191
|
+
try {
|
|
192
|
+
preloadSounds();
|
|
193
|
+
const sounds = sfxCache.get(name);
|
|
194
|
+
if (!sounds || sounds.length === 0)
|
|
195
|
+
return;
|
|
196
|
+
// Pick a random variation, avoiding an immediate repeat of the last one so
|
|
197
|
+
// consecutive plays don't land on the same file.
|
|
198
|
+
let index = Math.floor(Math.random() * sounds.length);
|
|
199
|
+
if (sounds.length > 1 && index === lastVariation.get(name)) {
|
|
200
|
+
index = (index + 1) % sounds.length;
|
|
201
|
+
}
|
|
202
|
+
lastVariation.set(name, index);
|
|
203
|
+
const sound = sounds[index];
|
|
204
|
+
// Howler queues per-play mutations while a sound is loading. If its global
|
|
205
|
+
// AudioContext is replaced before that queue drains, stereo setup can try
|
|
206
|
+
// to connect nodes from different contexts and throw asynchronously.
|
|
207
|
+
if (sound.state() !== 'loaded')
|
|
208
|
+
return;
|
|
209
|
+
const baseVolume = (masterVolume / 100) * (sfxVolume / 100);
|
|
210
|
+
const volumeJitter = config.volumeRange ? randomInRange(config.volumeRange) : 1;
|
|
211
|
+
const volumeMultiplier = Number.isFinite(options.volumeMultiplier)
|
|
212
|
+
? Math.max(0, Math.min(2, options.volumeMultiplier))
|
|
213
|
+
: 1;
|
|
214
|
+
const rate = config.rateRange ? randomInRange(config.rateRange) : 1;
|
|
215
|
+
const id = sound.play();
|
|
216
|
+
sound.volume(baseVolume * volumeJitter * volumeMultiplier, id);
|
|
217
|
+
if (Number.isFinite(options.stereo)) {
|
|
218
|
+
sound.stereo(Math.max(-1, Math.min(1, options.stereo)), id);
|
|
219
|
+
}
|
|
220
|
+
if (rate !== 1)
|
|
221
|
+
sound.rate(rate, id);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Optional audio must never abort an input callback. Rebuild from
|
|
225
|
+
// the current Howler context after a backoff instead of retrying every pointer cue.
|
|
226
|
+
unloadCachedSounds(false);
|
|
227
|
+
sfxRetryAfter = now + SFX_FAILURE_BACKOFF_MS;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Apply current volume and mute settings to cached sounds and the active loop. */
|
|
231
|
+
export function updateSoundVolumes() {
|
|
232
|
+
const { masterVolume, muted, sfxVolume } = useAudioSettings.getState();
|
|
233
|
+
if (muted) {
|
|
234
|
+
stopSoundLoop();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const finalVolume = (masterVolume / 100) * (sfxVolume / 100);
|
|
238
|
+
try {
|
|
239
|
+
if (performance.now() < sfxRetryAfter)
|
|
240
|
+
return;
|
|
241
|
+
sfxCache.forEach((sounds) => {
|
|
242
|
+
sounds.forEach((sound) => {
|
|
243
|
+
sound.volume(finalVolume);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
if (activeLoop) {
|
|
247
|
+
activeLoop.volume = loopVolume(activeLoop.name);
|
|
248
|
+
activeLoop.sound.volume(activeLoop.volume, activeLoop.id);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
unloadCachedSounds(false);
|
|
253
|
+
sfxRetryAfter = performance.now() + SFX_FAILURE_BACKOFF_MS;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Built-in interaction and viewer UI cues, keyed by their public names. */
|
|
2
|
+
export declare const SOUNDS: {
|
|
3
|
+
menuHover: {
|
|
4
|
+
src: string;
|
|
5
|
+
rateRange: [number, number];
|
|
6
|
+
volumeRange: [number, number];
|
|
7
|
+
minIntervalMs: number;
|
|
8
|
+
};
|
|
9
|
+
menuClick: {
|
|
10
|
+
src: string;
|
|
11
|
+
rateRange: [number, number];
|
|
12
|
+
volumeRange: [number, number];
|
|
13
|
+
};
|
|
14
|
+
doorOpen: {
|
|
15
|
+
src: string;
|
|
16
|
+
rateRange: [number, number];
|
|
17
|
+
volumeRange: [number, number];
|
|
18
|
+
};
|
|
19
|
+
doorClose: {
|
|
20
|
+
src: string;
|
|
21
|
+
rateRange: [number, number];
|
|
22
|
+
volumeRange: [number, number];
|
|
23
|
+
};
|
|
24
|
+
doorSwing: {
|
|
25
|
+
src: string;
|
|
26
|
+
rateRange: [number, number];
|
|
27
|
+
};
|
|
28
|
+
garageDoor: {
|
|
29
|
+
src: string;
|
|
30
|
+
volumeRange: [number, number];
|
|
31
|
+
};
|
|
32
|
+
shuttersOpen: {
|
|
33
|
+
src: string;
|
|
34
|
+
rateRange: [number, number];
|
|
35
|
+
};
|
|
36
|
+
shuttersClose: {
|
|
37
|
+
src: string;
|
|
38
|
+
rateRange: [number, number];
|
|
39
|
+
};
|
|
40
|
+
switchOn: {
|
|
41
|
+
src: string;
|
|
42
|
+
};
|
|
43
|
+
switchOff: {
|
|
44
|
+
src: string;
|
|
45
|
+
};
|
|
46
|
+
viewerHover: {
|
|
47
|
+
src: string;
|
|
48
|
+
minIntervalMs: number;
|
|
49
|
+
};
|
|
50
|
+
viewerSelect: {
|
|
51
|
+
src: string;
|
|
52
|
+
minIntervalMs: number;
|
|
53
|
+
};
|
|
54
|
+
viewerDeselect: {
|
|
55
|
+
src: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/** A built-in interaction or viewer UI cue name. */
|
|
59
|
+
export type SoundName = keyof typeof SOUNDS;
|
|
60
|
+
//# sourceMappingURL=sounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sounds.d.ts","sourceRoot":"","sources":["../../src/sfx/sounds.ts"],"names":[],"mappings":"AAEA,4EAA4E;AAC5E,eAAO,MAAM,MAAM;;QAEf,GAAG;QACH,SAAS;QACT,WAAW;QACX,aAAa;;;QAGb,GAAG;QACH,SAAS;QACT,WAAW;;;QAED,GAAG;QAAmB,SAAS;QAAgB,WAAW;;;QACzD,GAAG;QAAoB,SAAS;QAAgB,WAAW;;;QAC3D,GAAG;QAAoB,SAAS;;;QAC/B,GAAG;QAAqB,WAAW;;;QACjC,GAAG;QAAuB,SAAS;;;QAClC,GAAG;QAAwB,SAAS;;;QACzC,GAAG;;;QACF,GAAG;;;QACD,GAAG;QAAsB,aAAa;;;QACrC,GAAG;QAAuB,aAAa;;;QACrC,GAAG;;CACoB,CAAA;AAE3C,oDAAoD;AACpD,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,MAAM,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Built-in interaction and viewer UI cues, keyed by their public names. */
|
|
2
|
+
export const SOUNDS = {
|
|
3
|
+
menuHover: {
|
|
4
|
+
src: 'menu_hover.mp3',
|
|
5
|
+
rateRange: [0.98, 1.02],
|
|
6
|
+
volumeRange: [0.2, 0.3],
|
|
7
|
+
minIntervalMs: 0,
|
|
8
|
+
},
|
|
9
|
+
menuClick: {
|
|
10
|
+
src: 'menu_click.mp3',
|
|
11
|
+
rateRange: [0.98, 1.02],
|
|
12
|
+
volumeRange: [0.5, 0.6],
|
|
13
|
+
},
|
|
14
|
+
doorOpen: { src: 'door_open.mp3', rateRange: [0.96, 1.04], volumeRange: [0.9, 1] },
|
|
15
|
+
doorClose: { src: 'door_close.mp3', rateRange: [0.96, 1.04], volumeRange: [0.9, 1] },
|
|
16
|
+
doorSwing: { src: 'door_swing.mp3', rateRange: [0.97, 1.03] },
|
|
17
|
+
garageDoor: { src: 'garage_door.mp3', volumeRange: [0.9, 1] },
|
|
18
|
+
shuttersOpen: { src: 'shutters_open.mp3', rateRange: [0.96, 1.04] },
|
|
19
|
+
shuttersClose: { src: 'shutters_close.mp3', rateRange: [0.96, 1.04] },
|
|
20
|
+
switchOn: { src: 'switch_on.mp3' },
|
|
21
|
+
switchOff: { src: 'switch_off.mp3' },
|
|
22
|
+
viewerHover: { src: 'viewer_hover.mp3', minIntervalMs: 95 },
|
|
23
|
+
viewerSelect: { src: 'viewer_select.mp3', minIntervalMs: 90 },
|
|
24
|
+
viewerDeselect: { src: 'viewer_deselect.mp3' },
|
|
25
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type useViewer from '../store/use-viewer';
|
|
2
|
+
type Selection = ReturnType<typeof useViewer.getState>['selection'];
|
|
3
|
+
/** Which tick a selection change plays: anything newly picked selects, anything let go deselects. */
|
|
4
|
+
export declare function selectionCue(previous: Selection, next: Selection): 'viewerSelect' | 'viewerDeselect' | null;
|
|
5
|
+
/** Play selection and hover cues after user activation, except during walkthrough. */
|
|
6
|
+
export declare function useViewerUiSounds(): void;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=use-viewer-ui-sounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-viewer-ui-sounds.d.ts","sourceRoot":"","sources":["../../src/sfx/use-viewer-ui-sounds.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,SAAS,MAAM,qBAAqB,CAAA;AAGhD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,CAAA;AAEnE,qGAAqG;AACrG,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,SAAS,EACnB,IAAI,EAAE,SAAS,GACd,cAAc,GAAG,gBAAgB,GAAG,IAAI,CAW1C;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,SAsBhC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { playSound, preloadSounds } from './player';
|
|
4
|
+
/** Which tick a selection change plays: anything newly picked selects, anything let go deselects. */
|
|
5
|
+
export function selectionCue(previous, next) {
|
|
6
|
+
const picked = (next.levelId && next.levelId !== previous.levelId) ||
|
|
7
|
+
(next.zoneId && next.zoneId !== previous.zoneId) ||
|
|
8
|
+
next.selectedIds.some((id) => !previous.selectedIds.includes(id));
|
|
9
|
+
if (picked)
|
|
10
|
+
return 'viewerSelect';
|
|
11
|
+
const released = (previous.levelId && !next.levelId) ||
|
|
12
|
+
(previous.zoneId && !next.zoneId) ||
|
|
13
|
+
previous.selectedIds.some((id) => !next.selectedIds.includes(id));
|
|
14
|
+
return released ? 'viewerDeselect' : null;
|
|
15
|
+
}
|
|
16
|
+
/** Play selection and hover cues after user activation, except during walkthrough. */
|
|
17
|
+
export function useViewerUiSounds() {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let disposed = false;
|
|
20
|
+
let unsubscribe;
|
|
21
|
+
preloadSounds();
|
|
22
|
+
void import('../store/use-viewer').then(({ default: viewer }) => {
|
|
23
|
+
if (disposed)
|
|
24
|
+
return;
|
|
25
|
+
unsubscribe = viewer.subscribe((state, previous) => {
|
|
26
|
+
if (state.walkthroughMode || !globalThis.navigator?.userActivation?.hasBeenActive)
|
|
27
|
+
return;
|
|
28
|
+
if (state.selection !== previous.selection) {
|
|
29
|
+
const cue = selectionCue(previous.selection, state.selection);
|
|
30
|
+
if (cue)
|
|
31
|
+
playSound(cue);
|
|
32
|
+
}
|
|
33
|
+
if (state.hoveredId && state.hoveredId !== previous.hoveredId && !state.cameraDragging)
|
|
34
|
+
playSound('viewerHover');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
disposed = true;
|
|
39
|
+
unsubscribe?.();
|
|
40
|
+
};
|
|
41
|
+
}, []);
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type WalkthroughInteract = {
|
|
2
|
+
label: string;
|
|
3
|
+
verb: string;
|
|
4
|
+
} | null;
|
|
5
|
+
export type FirstPersonHudState = {
|
|
6
|
+
floorLabel: string | null;
|
|
7
|
+
zoneLabel: string | null;
|
|
8
|
+
interact: WalkthroughInteract;
|
|
9
|
+
setHud: (hud: Partial<Pick<FirstPersonHudState, 'floorLabel' | 'zoneLabel' | 'interact'>>) => void;
|
|
10
|
+
reset: () => void;
|
|
11
|
+
};
|
|
12
|
+
export declare const useFirstPersonHud: import("zustand").UseBoundStore<import("zustand").StoreApi<FirstPersonHudState>>;
|
|
13
|
+
//# sourceMappingURL=use-first-person-hud.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-first-person-hud.d.ts","sourceRoot":"","sources":["../../src/store/use-first-person-hud.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAA;AAExE,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,EAAE,mBAAmB,CAAA;IAC7B,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,YAAY,GAAG,WAAW,GAAG,UAAU,CAAC,CAAC,KAAK,IAAI,CAAA;IAClG,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB,CAAA;AAED,eAAO,MAAM,iBAAiB,kFA2B3B,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
export const useFirstPersonHud = create((set) => ({
|
|
3
|
+
floorLabel: null,
|
|
4
|
+
zoneLabel: null,
|
|
5
|
+
interact: null,
|
|
6
|
+
setHud: (hud) => set((state) => {
|
|
7
|
+
const floorLabel = hud.floorLabel === undefined ? state.floorLabel : hud.floorLabel;
|
|
8
|
+
const zoneLabel = hud.zoneLabel === undefined ? state.zoneLabel : hud.zoneLabel;
|
|
9
|
+
const interact = hud.interact === undefined ? state.interact : hud.interact;
|
|
10
|
+
if (floorLabel === state.floorLabel &&
|
|
11
|
+
zoneLabel === state.zoneLabel &&
|
|
12
|
+
interact?.label === state.interact?.label &&
|
|
13
|
+
interact?.verb === state.interact?.verb) {
|
|
14
|
+
return state;
|
|
15
|
+
}
|
|
16
|
+
return { floorLabel, zoneLabel, interact };
|
|
17
|
+
}),
|
|
18
|
+
reset: () => set((state) => state.floorLabel === null && state.zoneLabel === null && state.interact === null
|
|
19
|
+
? state
|
|
20
|
+
: { floorLabel: null, zoneLabel: null, interact: null }),
|
|
21
|
+
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAgCZ,KAAK,eAAe,EAUrB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"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"}
|
|
@@ -120,9 +120,14 @@ const MAX_ROOFS_PER_FRAME = 1;
|
|
|
120
120
|
const MAX_SEGMENTS_PER_FRAME = 3;
|
|
121
121
|
/** What each roof's shell was last merged from: its node and its segments, as the store held them. */
|
|
122
122
|
const mergedFrom = new Map();
|
|
123
|
+
/** A roof's shell is merged from the roof and its segments; its faces of other kinds draw apart. */
|
|
124
|
+
const shellInputIds = (roof, nodes) => [
|
|
125
|
+
roof.id,
|
|
126
|
+
...(roof.children ?? []).filter((id) => nodes[id]?.type === 'roof-segment'),
|
|
127
|
+
];
|
|
123
128
|
function rememberMergedInputs(roof, nodes) {
|
|
124
129
|
const inputs = new Map();
|
|
125
|
-
for (const id of
|
|
130
|
+
for (const id of shellInputIds(roof, nodes)) {
|
|
126
131
|
const node = nodes[id];
|
|
127
132
|
if (node)
|
|
128
133
|
inputs.set(id, node);
|
|
@@ -135,7 +140,7 @@ function mergedFromStillHolds(roofId, nodes) {
|
|
|
135
140
|
const roof = nodes[roofId];
|
|
136
141
|
if (!inputs || roof?.type !== 'roof')
|
|
137
142
|
return false;
|
|
138
|
-
const ids =
|
|
143
|
+
const ids = shellInputIds(roof, nodes);
|
|
139
144
|
return ids.length === inputs.size && ids.every((id) => inputs.get(id) === nodes[id]);
|
|
140
145
|
}
|
|
141
146
|
/**
|
|
@@ -498,7 +503,7 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes, framing) {
|
|
|
498
503
|
return cloneCachedMergedRoofSegmentBrushes(cached);
|
|
499
504
|
}
|
|
500
505
|
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, {
|
|
501
|
-
fascia: wearsFascia(segment, roofNode),
|
|
506
|
+
fascia: wearsFascia(segment, roofNode, nodes),
|
|
502
507
|
framing: framingDepth,
|
|
503
508
|
}));
|
|
504
509
|
if (!brushes) {
|
|
@@ -562,10 +567,11 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
|
|
|
562
567
|
// Merge each child through `getEffectiveNode` so an in-flight handle
|
|
563
568
|
// drag (live override on width / depth / wallHeight / pitch / rotation)
|
|
564
569
|
// is reflected in the merged shell during the drag, not only on commit.
|
|
570
|
+
// Faces of other kinds (`capabilities.roofs`) render as their own kind, outside the shell.
|
|
565
571
|
const children = (roofNode.children ?? [])
|
|
566
572
|
.map((id) => {
|
|
567
573
|
const scn = nodes[id];
|
|
568
|
-
return scn ? getEffectiveNode(scn) : undefined;
|
|
574
|
+
return scn?.type === 'roof-segment' ? getEffectiveNode(scn) : undefined;
|
|
569
575
|
})
|
|
570
576
|
.filter((n) => n !== undefined && !hasSegmentMaterialOverride(n) && !isIndependentRoofSegment(n));
|
|
571
577
|
if (children.length === 0) {
|
|
@@ -628,7 +634,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
|
|
|
628
634
|
else {
|
|
629
635
|
totalDeckSlab = brushes.deckSlab;
|
|
630
636
|
}
|
|
631
|
-
if (!shouldIncludeRoofSegmentWallShell(child, roofNode)) {
|
|
637
|
+
if (!shouldIncludeRoofSegmentWallShell(child, roofNode, nodes)) {
|
|
632
638
|
brushes.wallBrush.geometry.dispose();
|
|
633
639
|
brushes.innerBrush.geometry.dispose();
|
|
634
640
|
}
|
|
@@ -695,7 +701,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
|
|
|
695
701
|
}
|
|
696
702
|
let finalGeo = addShedInsetEndPanels(resultGeo, csgChildren, true);
|
|
697
703
|
splitRoofWallBandFaces(finalGeo, csgChildren
|
|
698
|
-
.filter((child) => shouldIncludeRoofSegmentWallShell(child, roofNode))
|
|
704
|
+
.filter((child) => shouldIncludeRoofSegmentWallShell(child, roofNode, nodes))
|
|
699
705
|
.map((child) => ({
|
|
700
706
|
node: child,
|
|
701
707
|
inverse: new THREE.Matrix4()
|
|
@@ -854,7 +860,7 @@ function getMergedRoofSegmentCacheKey(roofNode, segment, nodes, framing) {
|
|
|
854
860
|
roofPosition: roofNode.position ?? [0, 0, 0],
|
|
855
861
|
roofRotation: roofNode.rotation ?? 0,
|
|
856
862
|
// a shed's fascia depends on whether its roof is a composite of pieces
|
|
857
|
-
composite: (roofNode
|
|
863
|
+
composite: isCompositeRoof(roofNode, nodes),
|
|
858
864
|
segment,
|
|
859
865
|
accessories: getMergedRoofAccessoryCachePayload(segment, nodes),
|
|
860
866
|
framing,
|
|
@@ -986,7 +992,15 @@ function readShedOpenEndSides(node) {
|
|
|
986
992
|
return new Set();
|
|
987
993
|
return new Set(value.filter((side) => side === 'left' || side === 'right'));
|
|
988
994
|
}
|
|
989
|
-
|
|
995
|
+
/**
|
|
996
|
+
* Whether a roof's shell is pieced from several segments. Its faces of other kinds
|
|
997
|
+
* (`capabilities.roofs`) are no pieces of it; without the scene, every child counts.
|
|
998
|
+
*/
|
|
999
|
+
function isCompositeRoof(roof, nodes) {
|
|
1000
|
+
const children = roof.children ?? [];
|
|
1001
|
+
return (nodes ? children.filter((id) => nodes[id]?.type === 'roof-segment') : children).length > 1;
|
|
1002
|
+
}
|
|
1003
|
+
function shouldIncludeRoofSegmentWallShell(node, parentRoof, nodes) {
|
|
990
1004
|
if (node.wallShell === 'include')
|
|
991
1005
|
return true;
|
|
992
1006
|
if (node.wallShell === 'omit')
|
|
@@ -996,7 +1010,7 @@ function shouldIncludeRoofSegmentWallShell(node, parentRoof) {
|
|
|
996
1010
|
// Older composite roofs use overlapping shed segments as deck pieces. Their
|
|
997
1011
|
// wall volumes were never part of the rendered shell and make CSG grow
|
|
998
1012
|
// exponentially when unioned together.
|
|
999
|
-
return !parentRoof || (parentRoof
|
|
1013
|
+
return !parentRoof || !isCompositeRoof(parentRoof, nodes);
|
|
1000
1014
|
}
|
|
1001
1015
|
function hasSegmentTrim(node) {
|
|
1002
1016
|
const trim = normalizeRoofSegmentTrim(node);
|
|
@@ -1454,14 +1468,14 @@ function buildEaveCuts(node, reach, eave, seal) {
|
|
|
1454
1468
|
* one deck (the same pieces whose wall shells are left out), so boards would
|
|
1455
1469
|
* show between them.
|
|
1456
1470
|
*/
|
|
1457
|
-
function wearsFascia(node, parentRoof) {
|
|
1471
|
+
function wearsFascia(node, parentRoof, nodes) {
|
|
1458
1472
|
if (node.fascia !== true)
|
|
1459
1473
|
return false;
|
|
1460
1474
|
if (node.roofType !== 'shed')
|
|
1461
1475
|
return true;
|
|
1462
1476
|
if (isLeanToShedSegment(node))
|
|
1463
1477
|
return false;
|
|
1464
|
-
return !parentRoof || (parentRoof
|
|
1478
|
+
return !parentRoof || !isCompositeRoof(parentRoof, nodes);
|
|
1465
1479
|
}
|
|
1466
1480
|
/** What a framed roof's deck keeps over its open structure band at least: a 7/16 in sheet. */
|
|
1467
1481
|
const ROOF_DECK_KEPT = 0.0111;
|
|
@@ -1967,7 +1981,7 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
|
|
|
1967
1981
|
return result;
|
|
1968
1982
|
}
|
|
1969
1983
|
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, {
|
|
1970
|
-
fascia: wearsFascia(node, parentRoof),
|
|
1984
|
+
fascia: wearsFascia(node, parentRoof, nodes),
|
|
1971
1985
|
framing: options.framing ? roofFramingDepth(node) : 0,
|
|
1972
1986
|
}));
|
|
1973
1987
|
if (!brushes) {
|
|
@@ -1990,7 +2004,7 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
|
|
|
1990
2004
|
prepareBrushForCSG(shinDeck);
|
|
1991
2005
|
let combined = shinDeck;
|
|
1992
2006
|
let hollowWall = null;
|
|
1993
|
-
if (shouldIncludeRoofSegmentWallShell(node, parentRoof)) {
|
|
2007
|
+
if (shouldIncludeRoofSegmentWallShell(node, parentRoof, nodes)) {
|
|
1994
2008
|
hollowWall = csgEvaluator.evaluate(wallBrush, innerBrush, SUBTRACTION);
|
|
1995
2009
|
prepareBrushForCSG(hollowWall);
|
|
1996
2010
|
combined = csgEvaluator.evaluate(shinDeck, hollowWall, ADDITION);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button } from '../primitives/button';
|
|
3
|
+
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
|
4
|
+
label: string;
|
|
5
|
+
shortcut?: string;
|
|
6
|
+
isActive?: boolean;
|
|
7
|
+
tooltipContent?: React.ReactNode;
|
|
8
|
+
tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
9
|
+
/** The tooltip portals to `<body>`, so a button in a themed scope passes the scope here. */
|
|
10
|
+
tooltipClassName?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const ActionButton: React.ForwardRefExoticComponent<Omit<ActionButtonProps, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=action-button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-button.d.ts","sourceRoot":"","sources":["../../../src/ui/components/action-button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAI7C,UAAU,iBAAkB,SAAQ,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAChC,WAAW,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;IACjD,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,eAAO,MAAM,YAAY,0GAkExB,CAAA"}
|