@pascal-app/viewer 0.8.0 → 0.9.1

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 (194) hide show
  1. package/dist/components/renderers/node-renderer.d.ts +1 -1
  2. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.js +38 -22
  4. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  5. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  6. package/dist/components/renderers/parametric-node-renderer.js +34 -0
  7. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  8. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  9. package/dist/components/viewer/ground-occluder.d.ts +1 -1
  10. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  11. package/dist/components/viewer/ground-occluder.js +12 -24
  12. package/dist/components/viewer/index.d.ts +37 -1
  13. package/dist/components/viewer/index.d.ts.map +1 -1
  14. package/dist/components/viewer/index.js +186 -63
  15. package/dist/components/viewer/lights.d.ts +1 -1
  16. package/dist/components/viewer/lights.d.ts.map +1 -1
  17. package/dist/components/viewer/lights.js +170 -43
  18. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  19. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  20. package/dist/components/viewer/perf-monitor.js +99 -11
  21. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  22. package/dist/components/viewer/post-processing.js +231 -54
  23. package/dist/components/viewer/registered-systems.d.ts +12 -0
  24. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  25. package/dist/components/viewer/registered-systems.js +47 -0
  26. package/dist/components/viewer/scene-bvh.d.ts +17 -0
  27. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  28. package/dist/components/viewer/scene-bvh.js +94 -0
  29. package/dist/components/viewer/selection-manager.d.ts +1 -1
  30. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  31. package/dist/components/viewer/selection-manager.js +20 -5
  32. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  33. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  34. package/dist/components/viewer/walkthrough-controls.d.ts +1 -1
  35. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  36. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  37. package/dist/hooks/use-node-events.d.ts +5 -73
  38. package/dist/hooks/use-node-events.d.ts.map +1 -1
  39. package/dist/hooks/use-node-events.js +19 -7
  40. package/dist/index.d.ts +36 -4
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +67 -3
  43. package/dist/lib/csg-utils.d.ts +16 -0
  44. package/dist/lib/csg-utils.d.ts.map +1 -0
  45. package/dist/lib/csg-utils.js +33 -0
  46. package/dist/lib/drawable-geometry.d.ts +21 -0
  47. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  48. package/dist/lib/drawable-geometry.js +22 -0
  49. package/dist/lib/edge-style.d.ts +3 -0
  50. package/dist/lib/edge-style.d.ts.map +1 -0
  51. package/dist/lib/edge-style.js +11 -0
  52. package/dist/lib/gpu-perf.d.ts +4 -0
  53. package/dist/lib/gpu-perf.d.ts.map +1 -0
  54. package/dist/lib/gpu-perf.js +23 -0
  55. package/dist/lib/ink-edges.d.ts +9 -0
  56. package/dist/lib/ink-edges.d.ts.map +1 -0
  57. package/dist/lib/ink-edges.js +43 -0
  58. package/dist/lib/isolation.d.ts +39 -0
  59. package/dist/lib/isolation.d.ts.map +1 -0
  60. package/dist/lib/isolation.js +103 -0
  61. package/dist/lib/layers.d.ts +18 -0
  62. package/dist/lib/layers.d.ts.map +1 -1
  63. package/dist/lib/layers.js +18 -0
  64. package/dist/lib/materials.d.ts +25 -15
  65. package/dist/lib/materials.d.ts.map +1 -1
  66. package/dist/lib/materials.js +277 -62
  67. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  68. package/dist/lib/merged-outline-node.js +7 -0
  69. package/dist/lib/polygon-union.d.ts +4 -0
  70. package/dist/lib/polygon-union.d.ts.map +1 -0
  71. package/dist/lib/polygon-union.js +289 -0
  72. package/dist/lib/polygon-union.test.d.ts +2 -0
  73. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  74. package/dist/lib/polygon-union.test.js +105 -0
  75. package/dist/lib/scene-themes.d.ts +29 -0
  76. package/dist/lib/scene-themes.d.ts.map +1 -0
  77. package/dist/lib/scene-themes.js +193 -0
  78. package/dist/store/use-viewer.d.ts +42 -4
  79. package/dist/store/use-viewer.d.ts.map +1 -1
  80. package/dist/store/use-viewer.js +26 -3
  81. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  82. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  83. package/dist/systems/ceiling/ceiling-system.js +62 -12
  84. package/dist/systems/door/door-system.d.ts.map +1 -1
  85. package/dist/systems/door/door-system.js +373 -22
  86. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  87. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  88. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  89. package/dist/systems/fence/fence-system.d.ts +3 -0
  90. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  91. package/dist/systems/fence/fence-system.js +9 -5
  92. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  93. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  94. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  95. package/dist/systems/geometry/geometry-system.d.ts +34 -0
  96. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  97. package/dist/systems/geometry/geometry-system.js +279 -0
  98. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  99. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  100. package/dist/systems/item/item-system.d.ts +7 -0
  101. package/dist/systems/item/item-system.d.ts.map +1 -1
  102. package/dist/systems/item/item-system.js +8 -11
  103. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  104. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  105. package/dist/systems/item-light/item-light-system.js +18 -5
  106. package/dist/systems/roof/roof-materials.d.ts +3 -2
  107. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  108. package/dist/systems/roof/roof-materials.js +38 -14
  109. package/dist/systems/roof/roof-system.d.ts +27 -0
  110. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  111. package/dist/systems/roof/roof-system.js +396 -59
  112. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  113. package/dist/systems/slab/slab-system.js +167 -49
  114. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  115. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  116. package/dist/systems/slab/slab-system.test.js +62 -0
  117. package/dist/systems/stair/stair-materials.d.ts +4 -3
  118. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  119. package/dist/systems/stair/stair-materials.js +30 -15
  120. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  121. package/dist/systems/stair/stair-system.js +50 -61
  122. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  123. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  124. package/dist/systems/surface-hole-geometry.js +4 -0
  125. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  126. package/dist/systems/wall/wall-cutout.js +18 -2
  127. package/dist/systems/wall/wall-materials.d.ts +4 -3
  128. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  129. package/dist/systems/wall/wall-materials.js +52 -28
  130. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  131. package/dist/systems/wall/wall-system.js +171 -26
  132. package/dist/systems/window/window-system.d.ts.map +1 -1
  133. package/dist/systems/window/window-system.js +71 -7
  134. package/package.json +3 -4
  135. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  136. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  137. package/dist/components/renderers/building/building-renderer.js +0 -11
  138. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  139. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  140. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  141. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  142. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  143. package/dist/components/renderers/column/column-renderer.js +0 -597
  144. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  145. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  146. package/dist/components/renderers/door/door-renderer.js +0 -16
  147. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  148. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  149. package/dist/components/renderers/fence/fence-renderer.js +0 -15
  150. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  151. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  152. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  153. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  154. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  155. package/dist/components/renderers/item/item-renderer.js +0 -172
  156. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  157. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  158. package/dist/components/renderers/level/level-renderer.js +0 -11
  159. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  160. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  161. package/dist/components/renderers/roof/roof-materials.js +0 -16
  162. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  163. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  164. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  165. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  166. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  167. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  168. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  169. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  170. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  171. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  172. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  173. package/dist/components/renderers/site/site-renderer.js +0 -106
  174. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  175. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  176. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  177. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  178. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  179. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  180. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  181. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  182. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  183. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  184. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  185. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
  186. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  187. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  188. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  189. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  190. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  191. package/dist/components/renderers/window/window-renderer.js +0 -21
  192. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  193. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  194. package/dist/components/renderers/zone/zone-renderer.js +0 -181
