Skip to content

Conversation

@wrangelvid
Copy link
Contributor

@wrangelvid wrangelvid commented Dec 25, 2025

Description

Adds a recursive property to the Layers class. When set to true, if an object's layer test fails during traversal, the entire subtree is skipped. If the layer test passes, children inherit the parent's layers instead of using their own.

This is a revival of #28427 by @CodyJasonBennett.

Changes from original PR

  • Removed DEFAULT_RECURSIVE static property (main source of debate in the original PR)
  • Simplified implementation: layer inheritance is handled via a single inheritedLayers parameter passed through traversal

Use case

When using multiple cameras (e.g. secondary cameras for minimaps, portals, etc.), you often want to exclude entire UI hierarchies from certain cameras. Currently, you must manually set layers on every object in the hierarchy. With recursive, you set it once on the parent.

We have been using three.js with react-three-fiber and @pmndrs/uikit. Internally, we built a few different solutions, but it was quite tedious without affecting the renderer directly.

Looking forward to feedback!

@github-actions
Copy link

github-actions bot commented Dec 25, 2025

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 355.34
84.47
355.63
84.6
+294 B
+131 B
WebGPU 618.89
171.93
619.22
172.05
+321 B
+120 B
WebGPU Nodes 617.5
171.68
617.82
171.8
+321 B
+119 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 487.5
119.33
488.1
119.54
+595 B
+206 B
WebGPU 691.15
187.57
691.78
187.78
+622 B
+201 B
WebGPU Nodes 641
174.81
641.62
175.01
+622 B
+204 B

@wrangelvid wrangelvid marked this pull request as draft December 28, 2025 15:24
@wrangelvid
Copy link
Contributor Author

wrangelvid commented Dec 28, 2025

I converted this to draft as I realized there are many more places where we need to take account for layer inheritance.
The main issue stems from calling object.layers.test with a parent object having a recursive layer. We can't reliably use the local object to test against the layers.

Alternatively, we could store an inheritedMask in the layers object that is updated with updateMatrixWorld

@wrangelvid wrangelvid force-pushed the recursive_layers branch 2 times, most recently from 9b9b1e2 to e45ddc3 Compare December 28, 2025 17:35
@diffray-bot
Copy link

Changes Summary

This PR adds a recursive property to the Layers class that enables hierarchical layer inheritance during rendering and raycasting. When enabled on a parent object, all descendants inherit the parent's layers during traversal, and if the layer test fails, the entire subtree is skipped. This simplifies multi-camera setups where UI hierarchies need to be excluded from certain cameras.

Type: feature

Components Affected: Layers system, Object3D traversal, Renderer (common, WebGL, WebGPU), Raycaster, RenderList, CSS2D/CSS3D renderers, Shadow mapping

