@pascal-app/viewer 1.0.0-beta.4 → 1.0.0

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 (265) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/capture/asset-url.d.ts +2 -0
  4. package/dist/capture/asset-url.d.ts.map +1 -0
  5. package/dist/capture/asset-url.js +13 -0
  6. package/dist/capture/capture-runtime.d.ts +43 -0
  7. package/dist/capture/capture-runtime.d.ts.map +1 -0
  8. package/dist/capture/capture-runtime.js +228 -0
  9. package/dist/capture/frame.d.ts +4 -0
  10. package/dist/capture/frame.d.ts.map +1 -0
  11. package/dist/capture/frame.js +21 -0
  12. package/dist/capture/index.d.ts +12 -0
  13. package/dist/capture/index.d.ts.map +1 -0
  14. package/dist/capture/index.js +11 -0
  15. package/dist/capture/layer-visibility.d.ts +5 -0
  16. package/dist/capture/layer-visibility.d.ts.map +1 -0
  17. package/dist/capture/layer-visibility.js +11 -0
  18. package/dist/capture/layers/clay-matcap.d.ts +3 -0
  19. package/dist/capture/layers/clay-matcap.d.ts.map +1 -0
  20. package/dist/capture/layers/clay-matcap.js +26 -0
  21. package/dist/capture/layers/device-motion-layer.d.ts +7 -0
  22. package/dist/capture/layers/device-motion-layer.d.ts.map +1 -0
  23. package/dist/capture/layers/device-motion-layer.js +78 -0
  24. package/dist/capture/layers/point-cloud-layer.d.ts +15 -0
  25. package/dist/capture/layers/point-cloud-layer.d.ts.map +1 -0
  26. package/dist/capture/layers/point-cloud-layer.js +106 -0
  27. package/dist/capture/layers/room-model-layer.d.ts +9 -0
  28. package/dist/capture/layers/room-model-layer.d.ts.map +1 -0
  29. package/dist/capture/layers/room-model-layer.js +71 -0
  30. package/dist/capture/layers/surface-mesh-data.d.ts +9 -0
  31. package/dist/capture/layers/surface-mesh-data.d.ts.map +1 -0
  32. package/dist/capture/layers/surface-mesh-data.js +74 -0
  33. package/dist/capture/layers/surface-mesh-layer.d.ts +7 -0
  34. package/dist/capture/layers/surface-mesh-layer.d.ts.map +1 -0
  35. package/dist/capture/layers/surface-mesh-layer.js +16 -0
  36. package/dist/capture/preview.d.ts +3 -0
  37. package/dist/capture/preview.d.ts.map +1 -0
  38. package/dist/capture/preview.js +2 -0
  39. package/dist/capture/source-state.d.ts +22 -0
  40. package/dist/capture/source-state.d.ts.map +1 -0
  41. package/dist/capture/source-state.js +182 -0
  42. package/dist/capture/stream-rendering.d.ts +14 -0
  43. package/dist/capture/stream-rendering.d.ts.map +1 -0
  44. package/dist/capture/stream-rendering.js +69 -0
  45. package/dist/capture/trajectory.d.ts +20 -0
  46. package/dist/capture/trajectory.d.ts.map +1 -0
  47. package/dist/capture/trajectory.js +77 -0
  48. package/dist/components/viewer/batched-mesh-spike.d.ts +14 -0
  49. package/dist/components/viewer/batched-mesh-spike.d.ts.map +1 -0
  50. package/dist/components/viewer/batched-mesh-spike.js +64 -0
  51. package/dist/components/viewer/frame-limiter.d.ts +11 -0
  52. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  53. package/dist/components/viewer/frame-limiter.js +59 -18
  54. package/dist/components/viewer/index.d.ts +15 -0
  55. package/dist/components/viewer/index.d.ts.map +1 -1
  56. package/dist/components/viewer/index.js +74 -82
  57. package/dist/components/viewer/lights.d.ts.map +1 -1
  58. package/dist/components/viewer/lights.js +123 -89
  59. package/dist/components/viewer/perf-monitor.d.ts +6 -1
  60. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  61. package/dist/components/viewer/perf-monitor.js +306 -115
  62. package/dist/components/viewer/perf-panel.d.ts +3 -0
  63. package/dist/components/viewer/perf-panel.d.ts.map +1 -0
  64. package/dist/components/viewer/perf-panel.js +178 -0
  65. package/dist/components/viewer/pointer-raycast-layers.d.ts +15 -0
  66. package/dist/components/viewer/pointer-raycast-layers.d.ts.map +1 -0
  67. package/dist/components/viewer/pointer-raycast-layers.js +28 -0
  68. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  69. package/dist/components/viewer/post-processing.js +124 -83
  70. package/dist/components/viewer/registered-systems.d.ts +14 -5
  71. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  72. package/dist/components/viewer/registered-systems.js +22 -10
  73. package/dist/components/viewer/scene-atmosphere.d.ts +30 -0
  74. package/dist/components/viewer/scene-atmosphere.d.ts.map +1 -0
  75. package/dist/components/viewer/scene-atmosphere.js +89 -0
  76. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  77. package/dist/components/viewer/scene-environment.js +19 -5
  78. package/dist/components/viewer/scene-ground-replacement.d.ts +5 -0
  79. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -0
  80. package/dist/components/viewer/scene-ground-replacement.js +35 -0
  81. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  82. package/dist/components/viewer/selection-manager.js +7 -2
  83. package/dist/components/viewer/unsupported-gpu-fallback.d.ts +2 -0
  84. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -0
  85. package/dist/components/viewer/unsupported-gpu-fallback.js +4 -0
  86. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  87. package/dist/components/viewer/viewer-camera.js +4 -1
  88. package/dist/components/viewer/viewer-presentations.d.ts +67 -0
  89. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -0
  90. package/dist/components/viewer/viewer-presentations.js +120 -0
  91. package/dist/hooks/use-library-materials-version.d.ts +7 -0
  92. package/dist/hooks/use-library-materials-version.d.ts.map +1 -0
  93. package/dist/hooks/use-library-materials-version.js +10 -0
  94. package/dist/hooks/use-node-events.d.ts.map +1 -1
  95. package/dist/hooks/use-node-events.js +1 -0
  96. package/dist/index.d.ts +20 -8
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +19 -7
  99. package/dist/lib/csg-utils.d.ts +1 -0
  100. package/dist/lib/csg-utils.d.ts.map +1 -1
  101. package/dist/lib/csg-utils.js +6 -1
  102. package/dist/lib/dispose-object3d.d.ts +4 -0
  103. package/dist/lib/dispose-object3d.d.ts.map +1 -0
  104. package/dist/lib/dispose-object3d.js +27 -0
  105. package/dist/lib/geometry-groups.d.ts +17 -0
  106. package/dist/lib/geometry-groups.d.ts.map +1 -0
  107. package/dist/lib/geometry-groups.js +53 -0
  108. package/dist/lib/gpu-perf.d.ts +0 -2
  109. package/dist/lib/gpu-perf.d.ts.map +1 -1
  110. package/dist/lib/gpu-perf.js +7 -21
  111. package/dist/lib/isolation.d.ts +8 -8
  112. package/dist/lib/isolation.d.ts.map +1 -1
  113. package/dist/lib/isolation.js +18 -30
  114. package/dist/lib/layer-pass.d.ts +28 -0
  115. package/dist/lib/layer-pass.d.ts.map +1 -0
  116. package/dist/lib/layer-pass.js +254 -0
  117. package/dist/lib/layers.d.ts +16 -0
  118. package/dist/lib/layers.d.ts.map +1 -1
  119. package/dist/lib/layers.js +18 -0
  120. package/dist/lib/materials.d.ts +12 -0
  121. package/dist/lib/materials.d.ts.map +1 -1
  122. package/dist/lib/materials.js +51 -17
  123. package/dist/lib/merged-outline-node.d.ts +17 -8
  124. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  125. package/dist/lib/merged-outline-node.js +194 -52
  126. package/dist/lib/perf-actions.d.ts +55 -0
  127. package/dist/lib/perf-actions.d.ts.map +1 -0
  128. package/dist/lib/perf-actions.js +211 -0
  129. package/dist/lib/perf-observers.d.ts +2 -0
  130. package/dist/lib/perf-observers.d.ts.map +1 -0
  131. package/dist/lib/perf-observers.js +17 -0
  132. package/dist/lib/perf-panel-store.d.ts +71 -0
  133. package/dist/lib/perf-panel-store.d.ts.map +1 -0
  134. package/dist/lib/perf-panel-store.js +29 -0
  135. package/dist/lib/perf-tracks.d.ts +43 -0
  136. package/dist/lib/perf-tracks.d.ts.map +1 -0
  137. package/dist/lib/perf-tracks.js +113 -0
  138. package/dist/lib/pointer-events.d.ts +7 -0
  139. package/dist/lib/pointer-events.d.ts.map +1 -0
  140. package/dist/lib/pointer-events.js +680 -0
  141. package/dist/lib/post-processing-resources.d.ts +12 -0
  142. package/dist/lib/post-processing-resources.d.ts.map +1 -0
  143. package/dist/lib/post-processing-resources.js +19 -0
  144. package/dist/lib/renderer-capability.d.ts +56 -0
  145. package/dist/lib/renderer-capability.d.ts.map +1 -0
  146. package/dist/lib/renderer-capability.js +116 -0
  147. package/dist/lib/scene-visibility.d.ts +15 -0
  148. package/dist/lib/scene-visibility.d.ts.map +1 -0
  149. package/dist/lib/scene-visibility.js +69 -0
  150. package/dist/lib/shadow-only.d.ts +16 -0
  151. package/dist/lib/shadow-only.d.ts.map +1 -1
  152. package/dist/lib/shadow-only.js +8 -17
  153. package/dist/lib/snapshot-pipeline.d.ts +9 -0
  154. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  155. package/dist/lib/snapshot-pipeline.js +33 -9
  156. package/dist/store/use-viewer.d.ts +7 -6
  157. package/dist/store/use-viewer.d.ts.map +1 -1
  158. package/dist/systems/ceiling/ceiling-system.js +1 -1
  159. package/dist/systems/door/door-animation-system.d.ts.map +1 -1
  160. package/dist/systems/door/door-animation-system.js +6 -9
  161. package/dist/systems/door/door-system.d.ts.map +1 -1
  162. package/dist/systems/door/door-system.js +13 -2
  163. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  164. package/dist/systems/floor-elevation/floor-elevation-system.js +9 -0
  165. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  166. package/dist/systems/geometry/geometry-system.js +5 -28
  167. package/dist/systems/level/level-system.d.ts.map +1 -1
  168. package/dist/systems/level/level-system.js +8 -2
  169. package/dist/systems/level/level-utils.d.ts +9 -0
  170. package/dist/systems/level/level-utils.d.ts.map +1 -1
  171. package/dist/systems/level/level-utils.js +14 -0
  172. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts +8 -0
  173. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts.map +1 -0
  174. package/dist/systems/perf-action-settle/perf-action-settle-system.js +32 -0
  175. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  176. package/dist/systems/roof/roof-system.js +1196 -58
  177. package/dist/systems/scan/scan-system.js +6 -5
  178. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  179. package/dist/systems/slab/slab-system.js +1 -2
  180. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  181. package/dist/systems/stair/stair-system.js +9 -8
  182. package/dist/systems/surface-hole-geometry.d.ts +1 -1
  183. package/dist/systems/surface-hole-geometry.d.ts.map +1 -1
  184. package/dist/systems/surface-hole-geometry.js +1 -1
  185. package/dist/systems/wall/level-miter-cache.d.ts +10 -0
  186. package/dist/systems/wall/level-miter-cache.d.ts.map +1 -0
  187. package/dist/systems/wall/level-miter-cache.js +40 -0
  188. package/dist/systems/wall/wall-build-lifecycle.d.ts +9 -0
  189. package/dist/systems/wall/wall-build-lifecycle.d.ts.map +1 -0
  190. package/dist/systems/wall/wall-build-lifecycle.js +74 -0
  191. package/dist/systems/wall/wall-cutout-cache.d.ts +57 -0
  192. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-cutout-cache.js +304 -0
  194. package/dist/systems/wall/wall-cutout.d.ts +24 -1
  195. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  196. package/dist/systems/wall/wall-cutout.js +33 -140
  197. package/dist/systems/wall/wall-material-variant.d.ts +31 -0
  198. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -0
  199. package/dist/systems/wall/wall-material-variant.js +10 -0
  200. package/dist/systems/wall/wall-materials.d.ts +6 -0
  201. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  202. package/dist/systems/wall/wall-materials.js +29 -13
  203. package/dist/systems/wall/wall-placeholder-sweep.d.ts +50 -0
  204. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -0
  205. package/dist/systems/wall/wall-placeholder-sweep.js +51 -0
  206. package/dist/systems/wall/wall-rebuild-notifications.d.ts +5 -0
  207. package/dist/systems/wall/wall-rebuild-notifications.d.ts.map +1 -0
  208. package/dist/systems/wall/wall-rebuild-notifications.js +23 -0
  209. package/dist/systems/wall/wall-system.d.ts +13 -2
  210. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  211. package/dist/systems/wall/wall-system.js +413 -148
  212. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  213. package/dist/systems/window/window-animation-system.js +9 -8
  214. package/dist/systems/window/window-system.d.ts +1 -0
  215. package/dist/systems/window/window-system.d.ts.map +1 -1
  216. package/dist/systems/window/window-system.js +16 -2
  217. package/package.json +18 -3
  218. package/dist/components/viewer/scene-bvh.test.d.ts +0 -2
  219. package/dist/components/viewer/scene-bvh.test.d.ts.map +0 -1
  220. package/dist/components/viewer/scene-bvh.test.js +0 -13
  221. package/dist/lib/csg-utils.test.d.ts +0 -2
  222. package/dist/lib/csg-utils.test.d.ts.map +0 -1
  223. package/dist/lib/csg-utils.test.js +0 -45
  224. package/dist/lib/materials.test.d.ts +0 -2
  225. package/dist/lib/materials.test.d.ts.map +0 -1
  226. package/dist/lib/materials.test.js +0 -25
  227. package/dist/lib/merged-outline-node.test.d.ts +0 -2
  228. package/dist/lib/merged-outline-node.test.d.ts.map +0 -1
  229. package/dist/lib/merged-outline-node.test.js +0 -22
  230. package/dist/lib/polygon-union.d.ts +0 -4
  231. package/dist/lib/polygon-union.d.ts.map +0 -1
  232. package/dist/lib/polygon-union.js +0 -289
  233. package/dist/lib/polygon-union.test.d.ts +0 -2
  234. package/dist/lib/polygon-union.test.d.ts.map +0 -1
  235. package/dist/lib/polygon-union.test.js +0 -105
  236. package/dist/lib/texture-reference.test.d.ts +0 -2
  237. package/dist/lib/texture-reference.test.d.ts.map +0 -1
  238. package/dist/lib/texture-reference.test.js +0 -86
  239. package/dist/store/use-viewer.test.d.ts +0 -2
  240. package/dist/store/use-viewer.test.d.ts.map +0 -1
  241. package/dist/store/use-viewer.test.js +0 -61
  242. package/dist/systems/door/door-animation.test.d.ts +0 -2
  243. package/dist/systems/door/door-animation.test.d.ts.map +0 -1
  244. package/dist/systems/door/door-animation.test.js +0 -51
  245. package/dist/systems/door/door-floor-alignment.test.d.ts +0 -2
  246. package/dist/systems/door/door-floor-alignment.test.d.ts.map +0 -1
  247. package/dist/systems/door/door-floor-alignment.test.js +0 -50
  248. package/dist/systems/geometry/geometry-system.test.d.ts +0 -2
  249. package/dist/systems/geometry/geometry-system.test.d.ts.map +0 -1
  250. package/dist/systems/geometry/geometry-system.test.js +0 -30
  251. package/dist/systems/slab/slab-system.test.d.ts +0 -2
  252. package/dist/systems/slab/slab-system.test.d.ts.map +0 -1
  253. package/dist/systems/slab/slab-system.test.js +0 -102
  254. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +0 -2
  255. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +0 -1
  256. package/dist/systems/wall/opening-cutout-geometry.test.js +0 -172
  257. package/dist/systems/wall/wall-opening-cutout.test.d.ts +0 -2
  258. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +0 -1
  259. package/dist/systems/wall/wall-opening-cutout.test.js +0 -59
  260. package/dist/systems/wall/wall-support-extension.test.d.ts +0 -2
  261. package/dist/systems/wall/wall-support-extension.test.d.ts.map +0 -1
  262. package/dist/systems/wall/wall-support-extension.test.js +0 -116
  263. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +0 -2
  264. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +0 -1
  265. package/dist/systems/wall/wall-terrain-bottom.test.js +0 -34
@@ -0,0 +1,20 @@
1
+ import { type DeviceMotionTrajectoryPayload } from '@pascal-app/core/capture';
2
+ export type DeviceTrajectoryPose = {
3
+ position: [number, number, number];
4
+ quaternion: [number, number, number, number];
5
+ segment: number;
6
+ timestamp: number;
7
+ };
8
+ export type DeviceTrajectory = {
9
+ duration: number;
10
+ poses: DeviceTrajectoryPose[];
11
+ };
12
+ export type DeviceTrajectoryFrame = {
13
+ alpha: number;
14
+ from: DeviceTrajectoryPose;
15
+ to: DeviceTrajectoryPose;
16
+ };
17
+ export declare function parseDeviceTrajectoryPayload(trajectory: DeviceMotionTrajectoryPayload | null | undefined): DeviceTrajectory | null;
18
+ export declare function parseDeviceTrajectoryPackets(payloads: readonly unknown[]): DeviceTrajectory | null;
19
+ export declare function sampleDeviceTrajectory(trajectory: DeviceTrajectory, elapsed: number): DeviceTrajectoryFrame;
20
+ //# sourceMappingURL=trajectory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trajectory.d.ts","sourceRoot":"","sources":["../../src/capture/trajectory.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,6BAA6B,EAEnC,MAAM,0BAA0B,CAAA;AAGjC,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5C,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,oBAAoB,EAAE,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,oBAAoB,CAAA;IAC1B,EAAE,EAAE,oBAAoB,CAAA;CACzB,CAAA;AAED,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,6BAA6B,GAAG,IAAI,GAAG,SAAS,GAC3D,gBAAgB,GAAG,IAAI,CAezB;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,SAAS,OAAO,EAAE,GAC3B,gBAAgB,GAAG,IAAI,CAezB;AAED,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,gBAAgB,EAC5B,OAAO,EAAE,MAAM,GACd,qBAAqB,CAqBvB"}
@@ -0,0 +1,77 @@
1
+ import { DeviceMotionSampleSchema, DeviceMotionTrajectorySchema, } from '@pascal-app/core/capture';
2
+ import { Matrix4, Quaternion, Vector3 } from 'three';
3
+ export function parseDeviceTrajectoryPayload(trajectory) {
4
+ if (!trajectory)
5
+ return null;
6
+ const parsed = DeviceMotionTrajectorySchema.safeParse(trajectory);
7
+ if (!parsed.success)
8
+ return null;
9
+ const poses = parsed.data.samples
10
+ .map(parsePose)
11
+ .sort((left, right) => left.timestamp - right.timestamp);
12
+ if (poses.length < 2)
13
+ return null;
14
+ const firstTimestamp = poses[0]?.timestamp ?? 0;
15
+ for (const pose of poses)
16
+ pose.timestamp -= firstTimestamp;
17
+ const duration = poses.at(-1)?.timestamp ?? 0;
18
+ if (!(duration > 0))
19
+ return null;
20
+ return { duration, poses };
21
+ }
22
+ export function parseDeviceTrajectoryPackets(payloads) {
23
+ let coordinateSystem = 'source';
24
+ let samples = [];
25
+ for (const payload of payloads) {
26
+ const trajectory = DeviceMotionTrajectorySchema.safeParse(payload);
27
+ if (trajectory.success) {
28
+ coordinateSystem = trajectory.data.coordinateSystem;
29
+ samples = [...trajectory.data.samples];
30
+ continue;
31
+ }
32
+ const sample = DeviceMotionSampleSchema.safeParse(payload);
33
+ if (sample.success)
34
+ samples.push(sample.data);
35
+ }
36
+ if (samples.length < 2)
37
+ return null;
38
+ return parseDeviceTrajectoryPayload({ coordinateSystem, samples });
39
+ }
40
+ export function sampleDeviceTrajectory(trajectory, elapsed) {
41
+ const time = positiveModulo(elapsed, trajectory.duration);
42
+ const poses = trajectory.poses;
43
+ const first = poses[0];
44
+ const last = poses.at(-1);
45
+ if (!(first && last))
46
+ throw new Error('Device trajectory requires at least two poses.');
47
+ let upperIndex = poses.findIndex((pose) => pose.timestamp > time);
48
+ if (upperIndex < 0)
49
+ upperIndex = poses.length - 1;
50
+ if (upperIndex === 0)
51
+ return { alpha: 0, from: first, to: first };
52
+ const from = poses[upperIndex - 1] ?? first;
53
+ const to = poses[upperIndex] ?? last;
54
+ if (from.segment !== to.segment)
55
+ return { alpha: 0, from, to: from };
56
+ const interval = to.timestamp - from.timestamp;
57
+ return {
58
+ alpha: interval > 0 ? Math.min(1, Math.max(0, (time - from.timestamp) / interval)) : 0,
59
+ from,
60
+ to,
61
+ };
62
+ }
63
+ function parsePose(value) {
64
+ const matrix = new Matrix4().fromArray(value.transform);
65
+ const position = new Vector3();
66
+ const quaternion = new Quaternion();
67
+ matrix.decompose(position, quaternion, new Vector3());
68
+ return {
69
+ position: [position.x, position.y, position.z],
70
+ quaternion: [quaternion.x, quaternion.y, quaternion.z, quaternion.w],
71
+ segment: value.segment,
72
+ timestamp: value.timestamp,
73
+ };
74
+ }
75
+ function positiveModulo(value, divisor) {
76
+ return ((value % divisor) + divisor) % divisor;
77
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * De-risk spike for item instancing (charter backlog #3a): mounts one
3
+ * BatchedMesh — two geometries, a few hundred instances, one material —
4
+ * through the real render pipeline (post-FX, shadow pass, WebGPU backend).
5
+ * `?spike=batch` only; never mounted in normal sessions.
6
+ *
7
+ * What it proves, read via the ?perf panel + `window.__batchSpike`:
8
+ * - DRAW rises by ~1 per pass, not by the instance count.
9
+ * - TRI drops as instances leave the frustum (perObjectFrustumCulled works).
10
+ * - Shadows cast/receive; no pipeline crash.
11
+ */
12
+ export declare const BATCH_SPIKE_ENABLED: boolean;
13
+ export declare const BatchedMeshSpike: () => null;
14
+ //# sourceMappingURL=batched-mesh-spike.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batched-mesh-spike.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/batched-mesh-spike.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,SAEsC,CAAA;AAStE,eAAO,MAAM,gBAAgB,YAoD5B,CAAA"}
@@ -0,0 +1,64 @@
1
+ import { useThree } from '@react-three/fiber';
2
+ import { useEffect } from 'react';
3
+ import * as THREE from 'three';
4
+ /**
5
+ * De-risk spike for item instancing (charter backlog #3a): mounts one
6
+ * BatchedMesh — two geometries, a few hundred instances, one material —
7
+ * through the real render pipeline (post-FX, shadow pass, WebGPU backend).
8
+ * `?spike=batch` only; never mounted in normal sessions.
9
+ *
10
+ * What it proves, read via the ?perf panel + `window.__batchSpike`:
11
+ * - DRAW rises by ~1 per pass, not by the instance count.
12
+ * - TRI drops as instances leave the frustum (perObjectFrustumCulled works).
13
+ * - Shadows cast/receive; no pipeline crash.
14
+ */
15
+ export const BATCH_SPIKE_ENABLED = typeof window !== 'undefined' &&
16
+ new URLSearchParams(window.location.search).get('spike') === 'batch';
17
+ // `?spike=batch&spikeGrid=70` → 4,900 instances; default 20×20 = 400.
18
+ const GRID = typeof window !== 'undefined'
19
+ ? Math.min(120, Number(new URLSearchParams(window.location.search).get('spikeGrid')) || 20)
20
+ : 20;
21
+ const SPACING = 1.2;
22
+ export const BatchedMeshSpike = () => {
23
+ const scene = useThree((s) => s.scene);
24
+ useEffect(() => {
25
+ const box = new THREE.BoxGeometry(0.4, 0.4, 0.4);
26
+ const sphere = new THREE.SphereGeometry(0.25, 16, 12);
27
+ const material = new THREE.MeshStandardMaterial({ color: '#c2410c', roughness: 0.6 });
28
+ const maxVertices = (box.attributes.position?.count ?? 0) + (sphere.attributes.position?.count ?? 0);
29
+ const maxIndices = (box.index?.count ?? 0) + (sphere.index?.count ?? 0);
30
+ const batch = new THREE.BatchedMesh(GRID * GRID, maxVertices, maxIndices, material);
31
+ batch.castShadow = true;
32
+ batch.receiveShadow = true;
33
+ // Default is true — asserted explicitly because per-instance frustum
34
+ // culling is the property the whole plan depends on.
35
+ batch.perObjectFrustumCulled = true;
36
+ const boxGeomId = batch.addGeometry(box);
37
+ const sphereGeomId = batch.addGeometry(sphere);
38
+ const m = new THREE.Matrix4();
39
+ let count = 0;
40
+ for (let x = 0; x < GRID; x++) {
41
+ for (let z = 0; z < GRID; z++) {
42
+ const geomId = (x + z) % 2 === 0 ? boxGeomId : sphereGeomId;
43
+ const id = batch.addInstance(geomId);
44
+ m.setPosition((x - GRID / 2) * SPACING, 6 + Math.sin(x * 0.7) * 0.5 + Math.cos(z * 0.5) * 0.5, (z - GRID / 2) * SPACING);
45
+ batch.setMatrixAt(id, m);
46
+ count++;
47
+ }
48
+ }
49
+ scene.add(batch);
50
+ window.__batchSpike = {
51
+ instances: count,
52
+ geometries: 2,
53
+ };
54
+ return () => {
55
+ scene.remove(batch);
56
+ batch.dispose();
57
+ box.dispose();
58
+ sphere.dispose();
59
+ material.dispose();
60
+ delete window.__batchSpike;
61
+ };
62
+ }, [scene]);
63
+ return null;
64
+ };
@@ -1,6 +1,17 @@
1
1
  type FrameLimiterProps = {
2
2
  fps?: number;
3
+ paused?: boolean;
3
4
  };
5
+ export type FrameClock = {
6
+ sample: (wallTimeMs: number, intervalMs: number) => number | null;
7
+ step: (seconds: number) => number;
8
+ };
9
+ /**
10
+ * Keeps R3F's manual clock monotonic while each limiter effect owns a fresh
11
+ * wall-time baseline. The first rAF sample establishes that baseline instead
12
+ * of treating the browser's process uptime as elapsed frame time.
13
+ */
14
+ export declare function createFrameClock(initialTime?: number): FrameClock;
4
15
  declare const FrameLimiter: React.FC<FrameLimiterProps>;
5
16
  export default FrameLimiter;
6
17
  //# sourceMappingURL=frame-limiter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frame-limiter.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/frame-limiter.tsx"],"names":[],"mappings":"AAIA,KAAK,iBAAiB,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAgBD,QAAA,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAyE7C,CAAA;AAED,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"frame-limiter.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/frame-limiter.tsx"],"names":[],"mappings":"AAKA,KAAK,iBAAiB,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IACjE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,SAAI,GAAG,UAAU,CAwB5D;AAgBD,QAAA,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAoF7C,CAAA;AAED,eAAe,YAAY,CAAA"}
@@ -1,6 +1,35 @@
1
1
  import { useThree } from '@react-three/fiber';
2
- import { useLayoutEffect } from 'react';
2
+ import { useLayoutEffect, useRef } from 'react';
3
+ import { timeSpan } from '../../lib/perf-tracks';
3
4
  import useViewer from '../../store/use-viewer';
5
+ /**
6
+ * Keeps R3F's manual clock monotonic while each limiter effect owns a fresh
7
+ * wall-time baseline. The first rAF sample establishes that baseline instead
8
+ * of treating the browser's process uptime as elapsed frame time.
9
+ */
10
+ export function createFrameClock(initialTime = 0) {
11
+ let frameTime = initialTime;
12
+ let previousWallTime = null;
13
+ return {
14
+ sample(wallTimeMs, intervalMs) {
15
+ if (previousWallTime === null) {
16
+ previousWallTime = wallTimeMs;
17
+ return null;
18
+ }
19
+ const elapsedMs = wallTimeMs - previousWallTime;
20
+ if (elapsedMs < intervalMs)
21
+ return null;
22
+ const remainderMs = elapsedMs % intervalMs;
23
+ frameTime += (elapsedMs - remainderMs) / 1000;
24
+ previousWallTime = wallTimeMs - remainderMs;
25
+ return frameTime;
26
+ },
27
+ step(seconds) {
28
+ frameTime += seconds;
29
+ return frameTime;
30
+ },
31
+ };
32
+ }
4
33
  // `?disable=draw` (see post-processing.tsx): the page renders no real frames,
5
34
  // and Chromium's no-damage scheduler then throttles requestAnimationFrame to
6
35
  // 1Hz on Linux (measured in the headless bake worker — every useFrame system
@@ -11,23 +40,23 @@ const DRAW_DISABLED = typeof window !== 'undefined' &&
11
40
  new Set((new URLSearchParams(window.location.search).get('disable') ?? '')
12
41
  .split(',')
13
42
  .map((s) => s.trim())).has('draw');
14
- const FrameLimiter = ({ fps = 50 }) => {
43
+ const FrameLimiter = ({ fps = 50, paused = false }) => {
15
44
  const { advance, set, frameloop: initFrameloop } = useThree();
45
+ const nextFrameTimeRef = useRef(0);
16
46
  const renderer = useThree((state) => state.gl);
17
47
  const size = useThree((state) => state.size);
18
48
  const dpr = useThree((state) => state.viewport.dpr);
19
49
  // Fully covered canvas (e.g. studio gallery) → stop advancing frames
20
50
  const renderPaused = useViewer((s) => s.renderPaused);
21
51
  useLayoutEffect(() => {
22
- if (renderPaused)
52
+ if (renderPaused || paused)
23
53
  return;
24
- let elapsed = 0;
25
- let then = 0;
26
- let i = 0;
54
+ const clock = createFrameClock(nextFrameTimeRef.current);
27
55
  let raf = null;
28
56
  let timer = null;
29
57
  let sizeSynced = false;
30
- const interval = 1000 / fps;
58
+ const effectiveFps = Number.isFinite(fps) && fps > 0 ? fps : 50;
59
+ const interval = 1000 / effectiveFps;
31
60
  function syncSize() {
32
61
  if (sizeSynced)
33
62
  return;
@@ -38,17 +67,17 @@ const FrameLimiter = ({ fps = 50 }) => {
38
67
  function tick(t) {
39
68
  raf = requestAnimationFrame(tick);
40
69
  syncSize();
41
- elapsed = t - then;
42
- if (elapsed > interval) {
43
- advance(i);
44
- i += elapsed / 1000 - (elapsed % interval) / 1000;
45
- then = t - (elapsed % interval);
46
- }
70
+ const frameTime = clock.sample(t, interval);
71
+ if (frameTime === null)
72
+ return;
73
+ nextFrameTimeRef.current = frameTime;
74
+ timeSpan('frame-cpu', () => advance(frameTime));
47
75
  }
48
76
  function kick() {
49
77
  syncSize();
50
- i += 1 / 1000;
51
- advance(i);
78
+ const frameTime = clock.step(1 / 1000);
79
+ nextFrameTimeRef.current = frameTime;
80
+ timeSpan('frame-cpu', () => advance(frameTime));
52
81
  }
53
82
  function onVisibilityChange() {
54
83
  if (document.visibilityState === 'visible')
@@ -58,8 +87,9 @@ const FrameLimiter = ({ fps = 50 }) => {
58
87
  set({ frameloop: 'never' });
59
88
  if (DRAW_DISABLED) {
60
89
  timer = setInterval(() => {
61
- i += interval / 1000;
62
- advance(i);
90
+ const frameTime = clock.step(interval / 1000);
91
+ nextFrameTimeRef.current = frameTime;
92
+ timeSpan('frame-cpu', () => advance(frameTime));
63
93
  }, interval);
64
94
  }
65
95
  else {
@@ -84,7 +114,18 @@ const FrameLimiter = ({ fps = 50 }) => {
84
114
  window.removeEventListener('pageshow', kick);
85
115
  set({ frameloop: initFrameloop });
86
116
  };
87
- }, [advance, dpr, fps, initFrameloop, renderPaused, renderer, set, size.height, size.width]);
117
+ }, [
118
+ advance,
119
+ dpr,
120
+ fps,
121
+ initFrameloop,
122
+ paused,
123
+ renderPaused,
124
+ renderer,
125
+ set,
126
+ size.height,
127
+ size.width,
128
+ ]);
88
129
  return null;
89
130
  };
90
131
  export default FrameLimiter;
@@ -46,6 +46,19 @@ interface ViewerProps {
46
46
  * 180-frame cap in ~3.6s — shorter than a cold item-model download).
47
47
  */
48
48
  sceneReadyMaxWaitMs?: number;
49
+ /**
50
+ * Frame cap for the render loop, in frames per second. Defaults to 50, the
51
+ * value the viewer has always used.
52
+ *
53
+ * The viewer runs `frameloop="never"` and advances frames itself through
54
+ * `<FrameLimiter>`, so this cap is the only thing setting the cadence and a
55
+ * host cannot raise it from the outside. Hosts that animate the scene on
56
+ * their own clock — a timeline scrubbing node transforms, a walkthrough
57
+ * camera — are pinned to it and cannot reach display refresh, which reads as
58
+ * judder against a 60Hz+ monitor. Raise it for those; lower it to spare the
59
+ * GPU on a passive or background canvas.
60
+ */
61
+ maxFps?: number;
49
62
  /**
50
63
  * Skip the TSL post-processing pipeline (SSGI/denoise/ink/outline) and render
51
64
  * the scene directly. For headless/capture surfaces (the bake page) where
@@ -54,6 +67,8 @@ interface ViewerProps {
54
67
  * `?disable=postFx` diagnostic URL flag, but host-controlled.
55
68
  */
56
69
  disablePostFx?: boolean;
70
+ /** Keep the mounted renderer/context warm without advancing scene frames. */
71
+ renderPaused?: boolean;
57
72
  }
58
73
  /** Imperative handle exposed via `ref` on `<Viewer>`. */
59
74
  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,EAKf,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;AAGrE,OAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAQtE,OAAuB,EAAwB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAU1F,OAAO,QAAQ,oBAAoB,CAAC;IAOlC,UAAU,aAAa;QACrB,qBAAqB,EAAE,YAAY,CAAC,OAAO,KAAK,CAAC,qBAAqB,CAAC,CAAA;KACxE;CACF;AAuSD,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;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;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,sGA4NV,CAAA;AAkBF,eAAe,MAAM,CAAA"}
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,27 +1,36 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { nodeRegistry, StairOpeningSystem, sceneRegistry, useScene, } from '@pascal-app/core';
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';
4
4
  import { Canvas, extend, useFrame, useThree } from '@react-three/fiber';
5
5
  import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
6
6
  import * as THREE from 'three/webgpu';
7
7
  import { hasDrawableGeometry } from '../../lib/drawable-geometry';
8
- import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf';
8
+ import { PERF_OVERLAY_ENABLED } from '../../lib/gpu-perf';
9
9
  import { applyIsolation, clearIsolation } from '../../lib/isolation';
10
10
  import { ensureKtx2Support } from '../../lib/ktx2-loader';
11
+ import { choosePointerEvents } from '../../lib/pointer-events';
12
+ import { initializeGpuRenderer } from '../../lib/renderer-capability';
11
13
  import { getSceneTheme } from '../../lib/scene-themes';
12
14
  import { installTextureNodeNullGuard } from '../../lib/texture-node-guard';
13
15
  import useViewer, {} from '../../store/use-viewer';
14
16
  import { FloorElevationSystem } from '../../systems/floor-elevation/floor-elevation-system';
15
17
  import { GeometrySystem } from '../../systems/geometry/geometry-system';
18
+ import { PerfActionSettleSystem } from '../../systems/perf-action-settle/perf-action-settle-system';
19
+ import { subscribeWallBuildInteractions } from '../../systems/wall/wall-build-lifecycle';
16
20
  import { ErrorBoundary } from '../error-boundary';
17
21
  import { SceneRenderer } from '../renderers/scene-renderer';
22
+ import { BATCH_SPIKE_ENABLED, BatchedMeshSpike } from './batched-mesh-spike';
18
23
  import FrameLimiter from './frame-limiter';
19
24
  import { Lights } from './lights';
20
25
  import { PerfMonitor } from './perf-monitor';
26
+ import { PerfPanel } from './perf-panel';
27
+ import { PointerRaycastLayers } from './pointer-raycast-layers';
21
28
  import PostProcessing, { DEFAULT_HOVER_STYLES } from './post-processing';
22
29
  import { RegisteredSystems } from './registered-systems';
30
+ import { useSceneAtmosphere } from './scene-atmosphere';
23
31
  import { SceneBvh } from './scene-bvh';
24
32
  import { SelectionManager } from './selection-manager';
33
+ import { UnsupportedGpuViewerFallback } from './unsupported-gpu-fallback';
25
34
  import { ViewerCamera } from './viewer-camera';
26
35
  // Must be in place before any node material builds — a null texture pulled by
27
36
  // a shared override-material pass otherwise kills the render pass outright.
@@ -56,27 +65,6 @@ const DIRTY_BUILD_KINDS = new Set([
56
65
  'window',
57
66
  ]);
58
67
  const warnedEmptyDraw = process.env.NODE_ENV === 'production' ? null : new WeakSet();
59
- function canCreateWebGLContext() {
60
- if (typeof document === 'undefined')
61
- return false;
62
- const canvas = document.createElement('canvas');
63
- try {
64
- return Boolean(canvas.getContext('webgl2') ?? canvas.getContext('webgl'));
65
- }
66
- catch {
67
- return false;
68
- }
69
- }
70
- function canMountGpuViewer() {
71
- if (typeof window === 'undefined')
72
- return false;
73
- if (!('gpu' in navigator) && !canCreateWebGLContext())
74
- return false;
75
- return true;
76
- }
77
- function UnsupportedGpuViewerFallback() {
78
- return (_jsx("div", { className: "flex h-full min-h-64 w-full items-center justify-center bg-[#fafafa] p-6 text-center text-neutral-900", children: _jsxs("div", { className: "max-w-md rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm", children: [_jsx("h2", { className: "font-semibold text-lg", children: "3D viewer unavailable" }), _jsx("p", { className: "mt-2 text-neutral-600 text-sm", children: "This browser or environment does not expose WebGPU or WebGL, so Pascal cannot render the 3D scene here. Try opening the editor in a browser with hardware acceleration enabled." })] }) }));
79
- }
80
68
  /**
81
69
  * Renderer-level safety net against the empty-vertex-buffer crash.
82
70
  *
@@ -144,12 +132,20 @@ function GPUDeviceWatcher() {
144
132
  }
145
133
  function ToneMappingExposure() {
146
134
  const sceneTheme = useViewer((state) => state.sceneTheme);
135
+ const atmosphere = useSceneAtmosphere();
147
136
  const gl = useThree((state) => state.gl);
148
137
  const invalidate = useThree((state) => state.invalidate);
149
138
  useEffect(() => {
139
+ if (atmosphere)
140
+ return;
150
141
  gl.toneMappingExposure = getSceneTheme(sceneTheme).toneMappingExposure;
151
142
  invalidate();
152
- }, [gl, invalidate, sceneTheme]);
143
+ }, [atmosphere, gl, invalidate, sceneTheme]);
144
+ useFrame(() => {
145
+ if (!atmosphere)
146
+ return;
147
+ gl.toneMappingExposure = atmosphere.exposure;
148
+ }, -1);
153
149
  return null;
154
150
  }
155
151
  function hasPendingSceneBuildWork() {
@@ -230,7 +226,7 @@ function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, sceneReadyMaxWai
230
226
  }, 10);
231
227
  return null;
232
228
  }
233
- const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, renderContext = 'editor', transparent, defaultRender, isolate, sceneReadyKey, onSceneReadyChange, sceneReadyMaxWaitMs, disablePostFx = false, }, ref) {
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) {
234
230
  useEffect(() => {
235
231
  if (nodeRegistry.size === 0) {
236
232
  console.warn('[viewer] Node registry is empty. Install @pascal-app/nodes and call await loadPlugin(builtinPlugin) before mounting <Viewer>.');
@@ -253,16 +249,8 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
253
249
  clearIsolation();
254
250
  };
255
251
  }, [isolate]);
252
+ const [pointerEvents] = useState(() => choosePointerEvents());
256
253
  const [rendererInitFailed, setRendererInitFailed] = useState(false);
257
- // Capability detection runs after mount. We start optimistic (true) so the
258
- // server-rendered markup and the first client render agree (no hydration
259
- // mismatch); the effect flips it to false only on environments that expose
260
- // neither WebGPU nor WebGL.
261
- const [canMountViewer, setCanMountViewer] = useState(true);
262
- useEffect(() => {
263
- if (!canMountGpuViewer())
264
- setCanMountViewer(false);
265
- }, []);
266
254
  const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark');
267
255
  const transparentBackground = useViewer((state) => state.transparentBackground);
268
256
  // The shadows toggle drives `renderer.shadowMap.enabled` (via the Canvas
@@ -317,7 +305,7 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
317
305
  // fragment-shader cost down — saves another ~30% over 1.5x on high-DPI mobile.
318
306
  // Desktops (fine pointer) keep the original 1.5 cap.
319
307
  const maxDpr = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches ? 1.25 : 1.5;
320
- const showGpuFallback = !canMountViewer || rendererInitFailed;
308
+ const showGpuFallback = rendererInitFailed;
321
309
  // When we can't mount the GPU canvas, the SceneReadyTracker never mounts and
322
310
  // the host editor would otherwise wait on its scene-readiness timeout. Signal
323
311
  // readiness explicitly so the host can drop its loader immediately.
@@ -328,52 +316,56 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
328
316
  if (showGpuFallback) {
329
317
  return _jsx(UnsupportedGpuViewerFallback, {});
330
318
  }
331
- return (_jsxs(Canvas, { camera: { position: [50, 50, 50], fov: 50 }, className: `transition-colors duration-700 ${transparentBackground ? 'bg-transparent' : isDark ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`, dpr: [1, maxDpr], frameloop: "never", gl: ((props) => {
332
- const canvas = props.canvas;
333
- const cached = canvas ? WEBGPU_RENDERER_CACHE.get(canvas) : undefined;
334
- if (cached)
335
- return cached;
336
- const promise = (async () => {
337
- try {
338
- const renderer = new THREE.WebGPURenderer({ ...props, alpha: true });
339
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
340
- renderer.toneMappingExposure = getSceneTheme(useViewer.getState().sceneTheme).toneMappingExposure;
341
- await renderer.init();
342
- installEmptyDrawGuard(renderer);
343
- return renderer;
344
- }
345
- catch (err) {
346
- // Drop the failed promise from the cache so a future Canvas
347
- // mount on the same DOM can retry instead of inheriting the
348
- // rejection forever.
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) => {
320
+ const canvas = props.canvas;
321
+ const cached = canvas ? WEBGPU_RENDERER_CACHE.get(canvas) : undefined;
322
+ if (cached)
323
+ return cached;
324
+ const promise = (async () => {
325
+ const result = await initializeGpuRenderer({
326
+ // Supplying `device` makes three skip its own `requestAdapter`,
327
+ // so R3F's `powerPreference` only reaches the GPU if we forward it.
328
+ powerPreference: props.powerPreference,
329
+ createRenderer: (backendParameters) => {
330
+ const renderer = new THREE.WebGPURenderer({
331
+ ...props,
332
+ ...backendParameters,
333
+ alpha: true,
334
+ // Allocates the backend's timestamp query pool so
335
+ // `resolveTimestampsAsync()` can report real GPU render-pass
336
+ // time (post-processing.tsx). The backend self-disables it
337
+ // when the device lacks 'timestamp-query'.
338
+ trackTimestamp: PERF_OVERLAY_ENABLED,
339
+ });
340
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
341
+ renderer.toneMappingExposure = getSceneTheme(useViewer.getState().sceneTheme).toneMappingExposure;
342
+ return renderer;
343
+ },
344
+ });
345
+ if (result.status === 'ready') {
346
+ installEmptyDrawGuard(result.renderer);
347
+ return result.renderer;
348
+ }
349
+ if (canvas)
350
+ WEBGPU_RENDERER_CACHE.delete(canvas);
351
+ console.error('[viewer] WebGPURenderer init failed', result.error);
352
+ setRendererInitFailed(true);
353
+ // Never settles on purpose. Rejecting is what produced
354
+ // MONOREPO-EDITOR-59: R3F awaits this inside its own configure()
355
+ // with no catch, so a rejection surfaces as an unhandled rejection.
356
+ // Resolving is worse still — R3F would call render() on a renderer
357
+ // that has no context. The state update above unmounts this Canvas,
358
+ // which is what releases the pending configure().
359
+ return new Promise(() => undefined);
360
+ })();
349
361
  if (canvas)
350
- WEBGPU_RENDERER_CACHE.delete(canvas);
351
- console.error('[viewer] WebGPURenderer init failed', err);
352
- setRendererInitFailed(true);
353
- throw err;
354
- }
355
- })();
356
- if (canvas)
357
- WEBGPU_RENDERER_CACHE.set(canvas, promise);
358
- return promise;
359
- }), resize: {
360
- debounce: 100,
361
- }, shadows: {
362
- type: THREE.PCFShadowMap,
363
- enabled: shadowsEnabled,
364
- }, children: [_jsx(FrameLimiter, { fps: 50 }), _jsx(ViewerCamera, {}), _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(RegisteredSystems, {}), _jsx(PostProcessing, { disablePostFx: disablePostFx, hoverStyles: hoverStyles }), selectionManager === 'default' && _jsx(SelectionManager, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfMonitor, {}), children] })] }));
362
+ WEBGPU_RENDERER_CACHE.set(canvas, promise);
363
+ return promise;
364
+ }), resize: {
365
+ debounce: 100,
366
+ }, shadows: {
367
+ type: THREE.PCFShadowMap,
368
+ 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] })] })] }));
365
370
  });
366
- const DebugRenderer = () => {
367
- useFrame(({ gl, scene, camera }) => {
368
- const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0;
369
- gl.render(scene, camera);
370
- if (PERF_OVERLAY_ENABLED) {
371
- const queue = gl.backend?.device?.queue;
372
- queue?.onSubmittedWorkDone?.().then(() => {
373
- pushGpuSample(performance.now() - submittedAt);
374
- });
375
- }
376
- });
377
- return null;
378
- };
379
371
  export default Viewer;
@@ -1 +1 @@
1
- {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AA4EA,wBAAgB,MAAM,gCAgPrB"}
1
+ {"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AA6EA,wBAAgB,MAAM,gCAqQrB"}