feat(r3f): add material rendering diagnostics - #1633
Conversation
commit: |
Interactive terminal E2ETerminal Control verified the built CLI at
|
|
React Doctor skipped this pull request — it changed no React files. Reviewed by React Doctor for commit |
4989434 to
08eba2b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 08eba2b. Configure here.
| THREE_LIGHT_CONSTRUCTOR_NAMES.has(constructorName) && | ||
| (intensity === null || intensity > 0) | ||
| ) { | ||
| analysis.hasLight = true; |
There was a problem hiding this comment.
R3F lights ignore intensity ambiguity
High Severity
analyzeClosedR3fCanvasLighting treats a missing intensity and a non-static intensity the same: both become null and still set hasLight. A Canvas with intensity={amount} is therefore marked lit and stays complete, so r3f-require-lighting-for-pbr abstains instead of failing closed-scene analysis. The paired Three.js helper correctly marks non-static intensity incomplete.
Reviewed by Cursor Bugbot for commit 08eba2b. Configure here.
| const metalnessExpression = getStaticObjectPropertyValue(unwrappedParameters, "metalness"); | ||
| metalness = metalnessExpression ? getStaticNumber(metalnessExpression, context.scopes) : null; | ||
| const opacityExpression = getStaticObjectPropertyValue(unwrappedParameters, "opacity"); | ||
| opacity = opacityExpression ? getStaticNumber(opacityExpression, context.scopes) : null; |
There was a problem hiding this comment.
Dynamic opacity skips PBR abstention
Medium Severity
getStaticThreePbrMaterialLighting assigns non-static opacity to null without setting isComplete to false, both for constructor parameters and later writes. With transparent: true, isVisible stays true, so lighting and metal rules can flag fully transparent surfaces. The paired texture-property analyzer already abstains on the same shape.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 08eba2b. Configure here.
| context, | ||
| ); | ||
| if (material && (!material.isComplete || material.isVisible)) materials.push(material); | ||
| continue; |
There was a problem hiding this comment.
Unparsed mesh materials stay complete
Medium Severity
When a closed-scene Mesh material argument is an array, call, or otherwise fails getStaticThreePbrMaterialLighting, the analyzer skips it and leaves isComplete true. Material arrays such as new Mesh(geo, [new MeshStandardMaterial()]) therefore never contribute PBR facts and never force abstention, so lighting and metal diagnostics miss provable defects.
Reviewed by Cursor Bugbot for commit 08eba2b. Configure here.


Why
Three.js and React Three Fiber projects could pass static analysis while custom mapped geometry had no UVs, normal-mapped custom geometry had no normals, PBR materials had no lighting source, or strongly metallic materials had no environment reflections.
Before, these defects were left entirely to runtime grading. After, React Doctor reports them only when the local geometry/material/scene graph is closed and statically provable. Dynamic scenes, spreads, custom components, shader materials, hidden surfaces, externally mutable geometry, and ambiguous attachments abstain.
Three.js r183 added derivative-based flat-shading fallback for ordinary lit geometry without normals, so the normals rules deliberately target only active
normalMapcases instead of producing obsolete missing-normal warnings.What changed
@react-doctor/fuzzis private.Eval results
+0 / -0, 1,177,984 unchanged diagnostics, 0 skippedThe exact parity run compared base
8dfb01306772760201e75ea1478368390eddf58fwith detector headef85e01bde055320957198c1951b605f93195b11. Current head08eba2bdd1e30ac1739b7fa65a7fff8f6f0eac4cdiffers only by private fuzz seeds and their test; detector and runtime code are byte-identical to the parity candidate.Test plan
nr test— 15/15 monorepo tasks passednr lint— passednr typecheck— 16/16 tasks passednr format:check— passed across 6,500 filesnr smoke:json-report— schema v3 full-mode smoke passednr -C packages/fuzz test— 216 passed, 884 intentionally skippedFUZZ_STRICT=1 FUZZ_REQUIRE_FIRE=1 FUZZ_ITERATIONS=1000 nr fuzz— passed for each of the 8 new rulesnr -C packages/oxlint-plugin-react-doctor gen:checknpx react-doctor@latest --verbose --scope changedNote
Medium Risk
Adds substantial new static analysis over Three.js/R3F scene and material graphs. Risk is mainly false positives or missed abstentions, mitigated by closed-graph gating and extensive fuzz/parity coverage.
Overview
Adds eight paired Three.js / R3F diagnostics for material and geometry setup defects that previously needed runtime grading: missing UVs on mapped custom geometry, normal-mapped custom geometry without normals, unlit PBR materials, and strongly metallic materials without environment lighting.
Rules fire only on closed, locally provable meshes/scenes/canvases. Spreads, custom components, shaders, hidden surfaces, dynamic property writes, and open graphs abstain. Normals rules target active
normalMaponly, matching Three.js r183’s flat-shading fallback.Also adds shared closed-graph analyzers, registry/liveness wiring, fuzz corpus seeds, and a grading-rubric coverage doc that maps each probe to static vs runtime/craft evidence.
Reviewed by Cursor Bugbot for commit 08eba2b. Bugbot is set up for automated code reviews on this repo. Configure here.