rayzee 7.15.2 → 7.16.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.15.2",
3
+ "version": "7.16.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -775,11 +775,7 @@ export class PathTracerApp extends EventDispatcher {
775
775
  this.reset();
776
776
  this.cameraManager.currentCameraIndex = 0;
777
777
  this.dispatchEvent( eventPayload );
778
- this.dispatchEvent( {
779
- type: 'CamerasUpdated',
780
- cameras: this.cameraManager.cameras,
781
- cameraNames: this.cameraManager.getCameraNames()
782
- } );
778
+ this._dispatchCamerasUpdated();
783
779
 
784
780
  } finally {
785
781
 
@@ -1093,6 +1089,57 @@ export class PathTracerApp extends EventDispatcher {
1093
1089
 
1094
1090
  }
1095
1091
 
1092
+ // ═══════════════════════════════════════════════════════════════
1093
+ // Dynamic cameras (add / remove)
1094
+ // ═══════════════════════════════════════════════════════════════
1095
+
1096
+ /** The index of the currently active camera (0 = built-in default). */
1097
+ get currentCameraIndex() {
1098
+
1099
+ return this.cameraManager?.currentCameraIndex ?? 0;
1100
+
1101
+ }
1102
+
1103
+ /**
1104
+ * Snapshot the current view as a new named camera and switch to it.
1105
+ * @param {Object} [opts]
1106
+ * @param {string} [opts.name] - Display name (auto-generated otherwise).
1107
+ * @returns {number} The index of the newly added camera.
1108
+ */
1109
+ addCamera( { name } = {} ) {
1110
+
1111
+ const index = this.cameraManager.addCameraFromView( name );
1112
+ this.cameraManager.switchCamera( index );
1113
+ this._dispatchCamerasUpdated();
1114
+ return index;
1115
+
1116
+ }
1117
+
1118
+ /**
1119
+ * Remove a user-added camera by index. Built-in and model-embedded cameras
1120
+ * are protected. Falls back to the default camera if the active one is removed.
1121
+ * @param {number} index
1122
+ * @returns {boolean} true if a camera was removed.
1123
+ */
1124
+ removeCamera( index ) {
1125
+
1126
+ const removed = this.cameraManager.removeCamera( index );
1127
+ if ( removed ) this._dispatchCamerasUpdated();
1128
+ return removed;
1129
+
1130
+ }
1131
+
1132
+ /** Notify consumers that the camera list changed (names / count). */
1133
+ _dispatchCamerasUpdated() {
1134
+
1135
+ this.dispatchEvent( {
1136
+ type: 'CamerasUpdated',
1137
+ cameras: this.cameraManager.cameras,
1138
+ cameraNames: this.cameraManager.getCameraNames(),
1139
+ } );
1140
+
1141
+ }
1142
+
1096
1143
  // ═══════════════════════════════════════════════════════════════
1097
1144
  // BVH Refit (Animation)
1098
1145
  // ═══════════════════════════════════════════════════════════════
@@ -1874,6 +1921,8 @@ export class PathTracerApp extends EventDispatcher {
1874
1921
  onResize: () => this.onResize(),
1875
1922
  onReset: () => this.reset(),
1876
1923
  getSettings: ( k ) => this.settings.get( k ),
1924
+ // Per-camera DOF restore — silent + reset:false so switchCamera's own onReset() is the single reset.
1925
+ applySettings: ( updates ) => this.settings.setMany( updates, { silent: true, reset: false } ),
1877
1926
  } );
1878
1927
 
1879
1928
  // Auto-focus context — CameraManager stores it, reads it each frame
@@ -252,8 +252,9 @@ export class RenderSettings extends EventDispatcher {
252
252
  * @param {Object} updates - Key/value pairs
253
253
  * @param {Object} [options]
254
254
  * @param {boolean} [options.silent] - Suppress settingChanged events
255
+ * @param {boolean} [options.reset] - Override the routes' default reset behavior
255
256
  */
256
- setMany( updates, { silent } = {} ) {
257
+ setMany( updates, { silent, reset } = {} ) {
257
258
 
258
259
  let needsReset = false;
259
260
 
@@ -279,7 +280,8 @@ export class RenderSettings extends EventDispatcher {
279
280
 
280
281
  }
281
282
 
282
- if ( needsReset ) this._resetCallback?.();
283
+ const shouldReset = reset !== undefined ? reset : needsReset;
284
+ if ( shouldReset ) this._resetCallback?.();
283
285
 
284
286
  }
285
287
 
@@ -35,6 +35,9 @@ export class CameraManager extends EventDispatcher {
35
35
  this.cameras = [ this.camera ];
36
36
  this.currentCameraIndex = 0;
37
37
 
38
+ // Monotonic counter for naming user-added cameras (reset when the list is replaced).
39
+ this._userCameraCounter = 0;
40
+
38
41
  // Auto-focus state
39
42
  this.autoFocusMode = AF_DEFAULTS.SMOOTHING_FACTOR ? 'auto' : 'manual';
40
43
  this.afScreenPoint = { x: 0.5, y: 0.5 };
@@ -51,6 +54,7 @@ export class CameraManager extends EventDispatcher {
51
54
  this._onResize = null;
52
55
  this._onReset = null;
53
56
  this._getSettings = null;
57
+ this._applySettings = null;
54
58
 
55
59
  }
56
60
 
@@ -61,6 +65,70 @@ export class CameraManager extends EventDispatcher {
61
65
  setCameras( cameras ) {
62
66
 
63
67
  this.cameras = cameras;
68
+ this._userCameraCounter = 0;
69
+
70
+ }
71
+
72
+ /**
73
+ * Adds a new user camera that snapshots the current render camera's pose,
74
+ * FOV, clip planes, and orbit target. The snapshot is a standalone template
75
+ * appended to the list; switching to it restores this exact framing.
76
+ *
77
+ * @param {string} [name] - Optional display name (auto-generated otherwise).
78
+ * @returns {number} The index of the newly added camera.
79
+ */
80
+ addCameraFromView( name ) {
81
+
82
+ const src = this.camera;
83
+ const cam = new PerspectiveCamera( src.fov, src.aspect, src.near, src.far );
84
+ cam.position.copy( src.position );
85
+ cam.quaternion.copy( src.quaternion );
86
+ cam.updateMatrixWorld( true );
87
+
88
+ cam.userData.__rayzeeUserCamera = true;
89
+ cam.userData.__rayzeeEffects = this._captureEffects();
90
+ if ( this.controls ) cam.userData.__rayzeeOrbitTarget = this.controls.target.clone();
91
+
92
+ this._userCameraCounter += 1;
93
+ cam.name = name || `View Camera ${this._userCameraCounter}`;
94
+
95
+ this.cameras.push( cam );
96
+ return this.cameras.length - 1;
97
+
98
+ }
99
+
100
+ /**
101
+ * Removes a user-added camera by index. Built-in (index 0) and
102
+ * model-embedded cameras are protected and cannot be removed.
103
+ * If the active camera is removed, falls back to the default camera.
104
+ *
105
+ * @param {number} index
106
+ * @returns {boolean} true if a camera was removed.
107
+ */
108
+ removeCamera( index ) {
109
+
110
+ if ( ! Number.isInteger( index ) || index <= 0 || index >= this.cameras.length ) return false;
111
+ if ( ! this.cameras[ index ]?.userData?.__rayzeeUserCamera ) return false;
112
+
113
+ const wasActive = this.currentCameraIndex === index;
114
+ this.cameras.splice( index, 1 );
115
+
116
+ if ( wasActive ) {
117
+
118
+ // The active camera is gone. Invalidate currentCameraIndex before falling
119
+ // back so switchCamera(0) skips both the (deleted) outgoing-effects save and
120
+ // the default-state save, and runs the restore path instead.
121
+ this.currentCameraIndex = - 1;
122
+ this.switchCamera( 0 );
123
+
124
+ } else if ( this.currentCameraIndex > index ) {
125
+
126
+ // Indices after the removed slot shift down by one.
127
+ this.currentCameraIndex -= 1;
128
+
129
+ }
130
+
131
+ return true;
64
132
 
65
133
  }
66
134
 
@@ -86,15 +154,74 @@ export class CameraManager extends EventDispatcher {
86
154
  * Call once after all managers are ready.
87
155
  *
88
156
  * @param {Object} callbacks
89
- * @param {Function} callbacks.onResize - Trigger viewport resize
90
- * @param {Function} callbacks.onReset - Trigger accumulation reset
91
- * @param {Function} callbacks.getSettings - (key) => value
157
+ * @param {Function} callbacks.onResize - Trigger viewport resize
158
+ * @param {Function} callbacks.onReset - Trigger accumulation reset
159
+ * @param {Function} callbacks.getSettings - (key) => value
160
+ * @param {Function} callbacks.applySettings - (updates) => void, batch-writes render settings
92
161
  */
93
- initCallbacks( { onResize, onReset, getSettings } ) {
162
+ initCallbacks( { onResize, onReset, getSettings, applySettings } ) {
94
163
 
95
164
  this._onResize = onResize;
96
165
  this._onReset = onReset;
97
166
  this._getSettings = getSettings;
167
+ this._applySettings = applySettings;
168
+
169
+ }
170
+
171
+ // ── Per-camera effects (DOF / focus) ──────────────────────────
172
+ // Each camera keeps its own depth-of-field + focus configuration so effects
173
+ // defined on one camera don't leak to another. State is stored on the camera
174
+ // object's userData and swapped in/out on switchCamera().
175
+
176
+ /**
177
+ * Snapshot the current global DOF/focus settings into a plain object.
178
+ * focusDistance is stored scaled (as held in RenderSettings).
179
+ * @returns {Object|null}
180
+ */
181
+ _captureEffects() {
182
+
183
+ const get = this._getSettings;
184
+ if ( ! get ) return null;
185
+
186
+ return {
187
+ enableDOF: get( 'enableDOF' ),
188
+ focusDistance: get( 'focusDistance' ),
189
+ aperture: get( 'aperture' ),
190
+ focalLength: get( 'focalLength' ),
191
+ apertureScale: get( 'apertureScale' ),
192
+ anamorphicRatio: get( 'anamorphicRatio' ),
193
+ autoFocusMode: this.autoFocusMode,
194
+ afScreenPoint: { ...this.afScreenPoint },
195
+ };
196
+
197
+ }
198
+
199
+ /**
200
+ * Apply a previously-captured effects object to the global settings + focus state.
201
+ * @param {Object} [eff]
202
+ */
203
+ _applyEffects( eff ) {
204
+
205
+ if ( ! eff ) return;
206
+
207
+ this._applySettings?.( {
208
+ enableDOF: eff.enableDOF,
209
+ focusDistance: eff.focusDistance,
210
+ aperture: eff.aperture,
211
+ focalLength: eff.focalLength,
212
+ apertureScale: eff.apertureScale,
213
+ anamorphicRatio: eff.anamorphicRatio,
214
+ } );
215
+
216
+ if ( eff.autoFocusMode !== undefined ) this.setAutoFocusMode( eff.autoFocusMode );
217
+ if ( eff.afScreenPoint ) this.setAFScreenPoint( eff.afScreenPoint.x, eff.afScreenPoint.y );
218
+
219
+ }
220
+
221
+ /** Scene scale used to convert scaled focusDistance to UI-facing units. */
222
+ _getSceneScale() {
223
+
224
+ return this._afContext?.assetLoader?.getSceneScale?.() || 1;
98
225
 
99
226
  }
100
227
 
@@ -122,6 +249,10 @@ export class CameraManager extends EventDispatcher {
122
249
 
123
250
  }
124
251
 
252
+ // Save the outgoing camera's DOF/focus effects so they can be restored later.
253
+ const outgoing = this.cameras[ this.currentCameraIndex ];
254
+ if ( outgoing ) outgoing.userData.__rayzeeEffects = this._captureEffects();
255
+
125
256
  // Save default camera state before switching away from it
126
257
  if ( this.currentCameraIndex === 0 && index !== 0 ) {
127
258
 
@@ -169,21 +300,41 @@ export class CameraManager extends EventDispatcher {
169
300
  this.camera.updateProjectionMatrix();
170
301
  this.camera.updateMatrixWorld( true );
171
302
 
172
- // Place orbit target along forward direction
303
+ // Restore the saved orbit target for user cameras; otherwise place it
304
+ // along the forward direction at the focus distance.
173
305
  if ( this.controls ) {
174
306
 
175
- const forward = new Vector3( 0, 0, - 1 ).applyQuaternion( sourceCamera.quaternion );
176
- const focusDist = focusDistance || 5.0;
177
- this.controls.target.copy( this.camera.position ).addScaledVector( forward, focusDist );
307
+ const savedTarget = sourceCamera.userData?.__rayzeeOrbitTarget;
308
+ if ( savedTarget ) {
309
+
310
+ this.controls.target.copy( savedTarget );
311
+
312
+ } else {
313
+
314
+ const forward = new Vector3( 0, 0, - 1 ).applyQuaternion( sourceCamera.quaternion );
315
+ const focusDist = focusDistance || 5.0;
316
+ this.controls.target.copy( this.camera.position ).addScaledVector( forward, focusDist );
317
+
318
+ }
319
+
178
320
  this.controls.update();
179
321
 
180
322
  }
181
323
 
182
324
  }
183
325
 
326
+ // Restore the incoming camera's own DOF/focus effects (if it has any saved;
327
+ // otherwise it inherits the current global config).
328
+ this._applyEffects( this.cameras[ index ]?.userData?.__rayzeeEffects );
329
+
184
330
  onResize?.();
185
331
  onReset?.();
186
- this.dispatchEvent( { type: 'CameraSwitched', cameraIndex: index } );
332
+
333
+ // Emit UI-facing effects: focusDistance unscaled (matching AUTO_FOCUS_UPDATED),
334
+ // so the adapter is a plain passthrough with no scene-scale awareness.
335
+ const effects = this._captureEffects();
336
+ if ( effects ) effects.focusDistance /= this._getSceneScale();
337
+ this.dispatchEvent( { type: 'CameraSwitched', cameraIndex: index, effects, fov: this.camera.fov } );
187
338
 
188
339
  }
189
340