Files Changed
File Summary Change Impact
/tmp/workspace/src/core/Layers.js Added recursive boolean property (default false) to control layer inheritance behavior during traversal. ✏️ 🟢
/tmp/workspace/src/core/Object3D.js Added traverseVisibleWithLayers() method that respects recursive layer inheritance and skips subtrees that fail layer tests. ✏️ 🟢
/tmp/workspace/src/core/Raycaster.js Updated intersect() function to pass inherited layers through recursion and skip subtrees when recursive layer test fails. ✏️ 🟡
/tmp/workspace/src/renderers/common/Renderer.js Updated _projectObject() to track and pass inherited layers through scene traversal, enabling recursive layer filtering for render lists. ✏️ 🔴
/tmp/workspace/src/renderers/common/RenderList.js Added effectiveLayers parameter to render items for layer visibility testing during rendering. ✏️ 🟡
.../workspace/src/renderers/common/RenderObject.js Added effectiveLayers property to RenderObject for storing effective layers during rendering. ✏️ 🟡
/tmp/workspace/src/renderers/WebGLRenderer.js Updated render method signatures to pass effectiveLayers parameter through rendering pipeline. ✏️ 🟡
...rkspace/src/renderers/webgl/WebGLRenderLists.js Updated to pass effectiveLayers through render list operations. ✏️ 🟡
...workspace/src/renderers/webgl/WebGLShadowMap.js Updated shadow map rendering to respect inherited layers from recursive parents. ✏️ 🟡
...rkspace/examples/jsm/renderers/CSS2DRenderer.js Updated to use traverseVisibleWithLayers() method for layer-aware traversal. ✏️ 🟢
...rkspace/examples/jsm/renderers/CSS3DRenderer.js Updated to use traverseVisibleWithLayers() method for layer-aware traversal. ✏️ 🟢
/tmp/workspace/test/unit/src/core/Layers.tests.js Added unit tests for the new recursive property with default value and mutation tests. ✏️ 🟢
Architecture Impact
  • New Patterns: Layer inheritance pattern - parent layers propagate to children when recursive=true, Inherited parameter pattern - threadable context (inheritedLayers) passed through traversal
  • Coupling: Introduces coupling between Object3D traversal methods and Layers behavior. The renderer now depends on layer inheritance state to determine visibility. This is relatively low coupling as it maintains the existing layer testing interface.
  • Breaking Changes: Method signature changes: _projectObject() now accepts inheritedLayers parameter (optional, default null), Method signature changes: RenderList push() and unshift() now require effectiveLayers parameter, Method signature changes: renderObject() now accepts optional effectiveLayers parameter, Raycaster intersect() internal function signature changed to accept inheritedLayers

Risk Areas: Renderer traversal logic: Complex parameter threading through _projectObject() recursion could introduce subtle bugs if inheritance logic is incorrect, Raycasting precision: Changes to raycasting traversal may affect collision detection accuracy in multi-layer scenarios, Performance: Layer inheritance adds conditional checks in hot rendering paths; impact depends on typical layer recursion depth, Shadow maps: Modified shadow map rendering logic needs thorough testing with recursive layers, CSS renderers: CSS2D/CSS3D renderers use new traversal method; visual regression possible if inheritance behaves differently, API compatibility: Method signature changes require careful integration testing across renderer backends (WebGL, WebGPU, fallback), Inheritance semantics: Edge case behavior undefined for mixed recursive/non-recursive hierarchies (e.g., recursive parent with non-recursive child)

Suggestions
  • Add comprehensive integration tests for recursive layers in multi-camera scenarios
  • Test shadow rendering with recursive layers enabled in parent-child hierarchies
  • Verify CSS2D/CSS3D renderers produce identical visual output with new traversal method
  • Add performance benchmarks for deeply nested recursive layer hierarchies
  • Document the semantics of layer inheritance when recursive property varies through hierarchy
  • Consider adding a method to explicitly test whether a layer is recursive-inherited vs directly set
  • Test raycasting with complex layer hierarchies to ensure no precision or filtering issues

Full review in progress... | Powered by diffray


if ( layerVisible ) callback( this );

const childInheritedLayers = this.layers.recursive ? this.layers : inheritedLayers;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Incorrect layer inheritance - using object.layers instead of effectiveLayers
Agent: Delegated (architecture, bugs, documentation, performance)

Category: bug

Description:
When an object has recursive=true, children should inherit the effectiveLayers (which may have been inherited from an ancestor), not object.layers. The code checks object.layers.recursive instead of effectiveLayers.recursive, causing children to inherit the wrong layer mask when their parent inherited layers from an ancestor with recursive=true.

Suggestion:
Change line 1104 from 'const childInheritedLayers = this.layers.recursive ? this.layers : inheritedLayers;' to 'const childInheritedLayers = effectiveLayers.recursive ? effectiveLayers : inheritedLayers;'

Confidence: 85%
Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray


if ( propagate === true && recursive === true ) {

const childInheritedLayers = object.layers.recursive === true ? object.layers : inheritedLayers;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Incorrect layer inheritance in raycasting - using object.layers instead of effectiveLayers
Agent: Delegated (architecture, bugs, documentation, performance)

Category: bug

Description:
When an object has recursive=true, children should inherit the effectiveLayers (which may have been inherited from an ancestor), not object.layers. The code checks object.layers.recursive instead of effectiveLayers.recursive, causing incorrect raycasting behavior with inherited layers.

Suggestion:
Change line 255 from 'const childInheritedLayers = object.layers.recursive === true ? object.layers : inheritedLayers;' to 'const childInheritedLayers = effectiveLayers.recursive === true ? effectiveLayers : inheritedLayers;'

Confidence: 85%
Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray


}

const childInheritedLayers = object.layers.recursive === true ? object.layers : inheritedLayers;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Incorrect layer inheritance in rendering - using object.layers instead of effectiveLayers
Agent: Delegated (architecture, bugs, documentation, performance)

Category: bug

Description:
When an object has recursive=true, children should inherit the effectiveLayers (which may have been inherited from an ancestor), not object.layers. The code checks object.layers.recursive instead of effectiveLayers.recursive, causing incorrect rendering of objects with inherited recursive layers.

Suggestion:
Change line 2925 from 'const childInheritedLayers = object.layers.recursive === true ? object.layers : inheritedLayers;' to 'const childInheritedLayers = effectiveLayers.recursive === true ? effectiveLayers : inheritedLayers;'

Confidence: 85%
Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 288 to 323

if ( material.transparent === true || material.transmission > 0 ||
( material.transmissionNode && material.transmissionNode.isNode ) ||
( material.backdropNode && material.backdropNode.isNode ) ) {

if ( needsDoublePass( material ) ) this.transparentDoublePass.push( renderItem );

this.transparent.push( renderItem );

} else {

this.opaque.push( renderItem );

}

}

/**
* Inserts the given object as a render item at the start of the internal render lists.
* The selected lists depend on the object properties.
*
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The 3D object's geometry.
* @param {Material} material - The 3D object's material.
* @param {number} groupOrder - The current group order.
* @param {number} z - Th 3D object's depth value (z value in clip space).
* @param {?number} group - {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The current clipping context.
* @param {Layers} effectiveLayers - The effective layers for layer visibility testing.
*/
unshift( object, geometry, material, groupOrder, z, group, clippingContext ) {
unshift( object, geometry, material, groupOrder, z, group, clippingContext, effectiveLayers ) {

const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext );
const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext, effectiveLayers );

if ( material.transparent === true || material.transmission > 0 ||
( material.transmissionNode && material.transmissionNode.isNode ) ||

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Repeated material transparency checks
Agent: react

Category: quality

Description:
Material transparency logic is duplicated in push() and unshift() methods, checking material.transmission and related properties in identical patterns.

Suggestion:
Extract material transparency check into a shared helper function to eliminate duplication and reduce maintenance risk when logic changes.

Why this matters: Reduces duplicate work and logic drift.

Confidence: 75%
Rule: ts_use_computed_derived_properties_for_repe
Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +1083 to +1113
/**
* Like {@link Object3D#traverseVisible}, but the callback will only be executed
* for visible 3D objects that pass the layer test. Descendants of invisible
* 3D objects and objects with recursive layers that fail the test are not traversed.
*
* Note: Modifying the scene graph inside the callback is discouraged.
*
* @param {Layers} cameraLayers - The camera layers to test against.
* @param {Function} callback - A callback function that allows to process the current 3D object.
*/
traverseVisibleWithLayers( cameraLayers, callback, inheritedLayers = null ) {

if ( this.visible === false ) return;

const effectiveLayers = inheritedLayers !== null ? inheritedLayers : this.layers;
const layerVisible = effectiveLayers.test( cameraLayers );

if ( ! layerVisible && effectiveLayers.recursive ) return;

if ( layerVisible ) callback( this );

const childInheritedLayers = this.layers.recursive ? this.layers : inheritedLayers;

for ( let i = 0, l = this.children.length; i < l; i ++ ) {

this.children[ i ].traverseVisibleWithLayers( cameraLayers, callback, childInheritedLayers );

}

}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - New traverseVisibleWithLayers method lacks test coverage
Agent: testing

Category: quality

Description:
A new public method traverseVisibleWithLayers() was added to Object3D with parameters (cameraLayers, callback, inheritedLayers = null) but there are no corresponding unit tests. The existing traverse/traverseVisible/traverseAncestors tests follow a pattern in Object3D.tests.js but the new method combining layer visibility testing with inheritance logic has zero test coverage.

Suggestion:
Add comprehensive test case to test/unit/src/core/Object3D.tests.js following the existing pattern, testing: (1) Basic traversal with matching camera layers, (2) Traversal when child layers don't match camera layers, (3) Recursive layer inheritance - when parent has recursive=true and child has different layers, (4) Handling of inheritedLayers parameter (5) Edge case where recursive=true but test fails, (6) Multiple levels of nesting with mixed recursive values

Confidence: 92%
Rule: test_new_parameter_coverage
Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 236 to 261
function intersect( object, raycaster, intersects, recursive, inheritedLayers = null ) {

const effectiveLayers = inheritedLayers !== null ? inheritedLayers : object.layers;
const visible = effectiveLayers.test( raycaster.layers );

if ( visible === false && effectiveLayers.recursive === true ) return;

let propagate = true;

if ( object.layers.test( raycaster.layers ) ) {
if ( visible ) {

const result = object.raycast( raycaster, intersects );

if ( result === false ) propagate = false;

}

if ( propagate === true && recursive === true ) {

const childInheritedLayers = object.layers.recursive === true ? object.layers : inheritedLayers;

const children = object.children;

for ( let i = 0, l = children.length; i < l; i ++ ) {

intersect( children[ i ], raycaster, intersects, true );
intersect( children[ i ], raycaster, intersects, true, childInheritedLayers );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Raycaster.intersect() new parameter inheritedLayers lacks test coverage
Agent: testing

Category: quality

Description:
The intersect() helper function was modified to add a new parameter inheritedLayers = null and uses it to determine effective layers for visibility testing. The test file test/unit/src/core/Raycaster.tests.js tests intersectObject() and intersectObjects() with recursive and non-recursive searches, but does not test layer-based filtering or the new recursive layer inheritance behavior.

Suggestion:
Add test cases to test/unit/src/core/Raycaster.tests.js following the existing 'intersectObject' and 'intersectObjects' test pattern. Test: (1) Raycasting with layer filtering enabled (2) Child objects hidden by parent's recursive layer setting (3) Child objects with different layers when parent has recursive=true (4) Inheritance of parent layers through recursive property

Confidence: 90%
Rule: test_new_parameter_coverage
Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 2805 to 2931
* @param {Layers|null} inheritedLayers - Layers inherited from a recursive ancestor.
*/
_projectObject( object, camera, groupOrder, renderList, clippingContext ) {
_projectObject( object, camera, groupOrder, renderList, clippingContext, inheritedLayers = null ) {

if ( object.visible === false ) return;

const visible = object.layers.test( camera.layers );
const effectiveLayers = inheritedLayers !== null ? inheritedLayers : object.layers;
const visible = effectiveLayers.test( camera.layers );

if ( visible === false && effectiveLayers.recursive === true ) return;

if ( visible ) {

if ( object.isGroup ) {

groupOrder = object.renderOrder;

if ( object.isClippingGroup && object.enabled ) clippingContext = clippingContext.getGroupContext( object );

} else if ( object.isLOD ) {

if ( object.autoUpdate === true ) object.update( camera );

} else if ( object.isLight ) {

renderList.pushLight( object );

} else if ( object.isSprite ) {

const frustum = camera.isArrayCamera ? _frustumArray : _frustum;

if ( ! object.frustumCulled || frustum.intersectsSprite( object, camera ) ) {

if ( this.sortObjects === true ) {

_vector4.setFromMatrixPosition( object.matrixWorld ).applyMatrix4( _projScreenMatrix );

}

const { geometry, material } = object;

if ( material.visible ) {

renderList.push( object, geometry, material, groupOrder, _vector4.z, null, clippingContext );
renderList.push( object, geometry, material, groupOrder, _vector4.z, null, clippingContext, effectiveLayers );

}

}

} else if ( object.isLineLoop ) {

error( 'Renderer: Objects of type THREE.LineLoop are not supported. Please use THREE.Line or THREE.LineSegments.' );

} else if ( object.isMesh || object.isLine || object.isPoints ) {

const frustum = camera.isArrayCamera ? _frustumArray : _frustum;

if ( ! object.frustumCulled || frustum.intersectsObject( object, camera ) ) {

const { geometry, material } = object;

if ( this.sortObjects === true ) {

if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

_vector4
.copy( geometry.boundingSphere.center )
.applyMatrix4( object.matrixWorld )
.applyMatrix4( _projScreenMatrix );

}

if ( Array.isArray( material ) ) {

const groups = geometry.groups;

for ( let i = 0, l = groups.length; i < l; i ++ ) {

const group = groups[ i ];
const groupMaterial = material[ group.materialIndex ];

if ( groupMaterial && groupMaterial.visible ) {

renderList.push( object, geometry, groupMaterial, groupOrder, _vector4.z, group, clippingContext );
renderList.push( object, geometry, groupMaterial, groupOrder, _vector4.z, group, clippingContext, effectiveLayers );

}

}

} else if ( material.visible ) {

renderList.push( object, geometry, material, groupOrder, _vector4.z, null, clippingContext );
renderList.push( object, geometry, material, groupOrder, _vector4.z, null, clippingContext, effectiveLayers );

}

}

}

}

if ( object.isBundleGroup === true && this.backend.beginBundle !== undefined ) {

const baseRenderList = renderList;

// replace render list
renderList = this._renderLists.get( object, camera );

renderList.begin();

baseRenderList.pushBundle( {
bundleGroup: object,
camera,
renderList,
} );

renderList.finish();

}

const childInheritedLayers = object.layers.recursive === true ? object.layers : inheritedLayers;

const children = object.children;

for ( let i = 0, l = children.length; i < l; i ++ ) {

this._projectObject( children[ i ], camera, groupOrder, renderList, clippingContext );
this._projectObject( children[ i ], camera, groupOrder, renderList, clippingContext, childInheritedLayers );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Renderer._projectObject() new parameter inheritedLayers lacks test coverage
Agent: testing

Category: quality

Description:
The _projectObject() method signature was modified to add inheritedLayers = null parameter, implementing layer inheritance with effectiveLayers computation for visibility calculations. WebGLRenderer.tests.js is an empty stub with no tests.

Suggestion:
Add integration tests or create tests in WebGLRenderer.tests.js that verify: (1) Objects with recursive layers=true are rendered correctly (2) Child objects inherit parent's layers when parent has recursive=true (3) effectiveLayers is passed correctly through the rendering pipeline (4) Layer filtering works correctly with the new inheritance mechanism

Confidence: 85%
Rule: test_new_parameter_coverage
Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 228 to 320
getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext, effectiveLayers ) {

let renderItem = this.renderItems[ this.renderItemsIndex ];

if ( renderItem === undefined ) {

renderItem = {
id: object.id,
object: object,
geometry: geometry,
material: material,
groupOrder: groupOrder,
renderOrder: object.renderOrder,
z: z,
group: group,
clippingContext: clippingContext
clippingContext: clippingContext,
effectiveLayers: effectiveLayers
};

this.renderItems[ this.renderItemsIndex ] = renderItem;

} else {

renderItem.id = object.id;
renderItem.object = object;
renderItem.geometry = geometry;
renderItem.material = material;
renderItem.groupOrder = groupOrder;
renderItem.renderOrder = object.renderOrder;
renderItem.z = z;
renderItem.group = group;
renderItem.clippingContext = clippingContext;
renderItem.effectiveLayers = effectiveLayers;

}

this.renderItemsIndex ++;

return renderItem;

}

/**
* Pushes the given object as a render item to the internal render lists.
* The selected lists depend on the object properties.
*
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The 3D object's geometry.
* @param {Material} material - The 3D object's material.
* @param {number} groupOrder - The current group order.
* @param {number} z - Th 3D object's depth value (z value in clip space).
* @param {?number} group - {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The current clipping context.
* @param {Layers} effectiveLayers - The effective layers for layer visibility testing.
*/
push( object, geometry, material, groupOrder, z, group, clippingContext ) {
push( object, geometry, material, groupOrder, z, group, clippingContext, effectiveLayers ) {

const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext );
const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext, effectiveLayers );

if ( object.occlusionTest === true ) this.occlusionQueryCount ++;

if ( material.transparent === true || material.transmission > 0 ||
( material.transmissionNode && material.transmissionNode.isNode ) ||
( material.backdropNode && material.backdropNode.isNode ) ) {

if ( needsDoublePass( material ) ) this.transparentDoublePass.push( renderItem );

this.transparent.push( renderItem );

} else {

this.opaque.push( renderItem );

}

}

/**
* Inserts the given object as a render item at the start of the internal render lists.
* The selected lists depend on the object properties.
*
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The 3D object's geometry.
* @param {Material} material - The 3D object's material.
* @param {number} groupOrder - The current group order.
* @param {number} z - Th 3D object's depth value (z value in clip space).
* @param {?number} group - {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The current clipping context.
* @param {Layers} effectiveLayers - The effective layers for layer visibility testing.
*/
unshift( object, geometry, material, groupOrder, z, group, clippingContext ) {
unshift( object, geometry, material, groupOrder, z, group, clippingContext, effectiveLayers ) {

const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext );
const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext, effectiveLayers );

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - RenderList push/unshift/getNextRenderItem effectiveLayers lacks test coverage
Agent: testing

Category: quality

Description:
Three RenderList methods (push, unshift, getNextRenderItem) were modified to accept and store a new effectiveLayers parameter. These are internal rendering pipeline methods for the WebGPU/common renderer path. WebGLRenderLists.tests.js tests the WebGL-specific render list, but the common/RenderList.js class has no tests.

Suggestion:
Create tests for RenderList class to verify: (1) push() correctly stores effectiveLayers in the render item (2) unshift() correctly stores effectiveLayers in the render item (3) getNextRenderItem() properly initializes effectiveLayers property on new items (4) Re-used render items have effectiveLayers updated correctly

Confidence: 72%
Rule: test_new_parameter_coverage
Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

@diffray-bot
Copy link

Review Summary

Free public review - Want AI code reviews on your PRs? Check out diffray.ai

Validated 28 issues: 17 kept, 11 filtered

Issues Found: 17

💬 See 8 individual line comment(s) for details.

📊 8 unique issue type(s) across 17 location(s)

📋 Full issue list (click to expand)

🟠 HIGH - Incorrect layer inheritance - using object.layers instead of effectiveLayers

Agent: Delegated (architecture, bugs, documentation, performance)

Category: bug

File: src/core/Object3D.js:1104

Description: When an object has recursive=true, children should inherit the effectiveLayers (which may have been inherited from an ancestor), not object.layers. The code checks object.layers.recursive instead of effectiveLayers.recursive, causing children to inherit the wrong layer mask when their parent inherited layers from an ancestor with recursive=true.

Suggestion: Change line 1104 from 'const childInheritedLayers = this.layers.recursive ? this.layers : inheritedLayers;' to 'const childInheritedLayers = effectiveLayers.recursive ? effectiveLayers : inheritedLayers;'

Confidence: 85%


🟠 HIGH - Incorrect layer inheritance in raycasting - using object.layers instead of effectiveLayers

Agent: Delegated (architecture, bugs, documentation, performance)

Category: bug

File: src/core/Raycaster.js:255

Description: When an object has recursive=true, children should inherit the effectiveLayers (which may have been inherited from an ancestor), not object.layers. The code checks object.layers.recursive instead of effectiveLayers.recursive, causing incorrect raycasting behavior with inherited layers.

Suggestion: Change line 255 from 'const childInheritedLayers = object.layers.recursive === true ? object.layers : inheritedLayers;' to 'const childInheritedLayers = effectiveLayers.recursive === true ? effectiveLayers : inheritedLayers;'

Confidence: 85%


🟠 HIGH - Incorrect layer inheritance in rendering - using object.layers instead of effectiveLayers

Agent: Delegated (architecture, bugs, documentation, performance)

Category: bug

File: src/renderers/common/Renderer.js:2925

Description: When an object has recursive=true, children should inherit the effectiveLayers (which may have been inherited from an ancestor), not object.layers. The code checks object.layers.recursive instead of effectiveLayers.recursive, causing incorrect rendering of objects with inherited recursive layers.

Suggestion: Change line 2925 from 'const childInheritedLayers = object.layers.recursive === true ? object.layers : inheritedLayers;' to 'const childInheritedLayers = effectiveLayers.recursive === true ? effectiveLayers : inheritedLayers;'

Confidence: 85%


🟠 HIGH - JSDoc parameter description mismatch in disable() method (4 occurrences)

Agent: documentation

Category: docs

📍 View all locations
File Description Suggestion Confidence
src/core/Layers.js:87-88 The @param documentation says 'The layer to enable.' but the method actually disables a layer. This ... Change the JSDoc comment from 'The layer to enable.' to 'The layer to disable.' 95%
src/core/Object3D.js:980-983 The JSDoc says 'Returns a Quaternion representing the position of the 3D object in world space.' but... Change description to 'Returns a Quaternion representing the rotation of the 3D object in world spac... 95%
src/renderers/webgl-fallback/WebGLBackend.js:41 The constructor JSDoc says 'Constructs a new WebGPU backend.' but this is the WebGLBackend class. Th... Change 'Constructs a new WebGPU backend.' to 'Constructs a new WebGL backend.' 95%
src/core/Object3D.js:647 JSDoc says 'word space' instead of 'world space'. Fix typo: change 'word space' to 'world space' 95%

Rule: ts_jsdoc_description_mismatch


🟠 HIGH - New traverseVisibleWithLayers method lacks test coverage (4 occurrences)

Agent: testing

Category: quality

📍 View all locations
File Description Suggestion Confidence
src/core/Object3D.js:1083-1113 A new public method traverseVisibleWithLayers() was added to Object3D with parameters (cameraLayers,... Add comprehensive test case to test/unit/src/core/Object3D.tests.js following the existing pattern, ... 92%
src/core/Raycaster.js:236-261 The intersect() helper function was modified to add a new parameter inheritedLayers = null and uses ... Add test cases to test/unit/src/core/Raycaster.tests.js following the existing 'intersectObject' and... 90%
src/renderers/common/Renderer.js:2805-2931 The _projectObject() method signature was modified to add inheritedLayers = null parameter, implemen... Add integration tests or create tests in WebGLRenderer.tests.js that verify: (1) Objects with recurs... 85%
src/renderers/common/RenderList.js:228-320 Three RenderList methods (push, unshift, getNextRenderItem) were modified to accept and store a new ... Create tests for RenderList class to verify: (1) push() correctly stores effectiveLayers in the rend... 72%

Rule: test_new_parameter_coverage


🟠 HIGH - Object3D.toJSON() missing layers.recursive serialization (2 occurrences)

Agent: typescript

Category: bug

📍 View all locations
File Description Suggestion Confidence
src/core/Object3D.js:1303 The toJSON() method only serializes layers.mask but not the new layers.recursive property. When seri... Change line 1303 from 'object.layers = this.layers.mask;' to serialize both properties. Consider 'ob... 95%
src/core/Object3D.js:1590 The copy() method only copies layers.mask but not layers.recursive. When cloning or copying an Objec... Add 'this.layers.recursive = source.layers.recursive;' after line 1590 to copy the recursive propert... 95%

Rule: ts_recursive_object_traversal_no_cycle_detection


🟡 MEDIUM - JSDoc default value mismatch for CSS2DObject element property (3 occurrences)

Agent: documentation

Category: docs

Why this matters: Wrong return docs cause callers to mishandle return values.

📍 View all locations
File Description Suggestion Confidence
examples/jsm/renderers/CSS2DRenderer.js:36-39 The element property JSDoc says '@default true' but element is an HTMLElement reference, not a boole... Remove '@default true' or change to '@default document.createElement("div")' 90%
examples/jsm/renderers/CSS3DRenderer.js:36-45 The element property JSDoc says '@default true' but element is an HTMLElement reference, not a boole... Remove '@default true' or change to '@default document.createElement("div")' 90%
src/core/Layers.js:121 The @return tag uses '{boolean }' with a space before closing brace, inconsistent with JSDoc convent... Use '@returns {boolean}' without extra space before closing brace 70%

Rule: ts_jsdoc_returns_mismatch


🟡 MEDIUM - Repeated material transparency checks

Agent: react

Category: quality

Why this matters: Reduces duplicate work and logic drift.

File: src/renderers/common/RenderList.js:288-323

Description: Material transparency logic is duplicated in push() and unshift() methods, checking material.transmission and related properties in identical patterns.

Suggestion: Extract material transparency check into a shared helper function to eliminate duplication and reduce maintenance risk when logic changes.

Confidence: 75%

Rule: ts_use_computed_derived_properties_for_repe


ℹ️ 9 issue(s) outside PR diff (click to expand)

These issues were found in lines not modified in this PR.

🟠 HIGH - JSDoc parameter description mismatch in disable() method (4 occurrences)

Agent: documentation

Category: docs

📍 View all locations
File Description Suggestion Confidence
src/core/Layers.js:87-88 The @param documentation says 'The layer to enable.' but the method actually disables a layer. This ... Change the JSDoc comment from 'The layer to enable.' to 'The layer to disable.' 95%
src/core/Object3D.js:980-983 The JSDoc says 'Returns a Quaternion representing the position of the 3D object in world space.' but... Change description to 'Returns a Quaternion representing the rotation of the 3D object in world spac... 95%
src/renderers/webgl-fallback/WebGLBackend.js:41 The constructor JSDoc says 'Constructs a new WebGPU backend.' but this is the WebGLBackend class. Th... Change 'Constructs a new WebGPU backend.' to 'Constructs a new WebGL backend.' 95%
src/core/Object3D.js:647 JSDoc says 'word space' instead of 'world space'. Fix typo: change 'word space' to 'world space' 95%

Rule: ts_jsdoc_description_mismatch


🟠 HIGH - Object3D.toJSON() missing layers.recursive serialization (2 occurrences)

Agent: typescript

Category: bug

📍 View all locations
File Description Suggestion Confidence
src/core/Object3D.js:1303 The toJSON() method only serializes layers.mask but not the new layers.recursive property. When seri... Change line 1303 from 'object.layers = this.layers.mask;' to serialize both properties. Consider 'ob... 95%
src/core/Object3D.js:1590 The copy() method only copies layers.mask but not layers.recursive. When cloning or copying an Objec... Add 'this.layers.recursive = source.layers.recursive;' after line 1590 to copy the recursive propert... 95%

Rule: ts_recursive_object_traversal_no_cycle_detection


🟡 MEDIUM - JSDoc default value mismatch for CSS2DObject element property (3 occurrences)

Agent: documentation

Category: docs

Why this matters: Wrong return docs cause callers to mishandle return values.

📍 View all locations
File Description Suggestion Confidence
examples/jsm/renderers/CSS2DRenderer.js:36-39 The element property JSDoc says '@default true' but element is an HTMLElement reference, not a boole... Remove '@default true' or change to '@default document.createElement("div")' 90%
examples/jsm/renderers/CSS3DRenderer.js:36-45 The element property JSDoc says '@default true' but element is an HTMLElement reference, not a boole... Remove '@default true' or change to '@default document.createElement("div")' 90%
src/core/Layers.js:121 The @return tag uses '{boolean }' with a space before closing brace, inconsistent with JSDoc convent... Use '@returns {boolean}' without extra space before closing brace 70%

Rule: ts_jsdoc_returns_mismatch



Review ID: 18426889-7dd5-4a37-bc33-d2c4c0357982
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

@wrangelvid
Copy link
Contributor Author

Who invited diffray-bot ?

@wrangelvid wrangelvid marked this pull request as ready for review December 29, 2025 14:30
Co-Authored-By: Cody Bennett <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants