@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.
Files changed (224) hide show
  1. package/README.md +157 -0
  2. package/dist/components/viewer/drone-controller.d.ts.map +1 -1
  3. package/dist/components/viewer/drone-controller.js +1 -5
  4. package/dist/components/viewer/first-person/build-collider-world.d.ts +14 -0
  5. package/dist/components/viewer/first-person/build-collider-world.d.ts.map +1 -0
  6. package/dist/components/viewer/first-person/build-collider-world.js +426 -0
  7. package/dist/components/viewer/first-person/ground-holes.d.ts +22 -0
  8. package/dist/components/viewer/first-person/ground-holes.d.ts.map +1 -0
  9. package/dist/components/viewer/first-person/ground-holes.js +116 -0
  10. package/dist/components/viewer/first-person/mechanism-targets.d.ts +27 -0
  11. package/dist/components/viewer/first-person/mechanism-targets.d.ts.map +1 -0
  12. package/dist/components/viewer/first-person/mechanism-targets.js +79 -0
  13. package/dist/components/viewer/first-person/terrain-collider.d.ts +32 -0
  14. package/dist/components/viewer/first-person/terrain-collider.d.ts.map +1 -0
  15. package/dist/components/viewer/first-person/terrain-collider.js +175 -0
  16. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  17. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  18. package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
  19. package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
  20. package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
  21. package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
  22. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  23. package/dist/components/viewer/post-processing.js +22 -1
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -0
  27. package/dist/lib/daylight/room-map.d.ts +29 -4
  28. package/dist/lib/daylight/room-map.d.ts.map +1 -1
  29. package/dist/lib/daylight/room-map.js +219 -36
  30. package/dist/lib/daylight/room-scene.d.ts.map +1 -1
  31. package/dist/lib/daylight/room-scene.js +9 -3
  32. package/dist/lib/daylight/rooms.d.ts +15 -6
  33. package/dist/lib/daylight/rooms.d.ts.map +1 -1
  34. package/dist/lib/daylight/rooms.js +49 -9
  35. package/dist/lib/measurement-presentation.d.ts +2 -0
  36. package/dist/lib/measurement-presentation.d.ts.map +1 -1
  37. package/dist/lib/measurement-presentation.js +6 -0
  38. package/dist/lib/typing-target.d.ts +2 -0
  39. package/dist/lib/typing-target.d.ts.map +1 -0
  40. package/dist/lib/typing-target.js +7 -0
  41. package/dist/lib/viewer-config.d.ts +6 -0
  42. package/dist/lib/viewer-config.d.ts.map +1 -1
  43. package/dist/lib/viewer-config.js +2 -0
  44. package/dist/sfx/audio-settings.d.ts +81 -0
  45. package/dist/sfx/audio-settings.d.ts.map +1 -0
  46. package/dist/sfx/audio-settings.js +166 -0
  47. package/dist/sfx/index.d.ts +6 -0
  48. package/dist/sfx/index.d.ts.map +1 -0
  49. package/dist/sfx/index.js +5 -0
  50. package/dist/sfx/interaction-sounds.d.ts +7 -0
  51. package/dist/sfx/interaction-sounds.d.ts.map +1 -0
  52. package/dist/sfx/interaction-sounds.js +39 -0
  53. package/dist/sfx/player.d.ts +31 -0
  54. package/dist/sfx/player.d.ts.map +1 -0
  55. package/dist/sfx/player.js +255 -0
  56. package/dist/sfx/sounds.d.ts +60 -0
  57. package/dist/sfx/sounds.d.ts.map +1 -0
  58. package/dist/sfx/sounds.js +25 -0
  59. package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
  60. package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
  61. package/dist/sfx/use-viewer-ui-sounds.js +42 -0
  62. package/dist/store/use-first-person-hud.d.ts +13 -0
  63. package/dist/store/use-first-person-hud.d.ts.map +1 -0
  64. package/dist/store/use-first-person-hud.js +21 -0
  65. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  66. package/dist/systems/roof/roof-system.js +27 -13
  67. package/dist/ui/components/action-button.d.ts +14 -0
  68. package/dist/ui/components/action-button.d.ts.map +1 -0
  69. package/dist/ui/components/action-button.js +21 -0
  70. package/dist/ui/components/action-menu-placement.d.ts +7 -0
  71. package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
  72. package/dist/ui/components/action-menu-placement.js +10 -0
  73. package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
  74. package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
  75. package/dist/ui/components/floorplan-compass-button.js +22 -0
  76. package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
  77. package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
  78. package/dist/ui/components/follow-pascal-toggle.js +21 -0
  79. package/dist/ui/components/localized-html.d.ts +9 -0
  80. package/dist/ui/components/localized-html.d.ts.map +1 -0
  81. package/dist/ui/components/localized-html.js +16 -0
  82. package/dist/ui/components/render-settings-menu.d.ts +9 -0
  83. package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
  84. package/dist/ui/components/render-settings-menu.js +145 -0
  85. package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
  86. package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
  87. package/dist/ui/components/rooms/room-highlight.js +56 -0
  88. package/dist/ui/components/rooms/room-labels.d.ts +278 -0
  89. package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
  90. package/dist/ui/components/rooms/room-labels.js +1295 -0
  91. package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
  92. package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
  93. package/dist/ui/components/rooms/viewer-rooms.js +123 -0
  94. package/dist/ui/components/scene-loader.d.ts +18 -0
  95. package/dist/ui/components/scene-loader.d.ts.map +1 -0
  96. package/dist/ui/components/scene-loader.js +29 -0
  97. package/dist/ui/components/sky-control.d.ts +37 -0
  98. package/dist/ui/components/sky-control.d.ts.map +1 -0
  99. package/dist/ui/components/sky-control.js +382 -0
  100. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
  101. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
  102. package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
  103. package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
  104. package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
  105. package/dist/ui/components/viewer-controls-bar.js +130 -0
  106. package/dist/ui/components/viewer-hover-label.d.ts +14 -0
  107. package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
  108. package/dist/ui/components/viewer-hover-label.js +28 -0
  109. package/dist/ui/components/viewer-scene-header.d.ts +34 -0
  110. package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
  111. package/dist/ui/components/viewer-scene-header.js +92 -0
  112. package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
  113. package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
  114. package/dist/ui/components/viewer-selection-manager.js +116 -0
  115. package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
  116. package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
  117. package/dist/ui/components/viewer-stage-modes.js +42 -0
  118. package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
  119. package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
  120. package/dist/ui/components/viewer-stage-switcher.js +16 -0
  121. package/dist/ui/components/viewer-stage.d.ts +25 -0
  122. package/dist/ui/components/viewer-stage.d.ts.map +1 -0
  123. package/dist/ui/components/viewer-stage.js +107 -0
  124. package/dist/ui/components/walkthrough-hud.d.ts +29 -0
  125. package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
  126. package/dist/ui/components/walkthrough-hud.js +55 -0
  127. package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
  128. package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
  129. package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
  130. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
  131. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
  132. package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
  133. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
  134. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
  135. package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
  136. package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
  137. package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
  138. package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
  139. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
  140. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
  141. package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
  142. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
  143. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
  144. package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
  145. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
  146. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
  147. package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
  148. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
  149. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
  150. package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
  151. package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
  152. package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
  153. package/dist/ui/floorplan/floorplan-preview.js +700 -0
  154. package/dist/ui/hooks/use-mobile.d.ts +2 -0
  155. package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
  156. package/dist/ui/hooks/use-mobile.js +14 -0
  157. package/dist/ui/index.d.ts +49 -0
  158. package/dist/ui/index.d.ts.map +1 -0
  159. package/dist/ui/index.js +48 -0
  160. package/dist/ui/lib/below-grade.d.ts +38 -0
  161. package/dist/ui/lib/below-grade.d.ts.map +1 -0
  162. package/dist/ui/lib/below-grade.js +102 -0
  163. package/dist/ui/lib/camera-pose.d.ts +19 -0
  164. package/dist/ui/lib/camera-pose.d.ts.map +1 -0
  165. package/dist/ui/lib/camera-pose.js +113 -0
  166. package/dist/ui/lib/cn.d.ts +3 -0
  167. package/dist/ui/lib/cn.d.ts.map +1 -0
  168. package/dist/ui/lib/cn.js +5 -0
  169. package/dist/ui/lib/level-name.d.ts +10 -0
  170. package/dist/ui/lib/level-name.d.ts.map +1 -0
  171. package/dist/ui/lib/level-name.js +33 -0
  172. package/dist/ui/lib/node-display-label.d.ts +8 -0
  173. package/dist/ui/lib/node-display-label.d.ts.map +1 -0
  174. package/dist/ui/lib/node-display-label.js +36 -0
  175. package/dist/ui/lib/room-area.d.ts +3 -0
  176. package/dist/ui/lib/room-area.d.ts.map +1 -0
  177. package/dist/ui/lib/room-area.js +8 -0
  178. package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
  179. package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
  180. package/dist/ui/lib/room-assembly-overlay.js +222 -0
  181. package/dist/ui/lib/room-name-messages.d.ts +28 -0
  182. package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
  183. package/dist/ui/lib/room-name-messages.js +112 -0
  184. package/dist/ui/lib/room-selection.d.ts +58 -0
  185. package/dist/ui/lib/room-selection.d.ts.map +1 -0
  186. package/dist/ui/lib/room-selection.js +297 -0
  187. package/dist/ui/lib/scene-theme-name.d.ts +4 -0
  188. package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
  189. package/dist/ui/lib/scene-theme-name.js +22 -0
  190. package/dist/ui/lib/selection-modifiers.d.ts +20 -0
  191. package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
  192. package/dist/ui/lib/selection-modifiers.js +15 -0
  193. package/dist/ui/lib/viewer-selection.d.ts +132 -0
  194. package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
  195. package/dist/ui/lib/viewer-selection.js +462 -0
  196. package/dist/ui/lib/zone-content.d.ts +6 -0
  197. package/dist/ui/lib/zone-content.d.ts.map +1 -0
  198. package/dist/ui/lib/zone-content.js +96 -0
  199. package/dist/ui/primitives/button.d.ts +11 -0
  200. package/dist/ui/primitives/button.d.ts.map +1 -0
  201. package/dist/ui/primitives/button.js +35 -0
  202. package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
  203. package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
  204. package/dist/ui/primitives/dropdown-menu.js +53 -0
  205. package/dist/ui/primitives/popover.d.ts +8 -0
  206. package/dist/ui/primitives/popover.d.ts.map +1 -0
  207. package/dist/ui/primitives/popover.js +17 -0
  208. package/dist/ui/primitives/switch.d.ts +5 -0
  209. package/dist/ui/primitives/switch.d.ts.map +1 -0
  210. package/dist/ui/primitives/switch.js +8 -0
  211. package/dist/ui/primitives/tooltip.d.ts +8 -0
  212. package/dist/ui/primitives/tooltip.d.ts.map +1 -0
  213. package/dist/ui/primitives/tooltip.js +17 -0
  214. package/dist/ui/store/camera-pose-store.d.ts +17 -0
  215. package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
  216. package/dist/ui/store/camera-pose-store.js +15 -0
  217. package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
  218. package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
  219. package/dist/ui/store/navigation-sync-pose-store.js +16 -0
  220. package/dist/ui/store/use-follow-pascal.d.ts +30 -0
  221. package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
  222. package/dist/ui/store/use-follow-pascal.js +28 -0
  223. package/dist/ui/theme.css +339 -0
  224. 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":"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"}
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 [roof.id, ...(roof.children ?? [])]) {
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 = [roofId, ...(roof.children ?? [])];
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.children?.length ?? 0) > 1,
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
- function shouldIncludeRoofSegmentWallShell(node, parentRoof) {
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.children?.length ?? 0) <= 1;
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.children?.length ?? 0) <= 1;
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"}