@pascal-app/viewer 1.0.3 → 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 (164) 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 +14 -0
  19. package/dist/components/viewer/index.d.ts.map +1 -1
  20. package/dist/components/viewer/index.js +84 -26
  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 +4 -0
  38. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  39. package/dist/components/viewer/viewer-camera.js +11 -1
  40. package/dist/index.d.ts +22 -11
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +20 -14
  43. package/dist/lib/asset-url.d.ts.map +1 -1
  44. package/dist/lib/asset-url.js +7 -1
  45. package/dist/lib/display-state.d.ts +25 -0
  46. package/dist/lib/display-state.d.ts.map +1 -0
  47. package/dist/lib/display-state.js +1 -0
  48. package/dist/lib/hero-pose.d.ts +12 -2
  49. package/dist/lib/hero-pose.d.ts.map +1 -1
  50. package/dist/lib/hero-pose.js +72 -5
  51. package/dist/lib/ink-edges.d.ts +4 -2
  52. package/dist/lib/ink-edges.d.ts.map +1 -1
  53. package/dist/lib/ink-edges.js +17 -7
  54. package/dist/lib/key-light-override.d.ts +11 -0
  55. package/dist/lib/key-light-override.d.ts.map +1 -0
  56. package/dist/lib/key-light-override.js +14 -0
  57. package/dist/lib/live-frame-hold.d.ts +14 -0
  58. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  59. package/dist/lib/live-frame-hold.js +28 -0
  60. package/dist/lib/materials.d.ts +14 -7
  61. package/dist/lib/materials.d.ts.map +1 -1
  62. package/dist/lib/materials.js +31 -5
  63. package/dist/lib/node-top-surface-height.d.ts +6 -0
  64. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  65. package/dist/lib/node-top-surface-height.js +74 -0
  66. package/dist/lib/perf-probe-state.d.ts +39 -0
  67. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  68. package/dist/lib/perf-probe-state.js +17 -0
  69. package/dist/lib/pointer-events.d.ts +8 -0
  70. package/dist/lib/pointer-events.d.ts.map +1 -1
  71. package/dist/lib/pointer-events.js +27 -4
  72. package/dist/lib/procedural-emission.d.ts +7 -0
  73. package/dist/lib/procedural-emission.d.ts.map +1 -0
  74. package/dist/lib/procedural-emission.js +54 -0
  75. package/dist/lib/scene-support-height.d.ts +4 -0
  76. package/dist/lib/scene-support-height.d.ts.map +1 -0
  77. package/dist/lib/scene-support-height.js +34 -0
  78. package/dist/lib/scene-visibility.d.ts +6 -0
  79. package/dist/lib/scene-visibility.d.ts.map +1 -1
  80. package/dist/lib/scene-visibility.js +15 -3
  81. package/dist/lib/scripted-opening.d.ts +11 -0
  82. package/dist/lib/scripted-opening.d.ts.map +1 -0
  83. package/dist/lib/scripted-opening.js +20 -0
  84. package/dist/lib/snapshot-pipeline.d.ts +32 -2
  85. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  86. package/dist/lib/snapshot-pipeline.js +215 -105
  87. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  88. package/dist/lib/terrain-perimeter-fill.js +22 -14
  89. package/dist/lib/zone-geometry.d.ts +11 -0
  90. package/dist/lib/zone-geometry.d.ts.map +1 -0
  91. package/dist/lib/zone-geometry.js +46 -0
  92. package/dist/store/use-item-light-pool.d.ts +14 -10
  93. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  94. package/dist/store/use-item-light-pool.js +52 -26
  95. package/dist/store/use-viewer.d.ts +11 -0
  96. package/dist/store/use-viewer.d.ts.map +1 -1
  97. package/dist/store/use-viewer.js +13 -6
  98. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  99. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  100. package/dist/systems/ceiling/ceiling-system.js +86 -6
  101. package/dist/systems/door/door-system.d.ts.map +1 -1
  102. package/dist/systems/door/door-system.js +55 -17
  103. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  104. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  105. package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
  106. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  107. package/dist/systems/geometry/geometry-system.js +71 -11
  108. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  109. package/dist/systems/interactive/interactive-system.js +40 -15
  110. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  111. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  112. package/dist/systems/interactive/procedural-controls.js +17 -0
  113. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  114. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  115. package/dist/systems/interactive/scripted-clips.js +134 -0
  116. package/dist/systems/item/item-system.d.ts.map +1 -1
  117. package/dist/systems/item/item-system.js +9 -3
  118. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  119. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  120. package/dist/systems/item/object-cut-invalidation.js +68 -0
  121. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  122. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  123. package/dist/systems/item-light/item-light-system.js +47 -52
  124. package/dist/systems/level/level-system.d.ts.map +1 -1
  125. package/dist/systems/level/level-system.js +3 -5
  126. package/dist/systems/level/level-utils.d.ts +7 -4
  127. package/dist/systems/level/level-utils.d.ts.map +1 -1
  128. package/dist/systems/level/level-utils.js +17 -6
  129. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  130. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  131. package/dist/systems/level/shadow-only-levels.js +7 -0
  132. package/dist/systems/roof/roof-materials.d.ts +14 -2
  133. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  134. package/dist/systems/roof/roof-materials.js +54 -15
  135. package/dist/systems/roof/roof-system.d.ts +13 -3
  136. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  137. package/dist/systems/roof/roof-system.js +188 -13
  138. package/dist/systems/slab/slab-system.d.ts +1 -1
  139. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  140. package/dist/systems/slab/slab-system.js +49 -19
  141. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  142. package/dist/systems/stair/stair-system.js +133 -354
  143. package/dist/systems/wall/wall-cutout-cache.d.ts +7 -0
  144. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  145. package/dist/systems/wall/wall-cutout-cache.js +46 -17
  146. package/dist/systems/wall/wall-cutout.d.ts +6 -0
  147. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  148. package/dist/systems/wall/wall-cutout.js +42 -28
  149. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  150. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  151. package/dist/systems/wall/wall-finish-data.js +17 -0
  152. package/dist/systems/wall/wall-materials.d.ts +9 -4
  153. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-materials.js +99 -152
  155. package/dist/systems/wall/wall-system.d.ts +28 -2
  156. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  157. package/dist/systems/wall/wall-system.js +582 -143
  158. package/dist/systems/window/window-system.d.ts +1 -1
  159. package/dist/systems/window/window-system.d.ts.map +1 -1
  160. package/dist/systems/window/window-system.js +41 -22
  161. package/package.json +5 -5
  162. package/dist/systems/fence/fence-system.d.ts +0 -22
  163. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  164. 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) {
@@ -10,6 +10,13 @@ declare module '@react-three/fiber' {
10
10
  lineBasicNodeMaterial: ThreeElement<typeof THREE.LineBasicNodeMaterial>;
11
11
  }
12
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;
13
20
  export interface ViewerImmersiveSession {
14
21
  Session: ComponentType<{
15
22
  children: React.ReactNode;
@@ -49,6 +56,13 @@ interface ViewerProps {
49
56
  */
50
57
  sceneReadyKey?: string | number | null;
51
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;
52
66
  /**
53
67
  * Wall-clock give-up cap for scene readiness, replacing the default
54
68
  * frame-count cap. Set it on hosts whose frame cadence is decoupled from
@@ -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;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;AAY1F,OAAO,QAAQ,oBAAoB,CAAC;IAOlC,UAAU,aAAa;QACrB,qBAAqB,EAAE,YAAY,CAAC,OAAO,KAAK,CAAC,qBAAqB,CAAC,CAAA;KACxE;CACF;AAoRD,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,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,sGAuPV,CAAA;AAyEF,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
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { nodeRegistry, RoofElevationSystem, StairOpeningSystem, sceneRegistry, useScene, } from '@pascal-app/core';
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,9 +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
21
  import { ImmersiveXRPresentationProvider } from '../../xr/presentation-context';
21
- import { ErrorBoundary } from '../error-boundary';
22
22
  import { SceneRenderer } from '../renderers/scene-renderer';
23
23
  import { BATCH_SPIKE_ENABLED, BatchedMeshSpike } from './batched-mesh-spike';
24
24
  import FrameLimiter from './frame-limiter';
@@ -28,6 +28,7 @@ import { PerfPanel } from './perf-panel';
28
28
  import { PointerRaycastLayers } from './pointer-raycast-layers';
29
29
  import PostProcessing, { DEFAULT_HOVER_STYLES } from './post-processing';
30
30
  import { RegisteredSystems } from './registered-systems';
31
+ import { composeRenderErrorHandlers, SceneErrorBoundary } from './render-error';
31
32
  import { useSceneAtmosphere } from './scene-atmosphere';
32
33
  import { SceneBvh } from './scene-bvh';
33
34
  import { SelectionManager } from './selection-manager';
@@ -83,6 +84,35 @@ const warnedEmptyDraw = process.env.NODE_ENV === 'production' ? null : new WeakS
83
84
  * carry the same check inline).
84
85
  */
85
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
+ }
86
116
  renderer.setRenderObjectFunction((object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId) => {
87
117
  if (!hasDrawableGeometry(geometry)) {
88
118
  if (warnedEmptyDraw && !warnedEmptyDraw.has(geometry ?? object)) {
@@ -155,31 +185,51 @@ function ImmersiveXRBackground() {
155
185
  const background = useViewer((state) => getSceneTheme(state.sceneTheme).background);
156
186
  return _jsx("color", { args: [background], attach: "background" });
157
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
+ }
158
207
  function hasPendingSceneBuildWork() {
159
- const { dirtyNodes, nodes, rootNodeIds } = useScene.getState();
160
- for (const id of dirtyNodes) {
161
- const node = nodes[id];
162
- if (!node)
163
- continue;
164
- // Unreachable nodes (orphaned by a broken detach: the parent doesn't list
165
- // them in `children`, or no parent and not a root) never render — every
166
- // renderer enumerates the parent's `children` array — so no system will
167
- // ever build them or clear their mark. They must not hold scene-ready
168
- // hostage (observed in prod scene data: two dangling windows kept every
169
- // bake of that scene waiting out the full readiness cap).
170
- const parent = node.parentId ? nodes[node.parentId] : undefined;
171
- const reachable = parent
172
- ? parent.children?.includes(id) === true
173
- : rootNodeIds.includes(id);
174
- if (!reachable)
175
- continue;
176
- const def = nodeRegistry.get(node.type);
177
- 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))
178
211
  return true;
179
- }
180
212
  }
181
213
  return false;
182
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
+ }
183
233
  function hasCommittedSceneRoot() {
184
234
  const { nodes, rootNodeIds } = useScene.getState();
185
235
  if (rootNodeIds.length === 0)
@@ -233,7 +283,7 @@ function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, sceneReadyMaxWai
233
283
  }, 10);
234
284
  return null;
235
285
  }
236
- 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, immersive, }, 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) {
237
287
  useEffect(() => {
238
288
  if (nodeRegistry.size === 0) {
239
289
  console.warn('[viewer] Node registry is empty. Install @pascal-app/nodes and call await loadPlugin(builtinPlugin) before mounting <Viewer>.');
@@ -259,6 +309,14 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
259
309
  const [pointerEvents] = useState(() => choosePointerEvents());
260
310
  const [rendererInitFailed, setRendererInitFailed] = useState(false);
261
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]);
262
320
  const transparentBackground = useViewer((state) => state.transparentBackground);
263
321
  // The shadows toggle drives `renderer.shadowMap.enabled` (via the Canvas
264
322
  // `shadows` prop) rather than the lights' `castShadow`: toggling castShadow
@@ -382,11 +440,11 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
382
440
  }, shadows: {
383
441
  type: THREE.PCFShadowMap,
384
442
  enabled: shadowsEnabled,
385
- }, children: _jsx(ImmersiveXRPresentationProvider, { enabled: immersiveActive, children: ImmersiveSession ? (_jsx(ImmersiveSession, { children: _jsx(ViewerScene, { disablePostFx: true, hoverStyles: hoverStyles, immersiveXR: true, onRenderError: immersive.onError, onSceneReadyChange: onSceneReadyChange, perf: perf, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs, selectionManager: selectionManager, useBvh: useBvh, SceneWrapper: immersive.Scene, children: children }) })) : (_jsxs(_Fragment, { children: [_jsx(FrameLimiter, { fps: maxFps, paused: renderPaused }), _jsx(ViewerScene, { disablePostFx: disablePostFx, hoverStyles: hoverStyles, onSceneReadyChange: onSceneReadyChange, perf: perf, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs, selectionManager: selectionManager, useBvh: useBvh, children: children })] })) }) }, immersiveActive ? 'webgl-xr' : 'webgpu')] }));
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')] }));
386
444
  });
387
- function ViewerScene({ children, disablePostFx, hoverStyles, immersiveXR = false, onRenderError, onSceneReadyChange, perf, sceneReadyKey, sceneReadyMaxWaitMs, selectionManager, useBvh, SceneWrapper, }) {
445
+ function ViewerScene({ children, disablePostFx, hoverStyles, immersiveXR = false, renderErrorHandlers, onSceneReadyChange, perf, sceneReadyKey, sceneReadyMaxWaitMs, selectionManager, useBvh, SceneWrapper, }) {
388
446
  const renderedScene = useBvh ? (_jsx(SceneBvh, { children: _jsx(SceneRenderer, {}) })) : (_jsx(SceneRenderer, {}));
389
447
  const spatialScene = (_jsxs(_Fragment, { children: [renderedScene, _jsx(FloorElevationSystem, {}), _jsx(GeometrySystem, {}), _jsx(StairOpeningSystem, {}), _jsx(RoofElevationSystem, {}), _jsx(RegisteredSystems, {}), BATCH_SPIKE_ENABLED && _jsx(BatchedMeshSpike, {}), children] }));
390
- 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(ErrorBoundary, { fallback: null, onError: onRenderError, scope: "viewer-scene", 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, {})] })] }));
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, {})] })] }));
391
449
  }
392
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"}
@@ -233,6 +233,10 @@ export const SelectionManager = () => {
233
233
  // re-subscribe when plugin kinds register after mount (async plugin load)
234
234
  void registryVersion;
235
235
  const onEnter = (event) => {
236
+ // Walkthrough is camera-only: first-person controls own hover for the
237
+ // interactable target, and pointer lock keeps raycasting from a frozen cursor.
238
+ if (useViewer.getState().walkthroughMode)
239
+ return;
236
240
  const strategy = getStrategy();
237
241
  if (!strategy)
238
242
  return;
@@ -254,6 +258,8 @@ export const SelectionManager = () => {
254
258
  }
255
259
  };
256
260
  const onLeave = (event) => {
261
+ if (useViewer.getState().walkthroughMode)
262
+ return;
257
263
  const strategy = getStrategy();
258
264
  if (!strategy)
259
265
  return;
@@ -268,6 +274,8 @@ export const SelectionManager = () => {
268
274
  }
269
275
  };
270
276
  const onClick = (event) => {
277
+ if (useViewer.getState().walkthroughMode)
278
+ return;
271
279
  const strategy = getStrategy();
272
280
  if (!strategy)
273
281
  return;
@@ -324,6 +332,8 @@ const PointerMissedHandler = ({ clickHandledRef, }) => {
324
332
  const viewerState = useViewer.getState();
325
333
  if (viewerState.cameraDragging || viewerState.inputDragging)
326
334
  return;
335
+ if (viewerState.walkthroughMode)
336
+ return;
327
337
  if (event.button !== 0)
328
338
  return;
329
339
  // Use requestAnimationFrame to check after R3F event handlers
@@ -9,6 +9,10 @@ export declare function applyViewerCameraClipping(camera: {
9
9
  near: number;
10
10
  updateProjectionMatrix(): void;
11
11
  }, immersiveXR: boolean): void;
12
+ export declare function applyWalkthroughCameraClipping(camera: {
13
+ near: number;
14
+ updateProjectionMatrix(): void;
15
+ }): () => void;
12
16
  export declare function viewerUsesPerspectiveCamera(cameraMode: string, immersiveXR: boolean): boolean;
13
17
  export declare const ViewerCamera: ({ immersiveXR }: {
14
18
  immersiveXR?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"viewer-camera.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/viewer-camera.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAOnC,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,cAWxF;AAcD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,OAAO;;;EAExD;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,sBAAsB,IAAI,IAAI,CAAA;CAAE,EACrE,WAAW,EAAE,OAAO,QAMrB;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,WAEnF;AAED,eAAO,MAAM,YAAY,GAAI,iBAAyB;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,gCAqB9E,CAAA"}
1
+ {"version":3,"file":"viewer-camera.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/viewer-camera.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAOnC,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,cAWxF;AAcD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,OAAO;;;EAExD;AAED,wBAAgB,yBAAyB,CACvC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,sBAAsB,IAAI,IAAI,CAAA;CAAE,EACrE,WAAW,EAAE,OAAO,QAMrB;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB,IAAI,IAAI,CAAA;CAC/B,cASA;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,WAEnF;AAED,eAAO,MAAM,YAAY,GAAI,iBAAyB;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,gCAqB9E,CAAA"}
@@ -29,7 +29,7 @@ function ImmersiveXRViewLayers({ enabled }) {
29
29
  return null;
30
30
  }
31
31
  export function viewerCameraClipping(immersiveXR) {
32
- return immersiveXR ? { far: 10_000, near: 0.001 } : { far: 1000, near: 0.1 };
32
+ return immersiveXR ? { far: 10_000, near: 0.001 } : { far: 1000, near: 0.3 };
33
33
  }
34
34
  export function applyViewerCameraClipping(camera, immersiveXR) {
35
35
  const clipping = viewerCameraClipping(immersiveXR);
@@ -37,6 +37,16 @@ export function applyViewerCameraClipping(camera, immersiveXR) {
37
37
  camera.near = clipping.near;
38
38
  camera.updateProjectionMatrix();
39
39
  }
40
+ export function applyWalkthroughCameraClipping(camera) {
41
+ const previousNear = camera.near;
42
+ // The eye can approach a wall to the capsule's 0.25 m radius.
43
+ camera.near = Math.min(previousNear, 0.1);
44
+ camera.updateProjectionMatrix();
45
+ return () => {
46
+ camera.near = previousNear;
47
+ camera.updateProjectionMatrix();
48
+ };
49
+ }
40
50
  export function viewerUsesPerspectiveCamera(cameraMode, immersiveXR) {
41
51
  return immersiveXR || cameraMode === 'perspective';
42
52
  }