@pascal-app/viewer 1.0.1 → 1.0.4

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 (173) hide show
  1. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/node-renderer.js +8 -3
  3. package/dist/components/viewer/frame-limiter.d.ts +1 -0
  4. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  5. package/dist/components/viewer/frame-limiter.js +22 -3
  6. package/dist/components/viewer/glb-interactive.d.ts +25 -1
  7. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  8. package/dist/components/viewer/glb-interactive.js +252 -85
  9. package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
  10. package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
  11. package/dist/components/viewer/glb-mechanisms.js +19 -0
  12. package/dist/components/viewer/glb-scene.d.ts +45 -27
  13. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  14. package/dist/components/viewer/glb-scene.js +420 -506
  15. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  16. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  17. package/dist/components/viewer/glb-walkthrough-controller.js +12 -1
  18. package/dist/components/viewer/index.d.ts +26 -0
  19. package/dist/components/viewer/index.d.ts.map +1 -1
  20. package/dist/components/viewer/index.js +108 -29
  21. package/dist/components/viewer/lights.d.ts.map +1 -1
  22. package/dist/components/viewer/lights.js +6 -1
  23. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  24. package/dist/components/viewer/perf-monitor.js +6 -0
  25. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  26. package/dist/components/viewer/post-processing.js +8 -3
  27. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  28. package/dist/components/viewer/registered-systems.js +16 -2
  29. package/dist/components/viewer/render-error.d.ts +18 -0
  30. package/dist/components/viewer/render-error.d.ts.map +1 -0
  31. package/dist/components/viewer/render-error.js +29 -0
  32. package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
  33. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
  34. package/dist/components/viewer/scene-ground-replacement.js +15 -0
  35. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  36. package/dist/components/viewer/selection-manager.js +10 -0
  37. package/dist/components/viewer/viewer-camera.d.ts +19 -1
  38. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  39. package/dist/components/viewer/viewer-camera.js +53 -5
  40. package/dist/index.d.ts +28 -15
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +26 -18
  43. package/dist/lib/asset-url.d.ts.map +1 -1
  44. package/dist/lib/asset-url.js +7 -1
  45. package/dist/lib/csg-utils.d.ts +1 -1
  46. package/dist/lib/csg-utils.d.ts.map +1 -1
  47. package/dist/lib/csg-utils.js +1 -1
  48. package/dist/lib/display-state.d.ts +25 -0
  49. package/dist/lib/display-state.d.ts.map +1 -0
  50. package/dist/lib/display-state.js +1 -0
  51. package/dist/lib/hero-pose.d.ts +12 -2
  52. package/dist/lib/hero-pose.d.ts.map +1 -1
  53. package/dist/lib/hero-pose.js +72 -5
  54. package/dist/lib/ink-edges.d.ts +4 -2
  55. package/dist/lib/ink-edges.d.ts.map +1 -1
  56. package/dist/lib/ink-edges.js +17 -7
  57. package/dist/lib/key-light-override.d.ts +11 -0
  58. package/dist/lib/key-light-override.d.ts.map +1 -0
  59. package/dist/lib/key-light-override.js +14 -0
  60. package/dist/lib/live-frame-hold.d.ts +14 -0
  61. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  62. package/dist/lib/live-frame-hold.js +28 -0
  63. package/dist/lib/materials.d.ts +14 -7
  64. package/dist/lib/materials.d.ts.map +1 -1
  65. package/dist/lib/materials.js +31 -5
  66. package/dist/lib/node-top-surface-height.d.ts +6 -0
  67. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  68. package/dist/lib/node-top-surface-height.js +74 -0
  69. package/dist/lib/perf-probe-state.d.ts +39 -0
  70. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  71. package/dist/lib/perf-probe-state.js +17 -0
  72. package/dist/lib/pointer-events.d.ts +8 -0
  73. package/dist/lib/pointer-events.d.ts.map +1 -1
  74. package/dist/lib/pointer-events.js +27 -4
  75. package/dist/lib/procedural-emission.d.ts +7 -0
  76. package/dist/lib/procedural-emission.d.ts.map +1 -0
  77. package/dist/lib/procedural-emission.js +54 -0
  78. package/dist/lib/renderer-capability.d.ts +2 -1
  79. package/dist/lib/renderer-capability.d.ts.map +1 -1
  80. package/dist/lib/renderer-capability.js +2 -2
  81. package/dist/lib/scene-support-height.d.ts +4 -0
  82. package/dist/lib/scene-support-height.d.ts.map +1 -0
  83. package/dist/lib/scene-support-height.js +34 -0
  84. package/dist/lib/scene-visibility.d.ts +6 -0
  85. package/dist/lib/scene-visibility.d.ts.map +1 -1
  86. package/dist/lib/scene-visibility.js +15 -3
  87. package/dist/lib/scripted-opening.d.ts +11 -0
  88. package/dist/lib/scripted-opening.d.ts.map +1 -0
  89. package/dist/lib/scripted-opening.js +20 -0
  90. package/dist/lib/snapshot-pipeline.d.ts +32 -2
  91. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  92. package/dist/lib/snapshot-pipeline.js +215 -105
  93. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  94. package/dist/lib/terrain-perimeter-fill.js +22 -14
  95. package/dist/lib/zone-geometry.d.ts +11 -0
  96. package/dist/lib/zone-geometry.d.ts.map +1 -0
  97. package/dist/lib/zone-geometry.js +46 -0
  98. package/dist/store/use-item-light-pool.d.ts +14 -10
  99. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  100. package/dist/store/use-item-light-pool.js +52 -26
  101. package/dist/store/use-viewer.d.ts +11 -0
  102. package/dist/store/use-viewer.d.ts.map +1 -1
  103. package/dist/store/use-viewer.js +13 -6
  104. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  105. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  106. package/dist/systems/ceiling/ceiling-system.js +86 -6
  107. package/dist/systems/door/door-system.d.ts.map +1 -1
  108. package/dist/systems/door/door-system.js +73 -22
  109. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  110. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  111. package/dist/systems/floor-elevation/floor-elevation-system.js +148 -5
  112. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  113. package/dist/systems/geometry/geometry-system.js +71 -11
  114. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  115. package/dist/systems/interactive/interactive-system.js +40 -15
  116. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  117. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  118. package/dist/systems/interactive/procedural-controls.js +17 -0
  119. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  120. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  121. package/dist/systems/interactive/scripted-clips.js +134 -0
  122. package/dist/systems/item/item-system.d.ts.map +1 -1
  123. package/dist/systems/item/item-system.js +9 -3
  124. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  125. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  126. package/dist/systems/item/object-cut-invalidation.js +68 -0
  127. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  128. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  129. package/dist/systems/item-light/item-light-system.js +47 -52
  130. package/dist/systems/level/level-system.d.ts.map +1 -1
  131. package/dist/systems/level/level-system.js +17 -19
  132. package/dist/systems/level/level-utils.d.ts +28 -1
  133. package/dist/systems/level/level-utils.d.ts.map +1 -1
  134. package/dist/systems/level/level-utils.js +36 -4
  135. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  136. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  137. package/dist/systems/level/shadow-only-levels.js +7 -0
  138. package/dist/systems/roof/roof-materials.d.ts +14 -2
  139. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  140. package/dist/systems/roof/roof-materials.js +54 -15
  141. package/dist/systems/roof/roof-system.d.ts +13 -3
  142. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  143. package/dist/systems/roof/roof-system.js +188 -13
  144. package/dist/systems/slab/slab-system.d.ts +1 -1
  145. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  146. package/dist/systems/slab/slab-system.js +49 -19
  147. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  148. package/dist/systems/stair/stair-system.js +133 -354
  149. package/dist/systems/wall/wall-cutout-cache.d.ts +11 -1
  150. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  151. package/dist/systems/wall/wall-cutout-cache.js +58 -20
  152. package/dist/systems/wall/wall-cutout.d.ts +9 -1
  153. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-cutout.js +46 -32
  155. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  156. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  157. package/dist/systems/wall/wall-finish-data.js +17 -0
  158. package/dist/systems/wall/wall-materials.d.ts +17 -3
  159. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  160. package/dist/systems/wall/wall-materials.js +109 -156
  161. package/dist/systems/wall/wall-system.d.ts +43 -4
  162. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  163. package/dist/systems/wall/wall-system.js +601 -163
  164. package/dist/systems/window/window-system.d.ts +1 -1
  165. package/dist/systems/window/window-system.d.ts.map +1 -1
  166. package/dist/systems/window/window-system.js +44 -24
  167. package/dist/xr/presentation-context.d.ts +7 -0
  168. package/dist/xr/presentation-context.d.ts.map +1 -0
  169. package/dist/xr/presentation-context.js +10 -0
  170. package/package.json +6 -6
  171. package/dist/systems/fence/fence-system.d.ts +0 -22
  172. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  173. package/dist/systems/fence/fence-system.js +0 -357
