Skip to content

Light3d: accept a Vector3d for direction and position, as color already accepts a Color #1661

Closed

Description

@obiot

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] | Vector3d is 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#direction stays a Vector3d that 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 Vector3d today, which currently produces NaN and 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.direction and .position widen to [number, number, number] | Vector3d
  • the constructor branches like color already 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

  1. added a commit that references this issue on Sep 13, 2026
    a9ed552
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions