rayzee 7.15.1 → 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.
@@ -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