@pascal-app/viewer 1.0.6 → 1.0.7

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 (199) hide show
  1. package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
  2. package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
  3. package/dist/components/viewer/daylight-atmosphere.js +315 -0
  4. package/dist/components/viewer/drone-controller.d.ts +15 -0
  5. package/dist/components/viewer/drone-controller.d.ts.map +1 -0
  6. package/dist/components/viewer/drone-controller.js +194 -0
  7. package/dist/components/viewer/glb-interactive.d.ts +4 -18
  8. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  9. package/dist/components/viewer/glb-interactive.js +32 -268
  10. package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
  11. package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
  12. package/dist/components/viewer/glb-scene-motion.js +57 -0
  13. package/dist/components/viewer/glb-scene.d.ts +3 -1
  14. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  15. package/dist/components/viewer/glb-scene.js +289 -49
  16. package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
  17. package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
  18. package/dist/components/viewer/glb-selection-callout.js +54 -0
  19. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  20. package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
  21. package/dist/components/viewer/index.d.ts +5 -0
  22. package/dist/components/viewer/index.d.ts.map +1 -1
  23. package/dist/components/viewer/index.js +30 -6
  24. package/dist/components/viewer/lights.d.ts.map +1 -1
  25. package/dist/components/viewer/lights.js +192 -5
  26. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  27. package/dist/components/viewer/post-processing.js +150 -94
  28. package/dist/components/viewer/scene-environment.js +4 -5
  29. package/dist/components/viewer/selection-callout.d.ts +51 -0
  30. package/dist/components/viewer/selection-callout.d.ts.map +1 -0
  31. package/dist/components/viewer/selection-callout.js +197 -0
  32. package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
  33. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
  34. package/dist/components/viewer/use-daylight-rooms.js +236 -0
  35. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  36. package/dist/components/viewer/viewer-camera.js +12 -1
  37. package/dist/index.d.ts +21 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +23 -7
  40. package/dist/lib/daylight/ground.d.ts +21 -0
  41. package/dist/lib/daylight/ground.d.ts.map +1 -0
  42. package/dist/lib/daylight/ground.js +53 -0
  43. package/dist/lib/daylight/location.d.ts +32 -0
  44. package/dist/lib/daylight/location.d.ts.map +1 -0
  45. package/dist/lib/daylight/location.js +74 -0
  46. package/dist/lib/daylight/look.d.ts +33 -0
  47. package/dist/lib/daylight/look.d.ts.map +1 -0
  48. package/dist/lib/daylight/look.js +13 -0
  49. package/dist/lib/daylight/material-hooks.d.ts +2 -0
  50. package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
  51. package/dist/lib/daylight/material-hooks.js +150 -0
  52. package/dist/lib/daylight/room-context.d.ts +12 -0
  53. package/dist/lib/daylight/room-context.d.ts.map +1 -0
  54. package/dist/lib/daylight/room-context.js +53 -0
  55. package/dist/lib/daylight/room-scene.d.ts +54 -0
  56. package/dist/lib/daylight/room-scene.d.ts.map +1 -0
  57. package/dist/lib/daylight/room-scene.js +356 -0
  58. package/dist/lib/daylight/rooms.d.ts +262 -0
  59. package/dist/lib/daylight/rooms.d.ts.map +1 -0
  60. package/dist/lib/daylight/rooms.js +1223 -0
  61. package/dist/lib/daylight/runtime.d.ts +33 -0
  62. package/dist/lib/daylight/runtime.d.ts.map +1 -0
  63. package/dist/lib/daylight/runtime.js +42 -0
  64. package/dist/lib/daylight/sky.d.ts +26 -0
  65. package/dist/lib/daylight/sky.d.ts.map +1 -0
  66. package/dist/lib/daylight/sky.js +217 -0
  67. package/dist/lib/daylight/solar.d.ts +46 -0
  68. package/dist/lib/daylight/solar.d.ts.map +1 -0
  69. package/dist/lib/daylight/solar.js +156 -0
  70. package/dist/lib/device-tier.d.ts +29 -0
  71. package/dist/lib/device-tier.d.ts.map +1 -0
  72. package/dist/lib/device-tier.js +71 -0
  73. package/dist/lib/dispose-object3d.d.ts.map +1 -1
  74. package/dist/lib/dispose-object3d.js +4 -2
  75. package/dist/lib/drone-flight.d.ts +44 -0
  76. package/dist/lib/drone-flight.d.ts.map +1 -0
  77. package/dist/lib/drone-flight.js +121 -0
  78. package/dist/lib/editor-highlight.d.ts +16 -0
  79. package/dist/lib/editor-highlight.d.ts.map +1 -0
  80. package/dist/lib/editor-highlight.js +33 -0
  81. package/dist/lib/foundation-materials.d.ts.map +1 -1
  82. package/dist/lib/foundation-materials.js +2 -4
  83. package/dist/lib/glb-merge.d.ts +23 -0
  84. package/dist/lib/glb-merge.d.ts.map +1 -0
  85. package/dist/lib/glb-merge.js +184 -0
  86. package/dist/lib/hardware-geometry.d.ts +21 -0
  87. package/dist/lib/hardware-geometry.d.ts.map +1 -0
  88. package/dist/lib/hardware-geometry.js +283 -0
  89. package/dist/lib/layer-pass.d.ts +2 -0
  90. package/dist/lib/layer-pass.d.ts.map +1 -1
  91. package/dist/lib/layer-pass.js +20 -0
  92. package/dist/lib/materials.d.ts.map +1 -1
  93. package/dist/lib/materials.js +50 -23
  94. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  95. package/dist/lib/merged-outline-node.js +7 -1
  96. package/dist/lib/occlusion-culler.d.ts +34 -0
  97. package/dist/lib/occlusion-culler.d.ts.map +1 -0
  98. package/dist/lib/occlusion-culler.js +698 -0
  99. package/dist/lib/post-processing-resources.d.ts +4 -0
  100. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  101. package/dist/lib/post-processing-resources.js +5 -0
  102. package/dist/lib/readback-rows.d.ts +14 -0
  103. package/dist/lib/readback-rows.d.ts.map +1 -0
  104. package/dist/lib/readback-rows.js +31 -0
  105. package/dist/lib/reveal-pose.d.ts.map +1 -1
  106. package/dist/lib/reveal-pose.js +3 -0
  107. package/dist/lib/scene-matrices.d.ts +12 -0
  108. package/dist/lib/scene-matrices.d.ts.map +1 -0
  109. package/dist/lib/scene-matrices.js +287 -0
  110. package/dist/lib/scene-pass-mrt.d.ts +5 -2
  111. package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
  112. package/dist/lib/scene-pass-mrt.js +7 -2
  113. package/dist/lib/scene-themes.d.ts +0 -2
  114. package/dist/lib/scene-themes.d.ts.map +1 -1
  115. package/dist/lib/scene-themes.js +0 -2
  116. package/dist/lib/scene-visibility.d.ts +2 -0
  117. package/dist/lib/scene-visibility.d.ts.map +1 -1
  118. package/dist/lib/scene-visibility.js +12 -3
  119. package/dist/lib/site-surfaces.d.ts +1 -8
  120. package/dist/lib/site-surfaces.d.ts.map +1 -1
  121. package/dist/lib/site-surfaces.js +0 -19
  122. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  123. package/dist/lib/snapshot-pipeline.js +24 -40
  124. package/dist/lib/texture-reference.d.ts.map +1 -1
  125. package/dist/lib/texture-reference.js +10 -8
  126. package/dist/lib/underground-ambiance.d.ts +0 -2
  127. package/dist/lib/underground-ambiance.d.ts.map +1 -1
  128. package/dist/lib/underground-ambiance.js +0 -2
  129. package/dist/store/use-item-light-pool.d.ts +3 -6
  130. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  131. package/dist/store/use-item-light-pool.js +12 -0
  132. package/dist/store/use-viewer.d.ts +72 -15
  133. package/dist/store/use-viewer.d.ts.map +1 -1
  134. package/dist/store/use-viewer.js +103 -18
  135. package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
  136. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  137. package/dist/systems/ceiling/ceiling-system.js +11 -1
  138. package/dist/systems/door/door-system.d.ts.map +1 -1
  139. package/dist/systems/door/door-system.js +501 -145
  140. package/dist/systems/door/leaf-profile.d.ts +11 -0
  141. package/dist/systems/door/leaf-profile.d.ts.map +1 -0
  142. package/dist/systems/door/leaf-profile.js +140 -0
  143. package/dist/systems/interactive/callout-actions.d.ts +72 -0
  144. package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
  145. package/dist/systems/interactive/callout-actions.js +212 -0
  146. package/dist/systems/interactive/interactive-system.d.ts +5 -1
  147. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  148. package/dist/systems/interactive/interactive-system.js +44 -121
  149. package/dist/systems/interactive/opening-settled.d.ts +16 -0
  150. package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
  151. package/dist/systems/interactive/opening-settled.js +53 -0
  152. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  153. package/dist/systems/item-light/item-light-system.js +22 -214
  154. package/dist/systems/item-light/pooled-lights.d.ts +70 -0
  155. package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
  156. package/dist/systems/item-light/pooled-lights.js +449 -0
  157. package/dist/systems/level/level-system.d.ts.map +1 -1
  158. package/dist/systems/level/level-system.js +19 -1
  159. package/dist/systems/roof/roof-material-slots.d.ts +14 -0
  160. package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
  161. package/dist/systems/roof/roof-material-slots.js +13 -0
  162. package/dist/systems/roof/roof-materials.d.ts +25 -2
  163. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  164. package/dist/systems/roof/roof-materials.js +85 -26
  165. package/dist/systems/roof/roof-system.d.ts +34 -1
  166. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  167. package/dist/systems/roof/roof-system.js +477 -105
  168. package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
  169. package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
  170. package/dist/systems/wall/opening-casing-geometry.js +102 -0
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
  173. package/dist/systems/wall/opening-cutout-geometry.js +7 -3
  174. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  175. package/dist/systems/wall/wall-materials.js +28 -35
  176. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  177. package/dist/systems/wall/wall-system.js +34 -19
  178. package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
  179. package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
  180. package/dist/systems/wall/wall-t-junctions.js +149 -0
  181. package/dist/systems/window/window-animation-system.d.ts +3 -12
  182. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  183. package/dist/systems/window/window-animation-system.js +37 -122
  184. package/dist/systems/window/window-pose.d.ts +31 -0
  185. package/dist/systems/window/window-pose.d.ts.map +1 -0
  186. package/dist/systems/window/window-pose.js +152 -0
  187. package/dist/systems/window/window-roller-shutter.d.ts +8 -0
  188. package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
  189. package/dist/systems/window/window-roller-shutter.js +64 -0
  190. package/dist/systems/window/window-system.d.ts +2 -11
  191. package/dist/systems/window/window-system.d.ts.map +1 -1
  192. package/dist/systems/window/window-system.js +470 -303
  193. package/package.json +6 -6
  194. package/dist/systems/interactive/control-widget.d.ts +0 -10
  195. package/dist/systems/interactive/control-widget.d.ts.map +0 -1
  196. package/dist/systems/interactive/control-widget.js +0 -35
  197. package/dist/systems/interactive/procedural-controls.d.ts +0 -13
  198. package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
  199. package/dist/systems/interactive/procedural-controls.js +0 -17
@@ -4,6 +4,7 @@ import { nodeRegistry, RoofElevationSystem, StairOpeningSystem, sceneRegistry, u
4
4
  import { Canvas, extend, useFrame, useThree } from '@react-three/fiber';
5
5
  import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
6
6
  import * as THREE from 'three/webgpu';
7
+ import { classifyDevice, isSoftwareRenderer, readDeviceSignals } from '../../lib/device-tier';
7
8
  import { hasDrawableGeometry } from '../../lib/drawable-geometry';
8
9
  import { PERF_OVERLAY_ENABLED } from '../../lib/gpu-perf';
9
10
  import { applyIsolation, clearIsolation } from '../../lib/isolation';
@@ -21,6 +22,7 @@ import { subscribeWallBuildInteractions } from '../../systems/wall/wall-build-li
21
22
  import { ImmersiveXRPresentationProvider } from '../../xr/presentation-context';
22
23
  import { SceneRenderer } from '../renderers/scene-renderer';
23
24
  import { BATCH_SPIKE_ENABLED, BatchedMeshSpike } from './batched-mesh-spike';
25
+ import { DaylightAtmosphere } from './daylight-atmosphere';
24
26
  import FrameLimiter from './frame-limiter';
25
27
  import { Lights } from './lights';
26
28
  import { PerfMonitor } from './perf-monitor';
@@ -283,7 +285,13 @@ function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, sceneReadyMaxWai
283
285
  }, 10);
284
286
  return null;
285
287
  }
286
- const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, renderContext = 'editor', transparent, defaultRender, presentation: suppliedPresentation, isolate, sceneReadyKey, onSceneReadyChange, onRenderError, onBreadcrumb, sceneReadyMaxWaitMs, maxFps = 50, disablePostFx = false, renderPaused = false, immersive, }, ref) {
288
+ const MAX_DPR = { high: 1.5, medium: 1.25, low: 1 };
289
+ /**
290
+ * A software renderer can't reach the usual cadence, and chasing it starves the app of the CPU it
291
+ * rasterizes on: frames are capped lower there.
292
+ */
293
+ const SOFTWARE_MAX_FPS = 20;
294
+ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, renderContext = 'editor', transparent, defaultRender, presentation: suppliedPresentation, daylight = true, isolate, sceneReadyKey, onSceneReadyChange, onRenderError, onBreadcrumb, sceneReadyMaxWaitMs, maxFps = 50, disablePostFx = false, renderPaused = false, immersive, }, ref) {
287
295
  useEffect(() => {
288
296
  if (nodeRegistry.size === 0) {
289
297
  console.warn('[viewer] Node registry is empty. Install @pascal-app/nodes and call await loadPlugin(builtinPlugin) before mounting <Viewer>.');
@@ -313,12 +321,18 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
313
321
  const isDark = useViewer((state) => state.sceneAppearance.appearance === 'dark');
314
322
  const sceneTheme = useViewer((state) => state.sceneTheme);
315
323
  const previousLampTheme = useRef(sceneTheme);
324
+ // Scene themes drive the lamps only in monochrome; the colored appearance's daylight follows
325
+ // its time of day instead.
326
+ const colored = useViewer((state) => state.textures);
327
+ const themeDrivesLamps = !(daylight && colored);
316
328
  useEffect(() => {
329
+ if (!themeDrivesLamps)
330
+ return;
317
331
  useInteractive
318
332
  .getState()
319
333
  .setLampDefault(getSceneTheme(sceneTheme).appearance === 'dark', previousLampTheme.current !== sceneTheme);
320
334
  previousLampTheme.current = sceneTheme;
321
- }, [sceneTheme]);
335
+ }, [sceneTheme, themeDrivesLamps]);
322
336
  const transparentBackground = useViewer((state) => state.transparentBackground);
323
337
  // The shadows toggle drives `renderer.shadowMap.enabled` (via the Canvas
324
338
  // `shadows` prop) rather than the lights' `castShadow`: toggling castShadow
@@ -384,7 +398,10 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
384
398
  // Coarse-pointer devices (phones/tablets) get a tighter DPR ceiling to keep
385
399
  // fragment-shader cost down — saves another ~30% over 1.5x on high-DPI mobile.
386
400
  // Desktops (fine pointer) keep the original 1.5 cap.
387
- const maxDpr = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches ? 1.25 : 1.5;
401
+ // Pixel-ratio cap per quality tier (touch devices default to Medium).
402
+ const renderQuality = useViewer((state) => state.renderQuality);
403
+ const softwareRenderer = useViewer((state) => state.softwareRenderer);
404
+ const maxDpr = MAX_DPR[renderQuality];
388
405
  const showGpuFallback = rendererInitFailed;
389
406
  // When we can't mount the GPU canvas, the SceneReadyTracker never mounts and
390
407
  // the host editor would otherwise wait on its scene-readiness timeout. Signal
@@ -434,6 +451,11 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
434
451
  });
435
452
  if (result.status === 'ready') {
436
453
  installEmptyDrawGuard(result.renderer);
454
+ // Before the first frame, so the post-processing builds once at the device's tier.
455
+ const signals = readDeviceSignals(result.backend, result.renderer.backend?.device);
456
+ useViewer
457
+ .getState()
458
+ .setDetectedRenderQuality(classifyDevice(signals), isSoftwareRenderer(signals));
437
459
  return result.renderer;
438
460
  }
439
461
  if (canvas)
@@ -456,11 +478,13 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
456
478
  }, shadows: {
457
479
  type: THREE.PCFShadowMap,
458
480
  enabled: shadowsEnabled,
459
- }, children: _jsx(ImmersiveXRPresentationProvider, { enabled: immersiveActive, children: ImmersiveSession ? (_jsx(ImmersiveSession, { children: _jsx(ViewerScene, { disablePostFx: true, hoverStyles: hoverStyles, immersiveXR: true, renderErrorHandlers: [immersive.onError, onRenderError], onSceneReadyChange: onSceneReadyChange, perf: perf, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs, selectionManager: selectionManager, useBvh: useBvh, SceneWrapper: immersive.Scene, children: children }) })) : (_jsxs(_Fragment, { children: [_jsx(FrameLimiter, { fps: maxFps, onFrameError: composeRenderErrorHandlers(onRenderError), paused: renderPaused }), _jsx(ViewerScene, { disablePostFx: disablePostFx, hoverStyles: hoverStyles, renderErrorHandlers: [onRenderError], onSceneReadyChange: onSceneReadyChange, perf: perf, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs, selectionManager: selectionManager, useBvh: useBvh, children: children })] })) }) }, immersiveActive ? 'webgl-xr' : 'webgpu')] }));
481
+ }, children: _jsx(ImmersiveXRPresentationProvider, { enabled: immersiveActive, children: ImmersiveSession ? (_jsx(ImmersiveSession, { children: _jsx(ViewerScene, { daylight: daylight, disablePostFx: true, hoverStyles: hoverStyles, immersiveXR: true, renderErrorHandlers: [immersive.onError, onRenderError], onSceneReadyChange: onSceneReadyChange, perf: perf, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs, selectionManager: selectionManager, useBvh: useBvh, SceneWrapper: immersive.Scene, children: children }) })) : (_jsxs(_Fragment, { children: [_jsx(FrameLimiter, { fps: softwareRenderer ? Math.min(maxFps, SOFTWARE_MAX_FPS) : maxFps, onFrameError: composeRenderErrorHandlers(onRenderError), paused: renderPaused }), _jsx(ViewerScene, { daylight: daylight, disablePostFx: disablePostFx, hoverStyles: hoverStyles, renderErrorHandlers: [onRenderError], onSceneReadyChange: onSceneReadyChange, perf: perf, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs, selectionManager: selectionManager, useBvh: useBvh, children: children })] })) }) }, immersiveActive ? 'webgl-xr' : 'webgpu')] }));
460
482
  });
461
- function ViewerScene({ children, disablePostFx, hoverStyles, immersiveXR = false, renderErrorHandlers, onSceneReadyChange, perf, sceneReadyKey, sceneReadyMaxWaitMs, selectionManager, useBvh, SceneWrapper, }) {
483
+ function ViewerScene({ children, daylight, disablePostFx, hoverStyles, immersiveXR = false, renderErrorHandlers, onSceneReadyChange, perf, sceneReadyKey, sceneReadyMaxWaitMs, selectionManager, useBvh, SceneWrapper, }) {
484
+ // Colored appearance = the built-in daylight look; monochrome keeps the scene themes.
485
+ const colored = useViewer((state) => state.textures);
462
486
  const renderedScene = useBvh ? (_jsx(SceneBvh, { children: _jsx(SceneRenderer, {}) })) : (_jsx(SceneRenderer, {}));
463
487
  const spatialScene = (_jsxs(_Fragment, { children: [renderedScene, _jsx(FloorElevationSystem, {}), _jsx(GeometrySystem, {}), _jsx(StairOpeningSystem, {}), _jsx(RoofElevationSystem, {}), _jsx(RegisteredSystems, {}), BATCH_SPIKE_ENABLED && _jsx(BatchedMeshSpike, {}), children] }));
464
- return (_jsxs(_Fragment, { children: [_jsx(ViewerCamera, { immersiveXR: immersiveXR }), immersiveXR && _jsx(ImmersiveXRBackground, {}), _jsx(PointerRaycastLayers, {}), _jsx(GPUDeviceWatcher, { intentionalWebGL: immersiveXR }), _jsx(ToneMappingExposure, {}), _jsx(SceneReadyTracker, { onSceneReadyChange: onSceneReadyChange, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs }), _jsxs(SceneErrorBoundary, { handlers: renderErrorHandlers, children: [_jsx(Lights, {}), SceneWrapper ? _jsx(SceneWrapper, { children: spatialScene }) : spatialScene, !immersiveXR && _jsx(PostProcessing, { disablePostFx: disablePostFx, hoverStyles: hoverStyles }), selectionManager === 'default' && _jsx(SelectionManager, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfMonitor, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfActionSettleSystem, {})] })] }));
488
+ return (_jsxs(_Fragment, { children: [_jsx(ViewerCamera, { immersiveXR: immersiveXR }), immersiveXR && _jsx(ImmersiveXRBackground, {}), _jsx(PointerRaycastLayers, {}), _jsx(GPUDeviceWatcher, { intentionalWebGL: immersiveXR }), _jsx(ToneMappingExposure, {}), _jsx(SceneReadyTracker, { onSceneReadyChange: onSceneReadyChange, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs }), _jsxs(SceneErrorBoundary, { handlers: renderErrorHandlers, children: [daylight && colored && _jsx(DaylightAtmosphere, {}), _jsx(Lights, {}), SceneWrapper ? _jsx(SceneWrapper, { children: spatialScene }) : spatialScene, !immersiveXR && _jsx(PostProcessing, { disablePostFx: disablePostFx, hoverStyles: hoverStyles }), selectionManager === 'default' && _jsx(SelectionManager, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfMonitor, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfActionSettleSystem, {})] })] }));
465
489
  }
466
490
  export default Viewer;
@@ -1 +1 @@
1
- {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AAmGA,wBAAgB,MAAM,gCAiVrB"}
1
+ {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AAoGA,wBAAgB,MAAM,gCA+XrB"}
@@ -3,6 +3,7 @@ import { sceneRegistry } from '@pascal-app/core';
3
3
  import { useFrame } from '@react-three/fiber';
4
4
  import { useEffect, useMemo, useRef } from 'react';
5
5
  import * as THREE from 'three/webgpu';
6
+ import { isDaylightAtmosphere } from '../../lib/daylight/look';
6
7
  import { getKeyLightDirectionOverride } from '../../lib/key-light-override';
7
8
  import { SHADOW_ONLY_LAYER } from '../../lib/layers';
8
9
  import { registerSkyLights } from '../../lib/underground-ambiance';
@@ -97,7 +98,10 @@ export function Lights() {
97
98
  fillMounted.current = true;
98
99
  const filled = fillMounted.current;
99
100
  const atmosphere = useSceneAtmosphere();
100
- const lightSlots = useMemo(() => Array.from({ length: atmosphere ? 2 : theme.lights.length }, (_, index) => index), [atmosphere, theme.lights.length]);
101
+ // The daylight look lights with one key (sun, or moon at night) and the sky environment only:
102
+ // no moon slot, hemisphere or ambient light (each light costs every material's shader).
103
+ const daylight = isDaylightAtmosphere(atmosphere);
104
+ const lightSlots = useMemo(() => Array.from({ length: daylight ? 1 : atmosphere ? 2 : theme.lights.length }, (_, index) => index), [atmosphere, daylight, theme.lights.length]);
101
105
  const lightRefs = useRef([]);
102
106
  const shadowCamera = useRef(null);
103
107
  // Initial ortho half-size; overridden each refresh to fit the building.
@@ -109,6 +113,10 @@ export function Lights() {
109
113
  const boundsBox = useRef(new THREE.Box3()); // scratch: union AABB
110
114
  const boundsSphere = useRef(new THREE.Sphere()); // scratch: fitted sphere
111
115
  const lastBoundsTime = useRef(-1); // last refresh timestamp (-1 = never)
116
+ // Daylight sun shadows render on change only (see `shadowSignature`).
117
+ const shadowCasters = useRef([]);
118
+ const shadowCasterTime = useRef(-1);
119
+ const lastShadowSignature = useRef(Number.NaN);
112
120
  const shadowHelpers = useRef([]);
113
121
  const hemiRef = useRef(null);
114
122
  const ambientRef = useRef(null);
@@ -135,6 +143,9 @@ export function Lights() {
135
143
  const dt = Math.min(delta, 0.1) * 4;
136
144
  const sourceShadow = atmosphere ? undefined : theme.lights[0]?.shadow;
137
145
  const sourceCamera = sourceShadow?.camera;
146
+ // The daylight look: one key (sun or moon) with its own shadow settings, frustum fitted
147
+ // tightly around the building in light space.
148
+ const daylightShadow = isDaylightAtmosphere(atmosphere) ? atmosphere.shadow : undefined;
138
149
  // Atmosphere directions are stable mutable vectors. Keep both light
139
150
  // resources mounted and move them imperatively so time/angle changes never
140
151
  // rebuild materials or shadow resources.
@@ -168,11 +179,24 @@ export function Lights() {
168
179
  if (!sourceCamera && now - lastBoundsTime.current >= BOUNDS_REFRESH_INTERVAL) {
169
180
  lastBoundsTime.current = now;
170
181
  const box = boundsBox.current.makeEmpty();
182
+ const counted = new Set();
171
183
  for (const [id, obj] of sceneRegistry.nodes) {
172
184
  if (SHADOW_EXCLUDED_TYPES.some((type) => sceneRegistry.byType[type].has(id)))
173
185
  continue;
174
- box.expandByObject(obj);
186
+ counted.add(obj);
175
187
  }
188
+ // A counted node's subtree already holds the nodes below it: walk each mesh once, not
189
+ // once per counted ancestor (building, level, wall, item…).
190
+ for (const obj of counted) {
191
+ let nested = false;
192
+ for (let p = obj.parent; p && !nested; p = p.parent)
193
+ nested = counted.has(p);
194
+ if (!nested)
195
+ expandByWorldBounds(box, obj);
196
+ }
197
+ // Baked artifacts stay out of the registry: frame their shadow casters instead.
198
+ if (box.isEmpty() && daylightShadow)
199
+ expandByShadowCasters(state.scene, box);
176
200
  box.getBoundingSphere(boundsSphere.current);
177
201
  const center = boundsSphere.current.center;
178
202
  const radius = boundsSphere.current.radius;
@@ -235,7 +259,29 @@ export function Lights() {
235
259
  light.position.set(tx, ty, tz).add(shadowDir.current);
236
260
  }
237
261
  const cam = light.shadow?.camera;
238
- if (cam) {
262
+ if (cam && daylightShadow && !sourceCamera) {
263
+ light.shadow.mapSize.setScalar(daylightShadow.mapSize);
264
+ light.shadow.bias = daylightShadow.bias;
265
+ light.shadow.normalBias = daylightShadow.normalBias;
266
+ light.shadow.radius = daylightShadow.radius;
267
+ cam.layers.enable(SHADOW_ONLY_LAYER);
268
+ fitShadowToBounds(light, boundsBox.current, shadowDir.current);
269
+ // The sun's map re-renders only when something it depends on changes: its placement,
270
+ // frustum or settings, or a caster that moved, appeared, vanished or changed shape.
271
+ // A walkthrough or an orbit moves only the eye, so the map is reused frame to frame.
272
+ light.shadow.autoUpdate = false;
273
+ if (now - shadowCasterTime.current >= BOUNDS_REFRESH_INTERVAL) {
274
+ shadowCasterTime.current = now;
275
+ collectShadowCasters(state.scene, shadowCasters.current);
276
+ }
277
+ const signature = shadowSignature(light, cam, shadowCasters.current);
278
+ if (signature !== lastShadowSignature.current) {
279
+ lastShadowSignature.current = signature;
280
+ light.shadow.needsUpdate = true;
281
+ }
282
+ }
283
+ else if (cam) {
284
+ light.shadow.autoUpdate = true;
239
285
  // The calibrated building bias detaches furniture-scale contact shadows.
240
286
  // Small scenes need finer shadow edges too; retain the building map budget.
241
287
  const smallScene = !sourceCamera && shadowRadius.current < 1.5;
@@ -300,7 +346,9 @@ export function Lights() {
300
346
  light.color.lerp(target, dt);
301
347
  }
302
348
  if (index === 0 && light.shadow?.intensity !== undefined) {
303
- const shadowIntensity = sourceShadow?.intensity ?? (intensity <= 1 ? intensity : MAX_SHADOW_INTENSITY);
349
+ const shadowIntensity = daylightShadow
350
+ ? 1
351
+ : (sourceShadow?.intensity ?? (intensity <= 1 ? intensity : MAX_SHADOW_INTENSITY));
304
352
  light.shadow.intensity =
305
353
  atmosphere || !initialized.current
306
354
  ? shadowIntensity
@@ -353,5 +401,144 @@ export function Lights() {
353
401
  : (themeLight?.position ?? [0, 1, 0]), ref: (light) => {
354
402
  lightRefs.current[index] = light;
355
403
  }, "shadow-bias": SHADOW_DEPTH_BIAS, "shadow-mapSize": [1024, 1024], "shadow-normalBias": SHADOW_NORMAL_BIAS, "shadow-radius": 2, children: index === 0 && keyCastsShadow ? (_jsx("orthographicCamera", { attach: "shadow-camera", bottom: -shadowCameraSize, far: 400, left: -shadowCameraSize, near: 1, ref: shadowCamera, right: shadowCameraSize, top: shadowCameraSize })) : null }, index));
356
- }), atmosphere || theme.hemi ? _jsx("hemisphereLight", { ref: hemiRef }) : null, _jsx("ambientLight", { ref: ambientRef }), filled && _jsx(UndergroundFill, { lightRef: fillRef }), _jsx(UndergroundAmbiance, {})] }));
404
+ }), !daylight && (atmosphere || theme.hemi) ? _jsx("hemisphereLight", { ref: hemiRef }) : null, daylight ? null : _jsx("ambientLight", { ref: ambientRef }), filled && _jsx(UndergroundFill, { lightRef: fillRef }), _jsx(UndergroundAmbiance, {})] }));
405
+ }
406
+ const _fitCenter = new THREE.Vector3();
407
+ const _fitSize = new THREE.Vector3();
408
+ const _fitCorner = new THREE.Vector3();
409
+ const _fitBox = new THREE.Box3();
410
+ const _fitBounds = new THREE.Box3();
411
+ const _fitView = new THREE.Matrix4();
412
+ const _fitUp = new THREE.Vector3(0, 1, 0);
413
+ const _fitMargin = new THREE.Vector3(2, 0, 2);
414
+ /**
415
+ * Daylight key: place the light on its direction around the building and fit the ortho shadow
416
+ * camera to the building box (+2 m of ground) projected into light space — the tightest frustum,
417
+ * so the map's texels all land on the building. Its depth reaches on to where the casters' shadows
418
+ * meet the ground: a receiver beyond the far plane reads as fully lit, which cut long shadows off
419
+ * square. A shadow keeps its caster's light-space x/y, so only the depth grows, not the texels.
420
+ */
421
+ function fitShadowToBounds(light, bounds, direction) {
422
+ if (bounds.isEmpty())
423
+ return;
424
+ const center = bounds.getCenter(_fitCenter);
425
+ const radius = bounds.getSize(_fitSize).length() / 2 + 2;
426
+ const toLight = _fitSize.copy(direction).normalize();
427
+ light.position.copy(center).addScaledVector(toLight, radius * 2);
428
+ light.target.position.copy(center);
429
+ light.target.updateMatrixWorld();
430
+ light.updateMatrixWorld();
431
+ _fitView.lookAt(light.position, center, _fitUp).invert();
432
+ _fitView.setPosition(0, 0, 0);
433
+ const b = _fitBounds.copy(bounds).expandByVector(_fitMargin);
434
+ const box = _fitBox.makeEmpty();
435
+ // The ground under the casters (the site ground sits just below 0).
436
+ const groundY = Math.min(bounds.min.y, -0.07);
437
+ for (let i = 0; i < 8; i++) {
438
+ const y = i & 2 ? b.max.y : b.min.y;
439
+ _fitCorner
440
+ .set(i & 1 ? b.max.x : b.min.x, y, i & 4 ? b.max.z : b.min.z)
441
+ .sub(light.position)
442
+ .applyMatrix4(_fitView);
443
+ box.expandByPoint(_fitCorner);
444
+ // Down the light ray to the ground: same x/y in light space, only deeper.
445
+ if (toLight.y > 0)
446
+ box.min.z = Math.min(box.min.z, _fitCorner.z - (y - groundY) / toLight.y);
447
+ }
448
+ const cam = light.shadow.camera;
449
+ cam.left = box.min.x - 0.5;
450
+ cam.right = box.max.x + 0.5;
451
+ cam.bottom = box.min.y - 0.5;
452
+ cam.top = box.max.y + 0.5;
453
+ cam.near = Math.max(0.1, -box.max.z - 1);
454
+ cam.far = -box.min.z + 1;
455
+ cam.updateProjectionMatrix();
456
+ }
457
+ /** Every shadow-casting mesh under `root` (visible or not: a level shown again must count). */
458
+ function collectShadowCasters(root, out) {
459
+ out.length = 0;
460
+ root.traverse((object) => {
461
+ const mesh = object;
462
+ if (mesh.isMesh && mesh.castShadow)
463
+ out.push(mesh);
464
+ });
465
+ }
466
+ /**
467
+ * A number that changes whenever the sun's shadow map would: the light's world placement and
468
+ * projection, its map settings, and each caster's world transform, shown state (through its
469
+ * ancestors) and geometry. Cheap enough per frame: a few reads per caster, no allocation.
470
+ */
471
+ function shadowSignature(light, cam, casters) {
472
+ // A weighted sum, not a multiply-and-wrap hash: it stays exact to ~1e-7 at these magnitudes,
473
+ // so a door turning by a fraction of a degree still changes it.
474
+ let h = light.shadow.mapSize.x + light.shadow.bias * 1e4 + light.shadow.normalBias * 1e3;
475
+ h += light.shadow.radius * 7;
476
+ const lm = light.matrixWorld.elements;
477
+ const pm = cam.projectionMatrix.elements;
478
+ for (let i = 0; i < 16; i++)
479
+ h += (i + 1) * (lm[i] * 1.013 + pm[i] * 0.917);
480
+ for (let i = 0; i < casters.length; i++) {
481
+ const mesh = casters[i];
482
+ let shown = mesh.castShadow;
483
+ for (let x = mesh; x && shown; x = x.parent)
484
+ shown = x.visible;
485
+ if (!shown)
486
+ continue;
487
+ const m = mesh.matrixWorld.elements;
488
+ const w = (i % 89) + 1;
489
+ h +=
490
+ w *
491
+ (m[0] +
492
+ m[1] * 2 +
493
+ m[2] * 3 +
494
+ m[5] * 5 +
495
+ m[10] * 7 +
496
+ m[12] * 11 +
497
+ m[13] * 13 +
498
+ m[14] * 17 +
499
+ mesh.geometry.id * 0.019 +
500
+ 0.5);
501
+ }
502
+ return h;
503
+ }
504
+ const _objectBox = new THREE.Box3();
505
+ /**
506
+ * `Box3.expandByObject` without its `updateWorldMatrix` on every object visited, which recomposed
507
+ * ~10k matrices per refresh: the frame's render already brought world matrices up to date
508
+ * (scene-matrices.ts), and bounds a frame old are fine for fitting the shadow frustum.
509
+ */
510
+ function expandByWorldBounds(box, root) {
511
+ root.traverse((object) => {
512
+ const geometry = object.geometry;
513
+ if (geometry === undefined)
514
+ return;
515
+ const owned = object;
516
+ if (owned.boundingBox !== undefined) {
517
+ if (owned.boundingBox === null)
518
+ owned.computeBoundingBox?.();
519
+ if (!owned.boundingBox)
520
+ return;
521
+ _objectBox.copy(owned.boundingBox);
522
+ }
523
+ else {
524
+ if (geometry.boundingBox === null)
525
+ geometry.computeBoundingBox();
526
+ _objectBox.copy(geometry.boundingBox);
527
+ }
528
+ _objectBox.applyMatrix4(object.matrixWorld);
529
+ box.union(_objectBox);
530
+ });
531
+ }
532
+ /** Shadow-casting meshes of a scene (skipping the effectively infinite grounds). */
533
+ function expandByShadowCasters(scene, box) {
534
+ const mesh = new THREE.Box3();
535
+ scene.traverseVisible((object) => {
536
+ const candidate = object;
537
+ if (!candidate.isMesh || !candidate.castShadow)
538
+ return;
539
+ mesh.setFromObject(candidate);
540
+ if (mesh.isEmpty() || mesh.max.x - mesh.min.x > 500 || mesh.max.z - mesh.min.z > 500)
541
+ return;
542
+ box.union(mesh);
543
+ });
357
544
  }
@@ -1 +1 @@
1
- {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA0DA,eAAO,MAAM,YAAY;IACvB,QAAQ;IACR,UAAU;CACX,CAAA;AAGD,eAAO,MAAM,WAAW;IACtB,OAAO;IACP,UAAU;IACV,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;IACT,gBAAgB;IAChB,WAAW;IACX,WAAW;IACX,kBAAkB;IAClB,sBAAsB;IACtB,oBAAoB;CACrB,CAAA;AAgED,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,UAAU,CAAA;CACpB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;AAS9B,eAAO,MAAM,oBAAoB,EAAE,WAElC,CAAA;AA+CD,QAAA,MAAM,oBAAoB,oCAGvB;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,SAwsBA,CAAA;eAEc,oBAAoB"}
1
+ {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA+FA,eAAO,MAAM,YAAY;IACvB,QAAQ;IACR,UAAU;CACX,CAAA;AAGD,eAAO,MAAM,WAAW;IACtB,OAAO;IACP,UAAU;IACV,SAAS;IACT,MAAM;IACN,SAAS;IACT,SAAS;IACT,gBAAgB;IAChB,WAAW;IACX,WAAW;IACX,kBAAkB;IAClB,sBAAsB;IACtB,oBAAoB;CACrB,CAAA;AAgED,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,UAAU,CAAA;CACpB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;AAS9B,eAAO,MAAM,oBAAoB,EAAE,WAElC,CAAA;AA+CD,QAAA,MAAM,oBAAoB,oCAGvB;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,SAkvBA,CAAA;eAEc,oBAAoB"}