Repository navigation
Description
What
Light3dOptions.direction and .position are typed [number, number, number] and read by index:
this.direction.set(
options.direction[0],
options.direction[1],
options.direction[2],
);A Vector3d has no [0], so passing one sets the direction to NaN three times, normalize() keeps it NaN, and the light contributes nothing. Silently — no warning, no throw, and the scene still renders with whatever other lights it has.
color on the same options object already accepts three forms — a Color, a CSS string, or an [r, g, b] array — and branches on the type. direction and position taking a Vector3d alongside the array would make the object consistent with itself.
Why it is worth doing
This is not hypothetical. Three shipped examples passed a Vector3d here (afterBurner, materialTextures, and the in-progress jungleRabbit), and none of them ever lit correctly. It went unnoticed for as long as it did because a second bug masked it — those examples also called new Light3d(0, 0, {…}), and since the constructor takes options alone, JavaScript dropped the object entirely and the lights ran on defaults. Fixing the argument count is what finally surfaced the NaN.
A Vector3d is the natural thing to reach for when an option is named direction in a 3D engine, so the mistake is an easy one to keep making.
Backward compatibility
Yes, and this is the main reason it is cheap:
- Callers — widening to
[number, number, number] | Vector3dis additive. Every existing array call still compiles and takes the identical code path. - Runtime — the branch is
Array.isArray(d) ? d[0] : d.x, so arrays behave exactly as they do now. - What the property HOLDS is unchanged —
Light3d#directionstays aVector3dthat the engine owns and normalizes. Only what the setting accepts widens; this is not a case of a setter quietly changing the type a property reads back as. - The only behaviour that changes is code that passes a
Vector3dtoday, which currently producesNaNand no light. Nothing can depend on that.
One caveat worth stating: downstream TypeScript that reads the option type directly (Light3dOptions["direction"]) and assumes an array — calling .map() on it, say — would stop compiling against the widened union. Unlikely, but it is a source-level break rather than a purely additive change, so it belongs in a minor rather than a patch.
Scope
Light3dOptions.directionand.positionwiden to[number, number, number] | Vector3d- the constructor branches like
coloralready does - a unit test per form, and one asserting the array path is byte-identical
- the JSDoc already documents the array-read trap and the Y-down sign; that note becomes "either form" instead
Found while fixing the lighting in the examples.
Activity
Light3d: take a Vector3d for direction and position (melonjs#1663)