@@ -1,74 +1,201 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { sceneRegistry } from '@pascal-app/core';
2
3
  import { useFrame } from '@react-three/fiber';
3
4
  import { useMemo, useRef } from 'react';
4
5
  import * as THREE from 'three/webgpu';
6
+ import { getSceneTheme } from '../../lib/scene-themes';
5
7
  import useViewer from '../../store/use-viewer';
8
+ // Diagnostic toggle: `?disable=shadows` skips the shadow-map render pass
9
+ // (which doubles draw calls for every shadow-casting mesh) so you can
10
+ // isolate how much of the baseline GPU cost is shadows vs. raw geometry.
11
+ const SHADOWS_DISABLED = typeof window !== 'undefined' &&
12
+ new Set((new URLSearchParams(window.location.search).get('disable') ?? '')
13
+ .split(',')
14
+ .map((s) => s.trim())).has('shadows');
15
+ // Shadow darkness for the bright key lights (themes drive most lights past
16
+ // intensity 1). The aesthetic prototype runs these near-black (≈1.0); this is a
17
+ // deliberate middle ground — present, but not the heavy contact shadow there.
18
+ const MAX_SHADOW_INTENSITY = 0.55;
19
+ // Shadow frustum framing. The frustum is fit to the BUILDING geometry (not the
20
+ // camera): we union the bounds of all registered scene nodes, fit a sphere, and
21
+ // size the directional light's ortho shadow camera to that sphere plus a margin.
22
+ // This keeps shadows anchored to the building and a bit of surrounding ground no
23
+ // matter how the user zooms or pans — fixing the previous camera-following
24
+ // behaviour that fell apart when zoomed out (frustum too small) or zoomed into
25
+ // an empty corner (frustum centred on nothing).
26
+ //
27
+ // `site` nodes (the ground/site plane, which can be arbitrarily large) are
28
+ // excluded so they don't blow the frustum up to cover the whole lot.
29
+ const SHADOW_EXCLUDED_TYPES = ['site'];
30
+ // How often (seconds) to recompute building bounds. Bounds only change while
31
+ // editing, so we throttle the (subtree-walking) union instead of doing it every
32
+ // frame.
33
+ const BOUNDS_REFRESH_INTERVAL = 0.4;
34
+ // Extra coverage around the building bounds — the "and a bit nearby" margin so
35
+ // shadows don't get clipped right at the walls. Scales with building size.
36
+ const SHADOW_MARGIN_SCALE = 1.15;
37
+ const SHADOW_MARGIN = 3;
38
+ // Gap between the building bounds sphere and the light / near plane.
39
+ const SHADOW_BACKOFF = 10;
40
+ // Fallback radius when the scene has no building geometry yet (empty scene).
41
+ const SHADOW_FALLBACK_RADIUS = 30;
6
42
  export function Lights() {
7
- const theme = useViewer((state) => state.theme);
8
- const isDark = theme === 'dark';
9
- const light1Ref = useRef(null);
43
+ const sceneTheme = useViewer((state) => state.sceneTheme);
44
+ const theme = getSceneTheme(sceneTheme);
45
+ const shadows = useViewer((state) => state.shadows);
46
+ const lightRefs = useRef([]);
10
47
  const shadowCamera = useRef(null);
11
- const shadowCameraSize = 50; // The "area" around the camera to shadow
12
- const light2Ref = useRef(null);
13
- const light3Ref = useRef(null);
48
+ // Initial ortho half-size; overridden each refresh to fit the building.
49
+ const shadowCameraSize = 50;
50
+ // Building bounds the shadow frustum is fit to, recomputed on an interval.
51
+ const shadowFocus = useRef(new THREE.Vector3()); // sphere centre
52
+ const shadowRadius = useRef(SHADOW_FALLBACK_RADIUS); // sphere radius
53
+ const shadowDir = useRef(new THREE.Vector3()); // scratch: per-light direction
54
+ const boundsBox = useRef(new THREE.Box3()); // scratch: union AABB
55
+ const boundsSphere = useRef(new THREE.Sphere()); // scratch: fitted sphere
56
+ const lastBoundsTime = useRef(-1); // last refresh timestamp (-1 = never)
57
+ const hemiRef = useRef(null);
14
58
  const ambientRef = useRef(null);
15
59
  const initialized = useRef(false);
60
+ const lightTargets = useRef([]);
16
61
  const targets = useMemo(() => ({
17
- l1Color: new THREE.Color(),
18
- l2Color: new THREE.Color(),
19
- l3Color: new THREE.Color(),
62
+ hemiSky: new THREE.Color(),
63
+ hemiGround: new THREE.Color(),
20
64
  ambColor: new THREE.Color(),
21
65
  }), []);
22
- useFrame((_, delta) => {
66
+ useFrame((state, delta) => {
23
67
  // clamp delta to avoid huge jumps on tab switch
24
68
  const dt = Math.min(delta, 0.1) * 4;
25
- if (!initialized.current) {
26
- if (light1Ref.current) {
27
- light1Ref.current.intensity = isDark ? 0.8 : 4;
28
- light1Ref.current.color.set(isDark ? '#e0e5ff' : '#ffffff');
29
- if (light1Ref.current.shadow)
30
- light1Ref.current.shadow.intensity = isDark ? 0.8 : 0.4;
69
+ // Fit each shadow-casting light's frustum to the BUILDING geometry rather
70
+ // than the camera. We refresh the union bounds on an interval (cheap enough,
71
+ // and bounds only change while editing), fit a sphere, and size + place the
72
+ // ortho shadow camera so the building (plus a margin) is fully covered from
73
+ // the light's direction. The light DIRECTION stays exactly as the theme
74
+ // specifies; only its position/distance and the frustum extents change.
75
+ if (shadows) {
76
+ const now = state.clock.elapsedTime;
77
+ if (now - lastBoundsTime.current >= BOUNDS_REFRESH_INTERVAL) {
78
+ lastBoundsTime.current = now;
79
+ const box = boundsBox.current.makeEmpty();
80
+ for (const [id, obj] of sceneRegistry.nodes) {
81
+ if (SHADOW_EXCLUDED_TYPES.some((t) => sceneRegistry.byType[t].has(id)))
82
+ continue;
83
+ box.expandByObject(obj);
84
+ }
85
+ box.getBoundingSphere(boundsSphere.current);
86
+ const center = boundsSphere.current.center;
87
+ const radius = boundsSphere.current.radius;
88
+ // Empty scene OR a node with a NaN position/geometry poisoning the union
89
+ // box: fall back to the origin with a default radius. The directional
90
+ // light's position is derived from `focus`, so a single non-finite mesh
91
+ // must NOT be allowed to make `focus`/`radius` NaN — that breaks every
92
+ // shadow-casting light's position and renders the whole scene black.
93
+ const finiteBounds = !box.isEmpty() &&
94
+ Number.isFinite(center.x) &&
95
+ Number.isFinite(center.y) &&
96
+ Number.isFinite(center.z) &&
97
+ Number.isFinite(radius);
98
+ if (finiteBounds) {
99
+ shadowFocus.current.copy(center);
100
+ shadowRadius.current = radius;
101
+ }
102
+ else {
103
+ shadowFocus.current.set(0, 0, 0);
104
+ shadowRadius.current = SHADOW_FALLBACK_RADIUS;
105
+ }
31
106
  }
32
- if (light2Ref.current) {
33
- light2Ref.current.intensity = isDark ? 0.2 : 0.75;
34
- light2Ref.current.color.set(isDark ? '#8090ff' : '#ffffff');
107
+ const focus = shadowFocus.current;
108
+ // Ortho half-extent: the building sphere plus a proportional margin.
109
+ const size = shadowRadius.current * SHADOW_MARGIN_SCALE + SHADOW_MARGIN;
110
+ // Park the light just outside the sphere so the near plane stays positive
111
+ // and the whole building fits between near and far along the light axis.
112
+ const distance = size + SHADOW_BACKOFF;
113
+ const near = SHADOW_BACKOFF;
114
+ const far = distance + size;
115
+ for (let index = 0; index < theme.lights.length; index++) {
116
+ const config = theme.lights[index];
117
+ const light = lightRefs.current[index];
118
+ if (!(config?.castShadow && light))
119
+ continue;
120
+ const [ox, oy, oz] = config.position;
121
+ const dir = shadowDir.current.set(ox, oy, oz);
122
+ if (dir.lengthSq() === 0)
123
+ dir.set(0, 1, 0);
124
+ dir.normalize().multiplyScalar(distance);
125
+ light.position.set(focus.x + dir.x, focus.y + dir.y, focus.z + dir.z);
126
+ light.target.position.copy(focus);
127
+ light.target.updateMatrixWorld();
128
+ // Resize the ortho frustum to the fitted bounds. The shadow camera is
129
+ // the <orthographicCamera attach="shadow-camera"> below.
130
+ const cam = light.shadow?.camera;
131
+ if (cam) {
132
+ cam.left = -size;
133
+ cam.right = size;
134
+ cam.top = size;
135
+ cam.bottom = -size;
136
+ cam.near = near;
137
+ cam.far = far;
138
+ cam.updateProjectionMatrix();
139
+ }
35
140
  }
36
- if (light3Ref.current) {
37
- light3Ref.current.intensity = isDark ? 0.3 : 1;
38
- light3Ref.current.color.set(isDark ? '#a0b0ff' : '#ffffff');
141
+ }
142
+ if (!initialized.current) {
143
+ for (let index = 0; index < theme.lights.length; index++) {
144
+ const config = theme.lights[index];
145
+ const light = lightRefs.current[index];
146
+ if (!(config && light))
147
+ continue;
148
+ light.intensity = config.intensity;
149
+ light.color.set(config.color);
150
+ if (config.castShadow && light.shadow) {
151
+ light.shadow.intensity = config.intensity <= 1 ? config.intensity : MAX_SHADOW_INTENSITY;
152
+ }
153
+ }
154
+ if (hemiRef.current && theme.hemi) {
155
+ hemiRef.current.intensity = theme.hemi.intensity;
156
+ hemiRef.current.color.set(theme.hemi.sky);
157
+ hemiRef.current.groundColor.set(theme.hemi.ground);
39
158
  }
40
159
  if (ambientRef.current) {
41
- ambientRef.current.intensity = isDark ? 0.15 : 0.5;
42
- ambientRef.current.color.set(isDark ? '#a0b0ff' : '#ffffff');
160
+ ambientRef.current.intensity = theme.ambient.intensity;
161
+ ambientRef.current.color.set(theme.ambient.color);
43
162
  }
44
163
  initialized.current = true;
45
164
  return;
46
165
  }
47
- if (light1Ref.current) {
48
- light1Ref.current.intensity = THREE.MathUtils.lerp(light1Ref.current.intensity, isDark ? 0.8 : 4, dt);
49
- targets.l1Color.set(isDark ? '#e0e5ff' : '#ffffff');
50
- light1Ref.current.color.lerp(targets.l1Color, dt);
51
- if (light1Ref.current.shadow) {
52
- if (light1Ref.current.shadow.intensity !== undefined) {
53
- light1Ref.current.shadow.intensity = THREE.MathUtils.lerp(light1Ref.current.shadow.intensity, isDark ? 0.8 : 0.4, dt);
166
+ for (let index = 0; index < theme.lights.length; index++) {
167
+ const config = theme.lights[index];
168
+ const light = lightRefs.current[index];
169
+ if (!(config && light))
170
+ continue;
171
+ light.intensity = THREE.MathUtils.lerp(light.intensity, config.intensity, dt);
172
+ let target = lightTargets.current[index];
173
+ if (!target) {
174
+ target = new THREE.Color();
175
+ lightTargets.current[index] = target;
176
+ }
177
+ target.set(config.color);
178
+ light.color.lerp(target, dt);
179
+ if (config.castShadow && light.shadow) {
180
+ if (light.shadow.intensity !== undefined) {
181
+ light.shadow.intensity = THREE.MathUtils.lerp(light.shadow.intensity, config.intensity <= 1 ? config.intensity : MAX_SHADOW_INTENSITY, dt);
54
182
  }
55
183
  }
56
184
  }
57
- if (light2Ref.current) {
58
- light2Ref.current.intensity = THREE.MathUtils.lerp(light2Ref.current.intensity, isDark ? 0.2 : 0.75, dt);
59
- targets.l2Color.set(isDark ? '#8090ff' : '#ffffff');
60
- light2Ref.current.color.lerp(targets.l2Color, dt);
61
- }
62
- if (light3Ref.current) {
63
- light3Ref.current.intensity = THREE.MathUtils.lerp(light3Ref.current.intensity, isDark ? 0.3 : 1, dt);
64
- targets.l3Color.set(isDark ? '#a0b0ff' : '#ffffff');
65
- light3Ref.current.color.lerp(targets.l3Color, dt);
185
+ if (hemiRef.current && theme.hemi) {
186
+ hemiRef.current.intensity = THREE.MathUtils.lerp(hemiRef.current.intensity, theme.hemi.intensity, dt);
187
+ targets.hemiSky.set(theme.hemi.sky);
188
+ hemiRef.current.color.lerp(targets.hemiSky, dt);
189
+ targets.hemiGround.set(theme.hemi.ground);
190
+ hemiRef.current.groundColor.lerp(targets.hemiGround, dt);
66
191
  }
67
192
  if (ambientRef.current) {
68
- ambientRef.current.intensity = THREE.MathUtils.lerp(ambientRef.current.intensity, isDark ? 0.15 : 0.5, dt);
69
- targets.ambColor.set(isDark ? '#a0b0ff' : '#ffffff');
193
+ ambientRef.current.intensity = THREE.MathUtils.lerp(ambientRef.current.intensity, theme.ambient.intensity, dt);
194
+ targets.ambColor.set(theme.ambient.color);
70
195
  ambientRef.current.color.lerp(targets.ambColor, dt);
71
196
  }
72
197
  });
73
- return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { castShadow: true, position: [10, 10, 10], ref: light1Ref, "shadow-bias": -0.002, "shadow-mapSize": [1024, 1024], "shadow-normalBias": 0.3, "shadow-radius": 3, children: _jsx("orthographicCamera", { attach: "shadow-camera", bottom: -shadowCameraSize, far: 100, left: -shadowCameraSize, near: 1, ref: shadowCamera, right: shadowCameraSize, top: shadowCameraSize }) }), _jsx("directionalLight", { position: [-10, 10, -10], ref: light2Ref }), _jsx("directionalLight", { position: [-10, 10, 10], ref: light3Ref }), _jsx("ambientLight", { ref: ambientRef })] }));
198
+ return (_jsxs(_Fragment, { children: [theme.lights.map((light, index) => (_jsx("directionalLight", { castShadow: Boolean(light.castShadow) && !SHADOWS_DISABLED && shadows, position: light.position, ref: (ref) => {
199
+ lightRefs.current[index] = ref;
200
+ }, "shadow-bias": -0.002, "shadow-mapSize": [1024, 1024], "shadow-normalBias": 0.3, "shadow-radius": 1.5, children: light.castShadow && !SHADOWS_DISABLED && shadows ? (_jsx("orthographicCamera", { attach: "shadow-camera", bottom: -shadowCameraSize, far: 400, left: -shadowCameraSize, near: 1, ref: shadowCamera, right: shadowCameraSize, top: shadowCameraSize })) : null }, `${index}-${light.position.join(',')}`))), theme.hemi ? _jsx("hemisphereLight", { ref: hemiRef }) : null, _jsx("ambientLight", { ref: ambientRef })] }));
74
201
  }
@@ -1,2 +1,2 @@
1
- export declare const PerfMonitor: () => import("react/jsx-runtime").JSX.Element;
1
+ export declare const PerfMonitor: () => import("react").JSX.Element;
2
2
  //# sourceMappingURL=perf-monitor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"perf-monitor.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/perf-monitor.tsx"],"names":[],"mappings":"AAOA,eAAO,MAAM,WAAW,+CAoDvB,CAAA"}
1
+ {"version":3,"file":"perf-monitor.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/perf-monitor.tsx"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,mCAuIvB,CAAA"}
@@ -1,15 +1,48 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useScene } from '@pascal-app/core';
3
3
  import { Html } from '@react-three/drei';
4
- import { useFrame } from '@react-three/fiber';
5
- import { useRef, useState } from 'react';
4
+ import { useFrame, useThree } from '@react-three/fiber';
5
+ import { useEffect, useRef, useState } from 'react';
6
+ import { drainGpuSamples } from '../../lib/gpu-perf';
6
7
  const SAMPLE_INTERVAL = 0.5; // seconds between display updates
7
8
  export const PerfMonitor = () => {
8
- const [stats, setStats] = useState({ fps: 0, frameMs: 0, drawCalls: 0, triangles: 0, dirty: 0 });
9
+ const [stats, setStats] = useState({
10
+ fps: 0,
11
+ frameMs: 0,
12
+ gpuMs: 0,
13
+ gpuMaxMs: 0,
14
+ drawCalls: 0,
15
+ triangles: 0,
16
+ dirty: 0,
17
+ meshes: 0,
18
+ lines: 0,
19
+ sprites: 0,
20
+ lights: 0,
21
+ });
9
22
  const frameCount = useRef(0);
10
23
  const elapsed = useRef(0);
11
24
  const lastMs = useRef(0);
12
- useFrame(({ gl, clock }) => {
25
+ // Carry the previous tick's reading forward when no fresh samples arrive,
26
+ // so the display doesn't flicker to "—" on slow resolve windows.
27
+ const lastGpuMs = useRef(0);
28
+ const lastGpuMaxMs = useRef(0);
29
+ // Take ownership of info reset. The custom RenderPipeline.render() path
30
+ // we use in post-processing doesn't trigger three.js's automatic per-frame
31
+ // info reset, so calls/triangles accumulate across frames and the display
32
+ // shows lifetime totals. Disabling autoReset and explicitly resetting at
33
+ // each window gives true per-frame averages.
34
+ const gl = useThree((s) => s.gl);
35
+ useEffect(() => {
36
+ if (!gl?.info)
37
+ return;
38
+ const previousAutoReset = gl.info.autoReset;
39
+ gl.info.autoReset = false;
40
+ gl.info.reset();
41
+ return () => {
42
+ gl.info.autoReset = previousAutoReset;
43
+ };
44
+ }, [gl]);
45
+ useFrame(({ gl, scene, clock }) => {
13
46
  frameCount.current++;
14
47
  const now = clock.elapsedTime;
15
48
  const dt = now - elapsed.current;
@@ -17,10 +50,60 @@ export const PerfMonitor = () => {
17
50
  const fps = Math.round(frameCount.current / dt);
18
51
  const frameMs = lastMs.current;
19
52
  const info = gl.info;
20
- const drawCalls = info.render?.calls ?? 0;
21
- const triangles = info.render?.triangles ?? 0;
53
+ // calls/triangles have been accumulating since the last reset (start of
54
+ // window). Divide by frameCount to get a per-frame average.
55
+ const totalCalls = info.render?.calls ?? 0;
56
+ const totalTriangles = info.render?.triangles ?? 0;
57
+ const drawCalls = Math.round(totalCalls / Math.max(1, frameCount.current));
58
+ const triangles = totalTriangles / Math.max(1, frameCount.current);
59
+ info.reset();
22
60
  const dirty = useScene.getState().dirtyNodes.size;
23
- setStats({ fps, frameMs, drawCalls, triangles, dirty });
61
+ // Count visible drawables by type so we can match scene contents
62
+ // against the renderer's draw count and find hidden contributors.
63
+ let meshes = 0;
64
+ let lines = 0;
65
+ let sprites = 0;
66
+ let lights = 0;
67
+ scene.traverse((obj) => {
68
+ if (!obj.visible)
69
+ return;
70
+ if (obj.isMesh)
71
+ meshes++;
72
+ else if (obj.isLine || obj.isLineSegments || obj.isLineLoop)
73
+ lines++;
74
+ else if (obj.isSprite)
75
+ sprites++;
76
+ else if (obj.isLight)
77
+ lights++;
78
+ });
79
+ // GPU samples are pushed by post-processing.tsx after each pipeline
80
+ // render via device.queue.onSubmittedWorkDone(). We drain whatever
81
+ // has accumulated since the last tick.
82
+ const samples = drainGpuSamples();
83
+ if (samples.length > 0) {
84
+ let sum = 0;
85
+ let max = 0;
86
+ for (const s of samples) {
87
+ sum += s;
88
+ if (s > max)
89
+ max = s;
90
+ }
91
+ lastGpuMs.current = sum / samples.length;
92
+ lastGpuMaxMs.current = max;
93
+ }
94
+ setStats({
95
+ fps,
96
+ frameMs,
97
+ gpuMs: lastGpuMs.current,
98
+ gpuMaxMs: lastGpuMaxMs.current,
99
+ drawCalls,
100
+ triangles,
101
+ dirty,
102
+ meshes,
103
+ lines,
104
+ sprites,
105
+ lights,
106
+ });
24
107
  frameCount.current = 0;
25
108
  elapsed.current = now;
26
109
  }
@@ -35,8 +118,13 @@ export const PerfMonitor = () => {
35
118
  borderRadius: 6,
36
119
  padding: '6px 10px',
37
120
  whiteSpace: 'pre',
38
- }, children: `FPS ${stats.fps}
39
- DRAW ${stats.drawCalls}
40
- TRI ${(stats.triangles / 1000).toFixed(1)}k
41
- DIRTY ${stats.dirty}` }) }));
121
+ }, children: `FPS ${stats.fps}
122
+ GPU ${stats.gpuMs > 0 ? `${stats.gpuMs.toFixed(1)}ms (max ${stats.gpuMaxMs.toFixed(1)})` : '—'}
123
+ DRAW ${stats.drawCalls}
124
+ TRI ${(stats.triangles / 1000).toFixed(1)}k
125
+ DIRTY ${stats.dirty}
126
+ MESH ${stats.meshes}
127
+ LINE ${stats.lines}
128
+ SPRITE ${stats.sprites}
129
+ LIGHT ${stats.lights}` }) }));
42
130
  };
@@ -1 +1 @@
1
- {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA4BA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AAQD,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;AAiBD,QAAA,MAAM,oBAAoB,GAAI,kBAE3B;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B,SAyTA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AAgCA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AA0CD,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;AAiBD,QAAA,MAAM,oBAAoB,GAAI,kBAE3B;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B,SAgdA,CAAA;AAED,eAAe,oBAAoB,CAAA"}