rayzee 7.13.0 → 7.15.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.13.0",
3
+ "version": "7.15.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",
@@ -1780,6 +1780,7 @@ export class PathTracerApp extends EventDispatcher {
1780
1780
  transformManager: this.transformManager,
1781
1781
  appDispatch: ( e ) => this.dispatchEvent( e ),
1782
1782
  orbitControls: this.cameraManager.controls,
1783
+ helperScene: this._sceneHelpers.scene,
1783
1784
  } );
1784
1785
 
1785
1786
  this.denoisingManager.setOverlayManager( this.overlayManager );
@@ -90,6 +90,7 @@ export class LightSerializer {
90
90
 
91
91
  addDirectionalLight( light ) {
92
92
 
93
+ if ( ! light.visible ) return; // Skip hidden lights
93
94
  if ( light.intensity <= 0.0 ) return; // Skip zero intensity lights
94
95
 
95
96
  light.updateMatrixWorld();
@@ -146,6 +147,7 @@ export class LightSerializer {
146
147
 
147
148
  addRectAreaLight( light ) {
148
149
 
150
+ if ( ! light.visible ) return; // Skip hidden lights
149
151
  if ( light.intensity <= 0.0 ) return; // Skip zero intensity lights
150
152
 
151
153
  light.updateMatrixWorld();
@@ -190,6 +192,7 @@ export class LightSerializer {
190
192
 
191
193
  addPointLight( light ) {
192
194
 
195
+ if ( ! light.visible ) return; // Skip hidden lights
193
196
  if ( light.intensity <= 0.0 ) return; // Skip zero intensity lights
194
197
 
195
198
  light.updateMatrixWorld();
@@ -218,6 +221,7 @@ export class LightSerializer {
218
221
 
219
222
  addSpotLight( light ) {
220
223
 
224
+ if ( ! light.visible ) return; // Skip hidden lights
221
225
  if ( light.intensity <= 0.0 ) return; // Skip zero intensity lights
222
226
 
223
227
  light.updateMatrixWorld();
@@ -71,6 +71,7 @@ export class InteractionManager extends EventDispatcher {
71
71
  this._transformManager = null;
72
72
  this._appDispatch = null;
73
73
  this._orbitControls = null;
74
+ this._helperScene = null; // SceneHelpers' scene — lets clicking a light's visual helper select it
74
75
 
75
76
  }
76
77
 
@@ -85,13 +86,15 @@ export class InteractionManager extends EventDispatcher {
85
86
  * @param {import('./TransformManager.js').TransformManager} deps.transformManager
86
87
  * @param {Function} deps.appDispatch - (event) => dispatches on PathTracerApp
87
88
  * @param {import('three/addons/controls/OrbitControls.js').OrbitControls} [deps.orbitControls]
89
+ * @param {import('three').Scene} [deps.helperScene] - SceneHelpers' scene, for light-helper picking
88
90
  */
89
- setDependencies( { overlayManager, transformManager, appDispatch, orbitControls } ) {
91
+ setDependencies( { overlayManager, transformManager, appDispatch, orbitControls, helperScene } ) {
90
92
 
91
93
  this._overlayManager = overlayManager || null;
92
94
  this._transformManager = transformManager || null;
93
95
  this._appDispatch = appDispatch || null;
94
96
  this._orbitControls = orbitControls || null;
97
+ this._helperScene = helperScene || null;
95
98
 
96
99
  }
97
100
 
@@ -469,10 +472,26 @@ export class InteractionManager extends EventDispatcher {
469
472
 
470
473
  const intersects = this.raycaster.intersectObjects( this.scene.children, true );
471
474
  const validIntersects = this.filterValidIntersects( intersects );
475
+ const lightHit = this._pickLightHelper();
472
476
 
473
- if ( validIntersects.length > 0 ) {
477
+ let object = null;
478
+
479
+ if ( validIntersects.length > 0 && lightHit ) {
480
+
481
+ object = validIntersects[ 0 ].distance <= lightHit.distance ? validIntersects[ 0 ].object : lightHit.light;
482
+
483
+ } else if ( validIntersects.length > 0 ) {
484
+
485
+ object = validIntersects[ 0 ].object;
486
+
487
+ } else if ( lightHit ) {
488
+
489
+ object = lightHit.light;
490
+
491
+ }
492
+
493
+ if ( object ) {
474
494
 
475
- const object = validIntersects[ 0 ].object;
476
495
  const currentlySelectedObject = this.selectedObject;
477
496
  const isAlreadySelected = currentlySelectedObject && currentlySelectedObject.uuid === object.uuid;
478
497
 
@@ -674,6 +693,40 @@ export class InteractionManager extends EventDispatcher {
674
693
 
675
694
  }
676
695
 
696
+ /**
697
+ * Raycasts against the light-helper overlay scene (SceneHelpers) and
698
+ * resolves the closest hit back to its source light. Helper geometry
699
+ * (PointLightHelper, SpotLightHelper, DirectionalLightHelper,
700
+ * RectAreaLightHelper) is often line/child geometry, so this walks up the
701
+ * parent chain looking for the `.light` reference each helper exposes.
702
+ * Returns null when there's no helper scene, no lights, or no hit — this
703
+ * naturally happens when "Light Helper" is toggled off, since SceneHelpers
704
+ * empties its scene in that case.
705
+ * @private
706
+ */
707
+ _pickLightHelper() {
708
+
709
+ if ( ! this._helperScene || this._helperScene.children.length === 0 ) return null;
710
+
711
+ const intersects = this.raycaster.intersectObjects( this._helperScene.children, true );
712
+
713
+ for ( const hit of intersects ) {
714
+
715
+ let object = hit.object;
716
+
717
+ while ( object ) {
718
+
719
+ if ( object.light ) return { distance: hit.distance, light: object.light };
720
+ object = object.parent;
721
+
722
+ }
723
+
724
+ }
725
+
726
+ return null;
727
+
728
+ }
729
+
677
730
  /**
678
731
  * Update dependencies (useful when scene/camera changes)
679
732
  */
@@ -330,6 +330,7 @@ export class LightManager extends EventDispatcher {
330
330
  uuid: light.uuid,
331
331
  name: light.name,
332
332
  type: light.type,
333
+ visible: light.visible,
333
334
  intensity: light.intensity,
334
335
  color: `#${light.color.getHexString()}`,
335
336
  position: [ light.position.x, light.position.y, light.position.z ],
@@ -9,6 +9,11 @@ import { Matrix3, Scene, Vector3 } from 'three';
9
9
  import { TransformControls } from 'three/addons/controls/TransformControls.js';
10
10
  import { EngineEvents } from '../EngineEvents.js';
11
11
 
12
+ // Three.js "forward" convention (local -Z), used to derive a spot/directional
13
+ // light's aim direction from its quaternion. Read-only — setFromUnitVectors()
14
+ // does not mutate its arguments, so this can be a shared constant.
15
+ const FORWARD = new Vector3( 0, 0, - 1 );
16
+
12
17
  export class TransformManager {
13
18
 
14
19
  constructor( { camera, canvas, orbitControls, app } ) {
@@ -36,6 +41,12 @@ export class TransformManager {
36
41
  this._refitInFlight = false;
37
42
  this._baselineComputed = false;
38
43
 
44
+ // Light transform state — spot/directional lights aim via a separate
45
+ // `.target` Object3D rather than their own rotation (see attach()).
46
+ this._tempForward = new Vector3();
47
+ this._lightTargetDistance = null;
48
+ this._lastLightPosition = null;
49
+
39
50
  // Bind handlers
40
51
  this._onDraggingChanged = this._onDraggingChanged.bind( this );
41
52
  this._onObjectChange = this._onObjectChange.bind( this );
@@ -91,6 +102,32 @@ export class TransformManager {
91
102
  this._controls.attach( object );
92
103
  this._attached = object;
93
104
 
105
+ this._lightTargetDistance = null;
106
+ this._lastLightPosition = null;
107
+
108
+ // Spot/directional lights aim via `.target.position`, not their own
109
+ // quaternion. Sync the quaternion to the current aim direction now so
110
+ // rotate mode starts from the true current direction instead of identity.
111
+ if ( object.isLight && object.target ) {
112
+
113
+ const forward = object.target.position.clone().sub( object.position );
114
+ const distance = forward.length();
115
+
116
+ if ( distance > 1e-4 ) {
117
+
118
+ object.quaternion.setFromUnitVectors( FORWARD, forward.normalize() );
119
+ this._lightTargetDistance = distance;
120
+
121
+ } else {
122
+
123
+ this._lightTargetDistance = 1;
124
+
125
+ }
126
+
127
+ this._lastLightPosition = object.position.clone();
128
+
129
+ }
130
+
94
131
  }
95
132
 
96
133
  /**
@@ -102,6 +139,8 @@ export class TransformManager {
102
139
 
103
140
  this._controls.detach();
104
141
  this._attached = null;
142
+ this._lightTargetDistance = null;
143
+ this._lastLightPosition = null;
105
144
 
106
145
  }
107
146
 
@@ -112,6 +151,9 @@ export class TransformManager {
112
151
 
113
152
  this._controls.setMode( mode );
114
153
  this._app?.dispatchEvent( { type: EngineEvents.TRANSFORM_MODE_CHANGED, mode } );
154
+ // The gizmo shape changes (arrows/rings/boxes) but nothing else invalidates
155
+ // the frame — nudge a redraw so it doesn't wait for the next camera move.
156
+ this._app?.refreshFrame();
115
157
 
116
158
  }
117
159
 
@@ -121,6 +163,7 @@ export class TransformManager {
121
163
  setSpace( space ) {
122
164
 
123
165
  this._controls.setSpace( space );
166
+ this._app?.refreshFrame();
124
167
 
125
168
  }
126
169
 
@@ -188,8 +231,17 @@ export class TransformManager {
188
231
 
189
232
  } else {
190
233
 
191
- // Drag ended — trigger final refit
192
- this._recomputeAndRefit();
234
+ // Drag ended — trigger final refit (mesh) or finalize (light)
235
+ if ( this._attached?.isLight ) {
236
+
237
+ this._finalizeLightTransform();
238
+
239
+ } else {
240
+
241
+ this._recomputeAndRefit();
242
+
243
+ }
244
+
193
245
  this._app.dispatchEvent( { type: EngineEvents.OBJECT_TRANSFORM_END } );
194
246
 
195
247
  }
@@ -202,6 +254,73 @@ export class TransformManager {
202
254
  this._app.needsReset = true;
203
255
  this._app.wake();
204
256
 
257
+ if ( this._attached?.isLight ) {
258
+
259
+ this._syncLightDuringDrag();
260
+
261
+ }
262
+
263
+ }
264
+
265
+ // ── Light Transform Sync ──
266
+
267
+ /**
268
+ * Called every gizmo move while a light is attached. Translate mode carries
269
+ * `.target` along by the same delta (so moving a light doesn't silently
270
+ * swing its aim); rotate mode recomputes `.target` from the light's
271
+ * quaternion at a fixed distance (so rotating actually steers the beam/sun).
272
+ * Also resyncs GPU light buffers + the visible SceneHelpers gizmo live.
273
+ */
274
+ _syncLightDuringDrag() {
275
+
276
+ const light = this._attached;
277
+
278
+ if ( light.target ) {
279
+
280
+ const mode = this._controls.mode;
281
+
282
+ if ( mode === 'translate' && this._lastLightPosition ) {
283
+
284
+ const delta = this._tempForward.copy( light.position ).sub( this._lastLightPosition );
285
+ light.target.position.add( delta );
286
+ light.target.updateMatrixWorld( true );
287
+
288
+ } else if ( mode === 'rotate' && this._lightTargetDistance != null ) {
289
+
290
+ const forward = this._tempForward.set( 0, 0, - 1 ).applyQuaternion( light.quaternion );
291
+ light.target.position.copy( light.position ).addScaledVector( forward, this._lightTargetDistance );
292
+ light.target.updateMatrixWorld( true );
293
+
294
+ }
295
+
296
+ this._lastLightPosition.copy( light.position );
297
+
298
+ }
299
+
300
+ this._app.lightManager?.updateLights();
301
+
302
+ }
303
+
304
+ /**
305
+ * Called once on drag end while a light is attached. Bakes RectAreaLight
306
+ * scale into width/height (the serializer also reads scale live, but the
307
+ * Lights panel sliders are the source of truth for size) and does a final
308
+ * GPU/helper resync.
309
+ */
310
+ _finalizeLightTransform() {
311
+
312
+ const light = this._attached;
313
+
314
+ if ( light.isRectAreaLight && ( light.scale.x !== 1 || light.scale.y !== 1 ) ) {
315
+
316
+ light.width *= light.scale.x;
317
+ light.height *= light.scale.y;
318
+ light.scale.set( 1, 1, 1 );
319
+
320
+ }
321
+
322
+ this._app.lightManager?.updateLights();
323
+
205
324
  }
206
325
 
207
326
  // ── Position Extraction & BVH Refit ──
@@ -440,6 +559,9 @@ export class TransformManager {
440
559
  this._skinnedCache = null;
441
560
  this._normalCache = null;
442
561
  this._baselineComputed = false;
562
+ this._tempForward = null;
563
+ this._lightTargetDistance = null;
564
+ this._lastLightPosition = null;
443
565
 
444
566
  // Drop back-references to the owning app and shared resources so the
445
567
  // PathTracerApp graph can be GC'd. Without this, _app pinned the entire