@@ -14,7 +14,7 @@ export declare const EYE_LERP_SPEED = 12;
14
14
  * mesh collision) fed a collider built from the artifact's own geometry — so the
15
15
  * baked viewer walks the building with the same physics as the editor, without
16
16
  * the parametric scene. Pointer-lock drives look; WASD moves; Space jumps; Shift
17
- * sprints. Door/window interaction stays in `GlbScene` (its centre-ray HUD).
17
+ * sprints. Interaction stays in `GlbScene` (its centre-ray HUD).
18
18
  */
19
19
  export declare function GlbWalkthroughController({ url }: {
20
20
  url: string;
@@ -1 +1 @@
1
- {"version":3,"file":"glb-walkthrough-controller.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-walkthrough-controller.tsx"],"names":[],"mappings":"AAiCA,eAAO,MAAM,eAAe,KAAK,CAAA;AAkBjC,eAAO,MAAM,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AAChF,eAAO,MAAM,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AACjF,eAAO,MAAM,kBAAkB,MAAM,CAAA;AACrC,eAAO,MAAM,mBAAmB,OAAO,CAAA;AACvC,eAAO,MAAM,iBAAiB,MAAM,CAAA;AACpC,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAClC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAInC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,cAAc,KAAK,CAAA;AA8LhC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,sCAqPhE"}
1
+ {"version":3,"file":"glb-walkthrough-controller.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-walkthrough-controller.tsx"],"names":[],"mappings":"AAkCA,eAAO,MAAM,eAAe,KAAK,CAAA;AAkBjC,eAAO,MAAM,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AAChF,eAAO,MAAM,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AACjF,eAAO,MAAM,kBAAkB,MAAM,CAAA;AACrC,eAAO,MAAM,mBAAmB,OAAO,CAAA;AACvC,eAAO,MAAM,iBAAiB,MAAM,CAAA;AACpC,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAClC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAInC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,cAAc,KAAK,CAAA;AAiMhC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,sCA0PhE"}
@@ -10,6 +10,7 @@ import { useGLTFKTX2 } from '../../hooks/use-gltf-ktx2';
10
10
  import { SCENE_LAYER } from '../../lib/layers';
11
11
  import useViewer from '../../store/use-viewer';
12
12
  import BVHEcctrl, {} from './bvh-ecctrl';
13
+ import { applyWalkthroughCameraClipping } from './viewer-camera';
13
14
  // First-person FOV. The orbit camera is 50° (set on the Canvas), which feels
14
15
  // cramped on foot; ~60° vertical (~90° horizontal at 16:9) restores peripheral
15
16
  // awareness without wide-angle distortion. Applied only while walking — both
@@ -145,6 +146,11 @@ function buildGlbColliderWorld(scene) {
145
146
  return;
146
147
  if (!isEffectivelyVisible(mesh))
147
148
  return;
149
+ let ancestor = mesh;
150
+ while (ancestor && !ancestor.userData.proceduralMotion)
151
+ ancestor = ancestor.parent;
152
+ if (ancestor)
153
+ return;
148
154
  const kind = kindOf(mesh);
149
155
  if (kind && COLLIDER_EXCLUDED_KINDS.has(kind))
150
156
  return;
@@ -220,7 +226,7 @@ function resolveGlbSpawn(scene) {
220
226
  * mesh collision) fed a collider built from the artifact's own geometry — so the
221
227
  * baked viewer walks the building with the same physics as the editor, without
222
228
  * the parametric scene. Pointer-lock drives look; WASD moves; Space jumps; Shift
223
- * sprints. Door/window interaction stays in `GlbScene` (its centre-ray HUD).
229
+ * sprints. Interaction stays in `GlbScene` (its centre-ray HUD).
224
230
  */
225
231
  export function GlbWalkthroughController({ url }) {
226
232
  const { camera, gl } = useThree();
@@ -273,6 +279,11 @@ export function GlbWalkthroughController({ url }) {
273
279
  cam.updateProjectionMatrix();
274
280
  };
275
281
  }, [camera]);
282
+ useEffect(() => {
283
+ if (!camera.isPerspectiveCamera)
284
+ return;
285
+ return applyWalkthroughCameraClipping(camera);
286
+ }, [camera]);
276
287
  useEffect(() => {
277
288
  worldRef.current = world;
278
289
  if (world) {
@@ -1,5 +1,6 @@
1
1
  import { type AnyNodeId } from '@pascal-app/core';
2
2
  import { type ThreeElement } from '@react-three/fiber';
3
+ import { type ComponentType } from 'react';
3
4
  import * as THREE from 'three/webgpu';
4
5
  import type { ColorPreset, RenderShading } from '../../lib/materials';
5
6
  import { type RenderContext } from '../../store/use-viewer';
@@ -9,6 +10,22 @@ declare module '@react-three/fiber' {
9
10
  lineBasicNodeMaterial: ThreeElement<typeof THREE.LineBasicNodeMaterial>;
10
11
  }
11
12
  }
13
+ /**
14
+ * The scene-ready gate's own test as a count — nodes still to build or load
15
+ * (an item holds its mark until its model settles), 1 while the graph has
16
+ * not mounted. A capture waits on it, and reads a count that stops moving
17
+ * as a node no system will ever settle.
18
+ */
19
+ export declare function pendingSceneBuildCount(): number;
20
+ export interface ViewerImmersiveSession {
21
+ Session: ComponentType<{
22
+ children: React.ReactNode;
23
+ }>;
24
+ Scene: ComponentType<{
25
+ children: React.ReactNode;
26
+ }>;
27
+ onError?: (cause: unknown) => void;
28
+ }
12
29
  interface ViewerProps {
13
30
  children?: React.ReactNode;
14
31
  hoverStyles?: HoverStyles;
@@ -39,6 +56,13 @@ interface ViewerProps {
39
56
  */
40
57
  sceneReadyKey?: string | number | null;
41
58
  onSceneReadyChange?: (ready: boolean) => void;
59
+ /**
60
+ * Called when something in the scene throws while rendering (a node
61
+ * renderer or system, such as a plugin's lazy chunk that failed to load).
62
+ * The scene boundary still renders nothing in its place; this lets a host
63
+ * that must not carry on silently (a bake) fail with its own error.
64
+ */
65
+ onRenderError?: (cause: unknown) => void;
42
66
  /**
43
67
  * Wall-clock give-up cap for scene readiness, replacing the default
44
68
  * frame-count cap. Set it on hosts whose frame cadence is decoupled from
@@ -69,6 +93,8 @@ interface ViewerProps {
69
93
  disablePostFx?: boolean;
70
94
  /** Keep the mounted renderer/context warm without advancing scene frames. */
71
95
  renderPaused?: boolean;
96
+ /** Host-provided immersive XR session wrappers for the main scene. */
97
+ immersive?: ViewerImmersiveSession;
72
98
  }
73
99
  /** Imperative handle exposed via `ref` on `<Viewer>`. */
74
100
  export type ViewerHandle = {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,SAAS,EAMf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAkB,KAAK,YAAY,EAAsB,MAAM,oBAAoB,CAAA;AAS1F,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAKrE,OAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAatE,OAAuB,EAAwB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAY1F,OAAO,QAAQ,oBAAoB,CAAC;IAOlC,UAAU,aAAa;QACrB,qBAAqB,EAAE,YAAY,CAAC,OAAO,KAAK,CAAC,qBAAqB,CAAC,CAAA;KACxE;CACF;AA8QD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,aAAa,CAAA;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,WAAW,CAAC,EAAE,WAAW,CAAA;KAC1B,CAAA;IACD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACtC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;OAKG;IACH,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED,QAAA,MAAM,MAAM,sGA0PV,CAAA;AAEF,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,SAAS,EAOf,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAkB,KAAK,YAAY,EAAsB,MAAM,oBAAoB,CAAA;AAC1F,OAAO,EACL,KAAK,aAAa,EAOnB,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAKrE,OAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AActE,OAAuB,EAAwB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAa1F,OAAO,QAAQ,oBAAoB,CAAC;IAOlC,UAAU,aAAa;QACrB,qBAAqB,EAAE,YAAY,CAAC,OAAO,KAAK,CAAC,qBAAqB,CAAC,CAAA;KACxE;CACF;AAsPD;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAS/C;AAsED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;KAAE,CAAC,CAAA;IACrD,KAAK,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;KAAE,CAAC,CAAA;IACnD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,aAAa,CAAA;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,WAAW,CAAC,EAAE,WAAW,CAAA;KAC1B,CAAA;IACD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACtC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACxC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,sEAAsE;IACtE,SAAS,CAAC,EAAE,sBAAsB,CAAA;CACnC;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;OAKG;IACH,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED,QAAA,MAAM,MAAM,sGAwQV,CAAA;AAyEF,eAAe,MAAM,CAAA"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { nodeRegistry, RoofElevationSystem, StairOpeningSystem, sceneRegistry, useScene, } from '@pascal-app/core';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { nodeRegistry, RoofElevationSystem, StairOpeningSystem, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
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';
@@ -16,8 +16,9 @@ import useViewer, {} from '../../store/use-viewer';
16
16
  import { FloorElevationSystem } from '../../systems/floor-elevation/floor-elevation-system';
17
17
  import { GeometrySystem } from '../../systems/geometry/geometry-system';
18
18
  import { PerfActionSettleSystem } from '../../systems/perf-action-settle/perf-action-settle-system';
19
+ import { getPendingRoofMergeCount } from '../../systems/roof/roof-system';
19
20
  import { subscribeWallBuildInteractions } from '../../systems/wall/wall-build-lifecycle';
20
- import { ErrorBoundary } from '../error-boundary';
21
+ import { ImmersiveXRPresentationProvider } from '../../xr/presentation-context';
21
22
  import { SceneRenderer } from '../renderers/scene-renderer';
22
23
  import { BATCH_SPIKE_ENABLED, BatchedMeshSpike } from './batched-mesh-spike';
23
24
  import FrameLimiter from './frame-limiter';
@@ -27,6 +28,7 @@ import { PerfPanel } from './perf-panel';
27
28
  import { PointerRaycastLayers } from './pointer-raycast-layers';
28
29
  import PostProcessing, { DEFAULT_HOVER_STYLES } from './post-processing';
29
30
  import { RegisteredSystems } from './registered-systems';
31
+ import { composeRenderErrorHandlers, SceneErrorBoundary } from './render-error';
30
32
  import { useSceneAtmosphere } from './scene-atmosphere';
31
33
  import { SceneBvh } from './scene-bvh';
32
34
  import { SelectionManager } from './selection-manager';
@@ -82,6 +84,35 @@ const warnedEmptyDraw = process.env.NODE_ENV === 'production' ? null : new WeakS
82
84
  * carry the same check inline).
83
85
  */
84
86
  function installEmptyDrawGuard(renderer) {
87
+ // The render-object hook is one funnel; the post-processing passes and the
88
+ // outline node reach the backend by other paths, and a geometry whose
89
+ // draw range is empty still slips past a position count. The backend's
90
+ // own `draw` is the last gate before the command encoder — a zero-vertex
91
+ // draw leaves a vertex-buffer slot unbound ("Vertex buffer slot 1 required
92
+ // by [RenderPipeline "…MeshLambertNodeMaterial…"] was not set … Draw(0, …)",
93
+ // 2026-09-10) and poisons the whole encoder, so it is dropped here.
94
+ const backend = renderer.backend;
95
+ if (backend && typeof backend.draw === 'function' && !backend.__pascalDrawGuard) {
96
+ const draw = backend.draw.bind(backend);
97
+ backend.draw = (renderObject, ...rest) => {
98
+ const geometry = renderObject?.geometry;
99
+ const range = geometry?.drawRange;
100
+ if (!hasDrawableGeometry(geometry) || (range && range.count === 0)) {
101
+ if (warnedEmptyDraw && geometry && !warnedEmptyDraw.has(geometry)) {
102
+ warnedEmptyDraw.add(geometry);
103
+ console.warn('[viewer] dropped a zero-vertex draw at the backend', {
104
+ name: renderObject?.object
105
+ ?.name,
106
+ type: renderObject?.object
107
+ ?.type,
108
+ });
109
+ }
110
+ return;
111
+ }
112
+ return draw(renderObject, ...rest);
113
+ };
114
+ backend.__pascalDrawGuard = true;
115
+ }
85
116
  renderer.setRenderObjectFunction((object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId) => {
86
117
  if (!hasDrawableGeometry(geometry)) {
87
118
  if (warnedEmptyDraw && !warnedEmptyDraw.has(geometry ?? object)) {
@@ -94,7 +125,7 @@ function installEmptyDrawGuard(renderer) {
94
125
  renderer.renderObject(object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId);
95
126
  });
96
127
  }
97
- function GPUDeviceWatcher() {
128
+ function GPUDeviceWatcher({ intentionalWebGL = false }) {
98
129
  const gl = useThree((s) => s.gl);
99
130
  useEffect(() => {
100
131
  // Detect KTX2 transcode support as soon as the renderer exists, so catalog
@@ -103,13 +134,15 @@ function GPUDeviceWatcher() {
103
134
  ensureKtx2Support(gl);
104
135
  const backend = gl.backend;
105
136
  const device = backend?.device;
106
- if (!device) {
137
+ if (!device && !intentionalWebGL) {
107
138
  console.warn('[viewer] No WebGPU device on backend — running on a fallback renderer.', {
108
139
  backend: backend?.constructor?.name ?? 'unknown',
109
140
  rendererType: gl.constructor?.name ?? 'unknown',
110
141
  });
111
142
  return;
112
143
  }
144
+ if (!device)
145
+ return;
113
146
  console.log('[viewer] WebGPU device ready', {
114
147
  label: device.label,
115
148
  features: Array.from(device.features ?? []),
@@ -127,7 +160,7 @@ function GPUDeviceWatcher() {
127
160
  return () => {
128
161
  device.removeEventListener?.('uncapturederror', onUncapturedError);
129
162
  };
130
- }, [gl]);
163
+ }, [gl, intentionalWebGL]);
131
164
  return null;
132
165
  }
133
166
  function ToneMappingExposure() {
@@ -148,31 +181,55 @@ function ToneMappingExposure() {
148
181
  }, -1);
149
182
  return null;
150
183
  }
184
+ function ImmersiveXRBackground() {
185
+ const background = useViewer((state) => getSceneTheme(state.sceneTheme).background);
186
+ return _jsx("color", { args: [background], attach: "background" });
187
+ }
188
+ function isPendingSceneBuild(id, { nodes, rootNodeIds }) {
189
+ const node = nodes[id];
190
+ if (!node)
191
+ return false;
192
+ // Unreachable nodes (orphaned by a broken detach: the parent doesn't list
193
+ // them in `children`, or no parent and not a root) never render — every
194
+ // renderer enumerates the parent's `children` array — so no system will
195
+ // ever build them or clear their mark. They must not hold scene-ready
196
+ // hostage (observed in prod scene data: two dangling windows kept every
197
+ // bake of that scene waiting out the full readiness cap).
198
+ const parent = node.parentId ? nodes[node.parentId] : undefined;
199
+ const reachable = parent
200
+ ? parent.children?.includes(id) === true
201
+ : rootNodeIds.includes(id);
202
+ if (!reachable)
203
+ return false;
204
+ const def = nodeRegistry.get(node.type);
205
+ return Boolean(def?.geometry || def?.capabilities?.floorPlaced || DIRTY_BUILD_KINDS.has(node.type));
206
+ }
151
207
  function hasPendingSceneBuildWork() {
152
- const { dirtyNodes, nodes, rootNodeIds } = useScene.getState();
153
- for (const id of dirtyNodes) {
154
- const node = nodes[id];
155
- if (!node)
156
- continue;
157
- // Unreachable nodes (orphaned by a broken detach: the parent doesn't list
158
- // them in `children`, or no parent and not a root) never render — every
159
- // renderer enumerates the parent's `children` array — so no system will
160
- // ever build them or clear their mark. They must not hold scene-ready
161
- // hostage (observed in prod scene data: two dangling windows kept every
162
- // bake of that scene waiting out the full readiness cap).
163
- const parent = node.parentId ? nodes[node.parentId] : undefined;
164
- const reachable = parent
165
- ? parent.children?.includes(id) === true
166
- : rootNodeIds.includes(id);
167
- if (!reachable)
168
- continue;
169
- const def = nodeRegistry.get(node.type);
170
- if (def?.geometry || def?.capabilities?.floorPlaced || DIRTY_BUILD_KINDS.has(node.type)) {
208
+ const state = useScene.getState();
209
+ for (const id of state.dirtyNodes) {
210
+ if (isPendingSceneBuild(id, state))
171
211
  return true;
172
- }
173
212
  }
174
213
  return false;
175
214
  }
215
+ /**
216
+ * The scene-ready gate's own test as a count — nodes still to build or load
217
+ * (an item holds its mark until its model settles), 1 while the graph has
218
+ * not mounted. A capture waits on it, and reads a count that stops moving
219
+ * as a node no system will ever settle.
220
+ */
221
+ export function pendingSceneBuildCount() {
222
+ if (!hasCommittedSceneRoot())
223
+ return 1;
224
+ const state = useScene.getState();
225
+ // A roof's dirty mark clears when its merged shell is queued, frames before it is built.
226
+ let count = getPendingRoofMergeCount();
227
+ for (const id of state.dirtyNodes) {
228
+ if (isPendingSceneBuild(id, state))
229
+ count++;
230
+ }
231
+ return count;
232
+ }
176
233
  function hasCommittedSceneRoot() {
177
234
  const { nodes, rootNodeIds } = useScene.getState();
178
235
  if (rootNodeIds.length === 0)
@@ -226,7 +283,7 @@ function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, sceneReadyMaxWai
226
283
  }, 10);
227
284
  return null;
228
285
  }
229
- const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, renderContext = 'editor', transparent, defaultRender, isolate, sceneReadyKey, onSceneReadyChange, sceneReadyMaxWaitMs, maxFps = 50, disablePostFx = false, renderPaused = false, }, ref) {
286
+ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, renderContext = 'editor', transparent, defaultRender, isolate, sceneReadyKey, onSceneReadyChange, onRenderError, sceneReadyMaxWaitMs, maxFps = 50, disablePostFx = false, renderPaused = false, immersive, }, ref) {
230
287
  useEffect(() => {
231
288
  if (nodeRegistry.size === 0) {
232
289
  console.warn('[viewer] Node registry is empty. Install @pascal-app/nodes and call await loadPlugin(builtinPlugin) before mounting <Viewer>.');
@@ -252,6 +309,14 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
252
309
  const [pointerEvents] = useState(() => choosePointerEvents());
253
310
  const [rendererInitFailed, setRendererInitFailed] = useState(false);
254
311
  const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark');
312
+ const sceneTheme = useViewer((state) => state.sceneTheme);
313
+ const previousLampTheme = useRef(sceneTheme);
314
+ useEffect(() => {
315
+ useInteractive
316
+ .getState()
317
+ .setLampDefault(getSceneTheme(sceneTheme).appearance === 'dark', previousLampTheme.current !== sceneTheme);
318
+ previousLampTheme.current = sceneTheme;
319
+ }, [sceneTheme]);
255
320
  const transparentBackground = useViewer((state) => state.transparentBackground);
256
321
  // The shadows toggle drives `renderer.shadowMap.enabled` (via the Canvas
257
322
  // `shadows` prop) rather than the lights' `castShadow`: toggling castShadow
@@ -313,16 +378,25 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
313
378
  if (showGpuFallback)
314
379
  onSceneReadyChange?.(true);
315
380
  }, [showGpuFallback, onSceneReadyChange]);
381
+ useEffect(() => {
382
+ if (!immersive)
383
+ return;
384
+ const timeout = window.setTimeout(() => window.dispatchEvent(new Event('resize')), 0);
385
+ return () => window.clearTimeout(timeout);
386
+ }, [immersive]);
387
+ const ImmersiveSession = immersive?.Session;
388
+ const immersiveActive = immersive != null;
316
389
  if (showGpuFallback) {
317
390
  return _jsx(UnsupportedGpuViewerFallback, {});
318
391
  }
319
- return (_jsxs(_Fragment, { children: [(perf || PERF_OVERLAY_ENABLED) && _jsx(PerfPanel, {}), _jsxs(Canvas, { ref: subscribeWallBuildInteractions, camera: { position: [50, 50, 50], fov: 50 }, className: `transition-colors duration-700 ${transparentBackground ? 'bg-transparent' : isDark ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`, dpr: [1, maxDpr], events: pointerEvents, frameloop: "never", gl: ((props) => {
392
+ return (_jsxs(_Fragment, { children: [(perf || PERF_OVERLAY_ENABLED) && _jsx(PerfPanel, {}), _jsx(Canvas, { ref: subscribeWallBuildInteractions, camera: { position: [50, 50, 50], fov: 50 }, className: `transition-colors duration-700 ${transparentBackground ? 'bg-transparent' : isDark ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`, dpr: [1, maxDpr], events: pointerEvents, frameloop: "never", gl: ((props) => {
320
393
  const canvas = props.canvas;
321
394
  const cached = canvas ? WEBGPU_RENDERER_CACHE.get(canvas) : undefined;
322
395
  if (cached)
323
396
  return cached;
324
397
  const promise = (async () => {
325
398
  const result = await initializeGpuRenderer({
399
+ forceWebGL: immersiveActive,
326
400
  // Supplying `device` makes three skip its own `requestAdapter`,
327
401
  // so R3F's `powerPreference` only reaches the GPU if we forward it.
328
402
  powerPreference: props.powerPreference,
@@ -366,6 +440,11 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
366
440
  }, shadows: {
367
441
  type: THREE.PCFShadowMap,
368
442
  enabled: shadowsEnabled,
369
- }, children: [_jsx(FrameLimiter, { fps: maxFps, paused: renderPaused }), _jsx(ViewerCamera, {}), _jsx(PointerRaycastLayers, {}), _jsx(GPUDeviceWatcher, {}), _jsx(ToneMappingExposure, {}), _jsx(SceneReadyTracker, { onSceneReadyChange: onSceneReadyChange, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs }), _jsxs(ErrorBoundary, { fallback: null, scope: "viewer-scene", children: [_jsx(Lights, {}), useBvh ? (_jsx(SceneBvh, { children: _jsx(SceneRenderer, {}) })) : (_jsx(SceneRenderer, {})), _jsx(FloorElevationSystem, {}), _jsx(GeometrySystem, {}), _jsx(StairOpeningSystem, {}), _jsx(RoofElevationSystem, {}), _jsx(RegisteredSystems, {}), _jsx(PostProcessing, { disablePostFx: disablePostFx, hoverStyles: hoverStyles }), selectionManager === 'default' && _jsx(SelectionManager, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfMonitor, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfActionSettleSystem, {}), BATCH_SPIKE_ENABLED && _jsx(BatchedMeshSpike, {}), children] })] })] }));
443
+ }, 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')] }));
370
444
  });
445
+ function ViewerScene({ children, disablePostFx, hoverStyles, immersiveXR = false, renderErrorHandlers, onSceneReadyChange, perf, sceneReadyKey, sceneReadyMaxWaitMs, selectionManager, useBvh, SceneWrapper, }) {
446
+ const renderedScene = useBvh ? (_jsx(SceneBvh, { children: _jsx(SceneRenderer, {}) })) : (_jsx(SceneRenderer, {}));
447
+ const spatialScene = (_jsxs(_Fragment, { children: [renderedScene, _jsx(FloorElevationSystem, {}), _jsx(GeometrySystem, {}), _jsx(StairOpeningSystem, {}), _jsx(RoofElevationSystem, {}), _jsx(RegisteredSystems, {}), BATCH_SPIKE_ENABLED && _jsx(BatchedMeshSpike, {}), children] }));
448
+ 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, {})] })] }));
449
+ }
371
450
  export default Viewer;
@@ -1 +1 @@
1
- {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AA6EA,wBAAgB,MAAM,gCAqQrB"}
1
+ {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AA8EA,wBAAgB,MAAM,gCAwQrB"}
@@ -3,6 +3,7 @@ import { sceneRegistry } from '@pascal-app/core';
3
3
  import { useFrame } from '@react-three/fiber';
4
4
  import { useMemo, useRef } from 'react';
5
5
  import * as THREE from 'three/webgpu';
6
+ import { getKeyLightDirectionOverride } from '../../lib/key-light-override';
6
7
  import { SHADOW_ONLY_LAYER } from '../../lib/layers';
7
8
  import { getSceneTheme } from '../../lib/scene-themes';
8
9
  import useViewer from '../../store/use-viewer';
@@ -141,7 +142,11 @@ export function Lights() {
141
142
  const distance = size + SHADOW_BACKOFF;
142
143
  const near = SHADOW_BACKOFF;
143
144
  const far = distance + size;
144
- if (atmosphere) {
145
+ const directionOverride = getKeyLightDirectionOverride();
146
+ if (directionOverride) {
147
+ shadowDir.current.set(...directionOverride);
148
+ }
149
+ else if (atmosphere) {
145
150
  shadowDir.current.copy(atmosphere.sunDirection);
146
151
  }
147
152
  else if (themeKey) {
@@ -1 +1 @@
1
- {"version":3,"file":"perf-monitor.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/perf-monitor.tsx"],"names":[],"mappings":"AA2CA;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAuTvB,CAAA"}
1
+ {"version":3,"file":"perf-monitor.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/perf-monitor.tsx"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,eAAO,MAAM,WAAW,YA2TvB,CAAA"}
@@ -4,7 +4,9 @@ import { useEffect, useRef } from 'react';
4
4
  import { Vector3 } from 'three';
5
5
  import { initPerfObservers } from '../../lib/perf-observers';
6
6
  import { publishPerfStats, readPerfBatchStats } from '../../lib/perf-panel-store';
7
+ import { readPerfHistory, readPerfSelection } from '../../lib/perf-probe-state';
7
8
  import { clearPerfMeasures, drainPerfCounters } from '../../lib/perf-tracks';
9
+ import useViewer from '../../store/use-viewer';
8
10
  const SAMPLE_INTERVAL = 0.5; // seconds between display updates
9
11
  // Walking the scene graph is the overlay's own biggest cost on large projects,
10
12
  // and the counts barely move between ticks — sample it at 2s instead of 0.5s.
@@ -73,6 +75,10 @@ export const PerfMonitor = () => {
73
75
  useEffect(() => {
74
76
  const probe = {
75
77
  batchStats: readPerfBatchStats,
78
+ // Undo/redo entry counts and history pause state, and the selection path: what a
79
+ // gesture receipt needs on a production build, where the stores have no handle.
80
+ history: () => readPerfHistory(useScene),
81
+ selection: () => readPerfSelection(useViewer),
76
82
  listNodes(type) {
77
83
  return Object.values(useScene.getState().nodes)
78
84
  .filter((n) => n.type === type)
@@ -1 +1 @@
1
- {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA8CA,eAAO,MAAM,YAAY;;;CAGxB,CAAA;AAGD,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,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,GAAI,iCAG3B;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,SAmnBA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA+CA,eAAO,MAAM,YAAY;;;CAGxB,CAAA;AAGD,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,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,GAAI,iCAG3B;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,SAmnBA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
@@ -8,10 +8,11 @@ import { RenderPipeline, TimestampQuery } from 'three/webgpu';
8
8
  import { backdropGradient, deepSkyColor, horizonHazeColor } from '../../lib/backdrop';
9
9
  import { edgeColorFor, edgeOpacityScaleFor } from '../../lib/edge-style';
10
10
  import { PERF_OVERLAY_ENABLED } from '../../lib/gpu-perf';
11
- import { inkedEdges } from '../../lib/ink-edges';
11
+ import { createEdgeDepthSampler, inkedEdges } from '../../lib/ink-edges';
12
12
  import { refreshIsolation } from '../../lib/isolation';
13
13
  import { LayerPassIndex, LayerPassNode } from '../../lib/layer-pass';
14
14
  import { GRID_LAYER, OVERLAY_LAYER, SCENE_LAYER, ZONE_LAYER } from '../../lib/layers';
15
+ import { isLiveFrameHeld } from '../../lib/live-frame-hold';
15
16
  import { mergedOutline } from '../../lib/merged-outline-node';
16
17
  import { recordPerfSample, timeSpan } from '../../lib/perf-tracks';
17
18
  import { PostProcessingResources } from '../../lib/post-processing-resources';
@@ -371,6 +372,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
371
372
  // Scene depth is shared by SSGI, ink, and outlines.
372
373
  // The normal MRT is only built when SSGI or ink needs it.
373
374
  const scenePassDepth = scenePass.getTextureNode('depth');
375
+ const sampleEdgeDepth = createEdgeDepthSampler(scenePassDepth, camera);
374
376
  let scenePassNormal = null;
375
377
  if (needsNormalMRT) {
376
378
  scenePass.setMRT(mrt({
@@ -428,7 +430,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
428
430
  // (same ≈150→350 m window as ink-edges' distanceFade) so the horizon
429
431
  // disc and the geometry↔sky depth cliff never grow an AO band — that
430
432
  // band read as a visible line along the horizon.
431
- const aoFarFade = smoothstep(tslFloat(0.9994), tslFloat(0.9998), scenePassDepth.sample(screenUV).r);
433
+ const aoFarFade = smoothstep(tslFloat(0.9994), tslFloat(0.9998), sampleEdgeDepth(screenUV));
432
434
  ao = mix(ao, tslFloat(1), aoFarFade);
433
435
  // Composite: scene * AO + diffuse * GI
434
436
  sceneColor = vec4(add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))), contentAlpha);
@@ -439,7 +441,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
439
441
  if (inkEnabled && scenePassDepth && scenePassNormal) {
440
442
  sceneColor = vec4(inkedEdges({
441
443
  sceneRgb: sceneColor.rgb,
442
- depthTex: scenePassDepth,
444
+ sampleDepth: sampleEdgeDepth,
443
445
  normalTex: scenePassNormal,
444
446
  inkColor: inkColorUniform.current,
445
447
  radius: inkRadius,
@@ -574,6 +576,9 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
574
576
  if (size.width < 1 || size.height < 1) {
575
577
  return;
576
578
  }
579
+ // A capture is working on the shared scene: the canvas keeps its last frame.
580
+ if (isLiveFrameHeld())
581
+ return;
577
582
  // `?disable=draw`: nothing downstream wants pixels — render an EMPTY scene
578
583
  // instead of the real one. This is the only render call (positive-priority
579
584
  // useFrame subscribers already disable R3F's automatic render), so the real
@@ -1 +1 @@
1
- {"version":3,"file":"registered-systems.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/registered-systems.tsx"],"names":[],"mappings":"AA+BA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,uCAgChC"}
1
+ {"version":3,"file":"registered-systems.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/registered-systems.tsx"],"names":[],"mappings":"AAoDA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,uCAkChC"}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { createSceneApi, isNodeKindEnabled, nodeRegistry, useRegistryVersion, useScene, } from '@pascal-app/core';
4
- import { lazy, Suspense, useMemo } from 'react';
4
+ import { Component, lazy, Suspense, useMemo } from 'react';
5
5
  const DEFAULT_PRIORITY = 5;
6
6
  const lazyCache = new WeakMap();
7
7
  function loadSystem(def) {
@@ -14,6 +14,20 @@ function loadSystem(def) {
14
14
  lazyCache.set(def.system.module, Comp);
15
15
  return Comp;
16
16
  }
17
+ class SystemErrorBoundary extends Component {
18
+ state = { failed: false };
19
+ static getDerivedStateFromError() {
20
+ return { failed: true };
21
+ }
22
+ componentDidCatch(error) {
23
+ if (process.env.NODE_ENV !== 'production') {
24
+ console.error(`[viewer] Disabled system ${this.props.systemId}`, error);
25
+ }
26
+ }
27
+ render() {
28
+ return this.state.failed ? null : this.props.children;
29
+ }
30
+ }
17
31
  /**
18
32
  * Mounts every registered node kind's system component, ordered by
19
33
  * `system.priority` (default {@link DEFAULT_PRIORITY}).
@@ -56,6 +70,6 @@ export function RegisteredSystems() {
56
70
  const Comp = loadSystem(def);
57
71
  if (!Comp)
58
72
  return null;
59
- return (_jsx(Suspense, { fallback: null, children: _jsx(Comp, { sceneApi: sceneApi }) }, `registered-system:${kind}`));
73
+ return (_jsx(SystemErrorBoundary, { systemId: kind, children: _jsx(Suspense, { fallback: null, children: _jsx(Comp, { sceneApi: sceneApi }) }) }, `registered-system:${kind}`));
60
74
  }) }));
61
75
  }
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react';
2
+ type RenderErrorHandler = ((cause: unknown) => void) | undefined;
3
+ /**
4
+ * One handler that notifies every given handler (the immersive session's and
5
+ * the host's), or undefined when there is none. A handler that throws is
6
+ * logged and does not stop the others.
7
+ */
8
+ export declare function composeRenderErrorHandlers(...handlers: RenderErrorHandler[]): RenderErrorHandler;
9
+ /**
10
+ * The viewer-scene error boundary: a node renderer or system that throws while
11
+ * rendering renders nothing in its place, and every handler hears about it.
12
+ */
13
+ export declare function SceneErrorBoundary({ handlers, children, }: {
14
+ handlers: RenderErrorHandler[];
15
+ children: ReactNode;
16
+ }): import("react").JSX.Element;
17
+ export {};
18
+ //# sourceMappingURL=render-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-error.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/render-error.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,KAAK,kBAAkB,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;AAEhE;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,QAAQ,EAAE,kBAAkB,EAAE,GAAG,kBAAkB,CAYhG;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,QAAQ,EAAE,SAAS,CAAA;CACpB,+BAUA"}
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ErrorBoundary } from '../error-boundary';
3
+ /**
4
+ * One handler that notifies every given handler (the immersive session's and
5
+ * the host's), or undefined when there is none. A handler that throws is
6
+ * logged and does not stop the others.
7
+ */
8
+ export function composeRenderErrorHandlers(...handlers) {
9
+ const present = handlers.filter((handler) => handler !== undefined);
10
+ if (present.length === 0)
11
+ return;
12
+ return (cause) => {
13
+ for (const handler of present) {
14
+ try {
15
+ handler(cause);
16
+ }
17
+ catch (error) {
18
+ console.error('[viewer] a render-error handler threw', error);
19
+ }
20
+ }
21
+ };
22
+ }
23
+ /**
24
+ * The viewer-scene error boundary: a node renderer or system that throws while
25
+ * rendering renders nothing in its place, and every handler hears about it.
26
+ */
27
+ export function SceneErrorBoundary({ handlers, children, }) {
28
+ return (_jsx(ErrorBoundary, { fallback: null, onError: composeRenderErrorHandlers(...handlers), scope: "viewer-scene", children: children }));
29
+ }
@@ -1,5 +1,11 @@
1
+ import type { Scene } from 'three';
1
2
  /** Whether modeled exterior ground replaces this scene's fallback horizon disc. */
2
3
  export declare function useSceneGroundReplacement(): boolean;
4
+ /**
5
+ * The same claim from outside React (a map layer that hangs its own meshes on
6
+ * the scene): returns the release. The last release restores the fallback.
7
+ */
8
+ export declare function claimSceneGround(scene: Scene): () => void;
3
9
  /** Mount alongside replacement ground. The last release restores the fallback. */
4
10
  export declare function SceneGroundReplacement(): null;
5
11
  //# sourceMappingURL=scene-ground-replacement.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-ground-replacement.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-ground-replacement.tsx"],"names":[],"mappings":"AAoBA,mFAAmF;AACnF,wBAAgB,yBAAyB,IAAI,OAAO,CAInD;AAED,kFAAkF;AAClF,wBAAgB,sBAAsB,SAarC"}
1
+ {"version":3,"file":"scene-ground-replacement.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-ground-replacement.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAA;AAgBlC,mFAAmF;AACnF,wBAAgB,yBAAyB,IAAI,OAAO,CAInD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,IAAI,CASzD;AAED,kFAAkF;AAClF,wBAAgB,sBAAsB,SAarC"}
@@ -18,6 +18,21 @@ export function useSceneGroundReplacement() {
18
18
  const store = useMemo(() => groundStore(scene), [scene]);
19
19
  return useStore(store, (state) => state.owners > 0);
20
20
  }
21
+ /**
22
+ * The same claim from outside React (a map layer that hangs its own meshes on
23
+ * the scene): returns the release. The last release restores the fallback.
24
+ */
25
+ export function claimSceneGround(scene) {
26
+ const store = groundStore(scene);
27
+ store.setState((state) => ({ owners: state.owners + 1 }));
28
+ let released = false;
29
+ return () => {
30
+ if (released)
31
+ return;
32
+ released = true;
33
+ store.setState((state) => ({ owners: Math.max(0, state.owners - 1) }));
34
+ };
35
+ }
21
36
  /** Mount alongside replacement ground. The last release restores the fallback. */
22
37
  export function SceneGroundReplacement() {
23
38
  const scene = useThree((state) => state.scene);
@@ -1 +1 @@
1
- {"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"AAqSA,eAAO,MAAM,gBAAgB,mCA0G5B,CAAA"}
1
+ {"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"AAqSA,eAAO,MAAM,gBAAgB,mCA+G5B,CAAA"}