Skip to content

Types: make ObservableVector3d/Vector3d assignable to 2D so Camera3d accepts pos.set(x, y, z) #1510

Description

@obiot

Summary

Make ObservableVector3d (and Vector3d) assignable to ObservableVector2d (Vector2d) so 3D renderables — notably Camera3d — can re-declare pos as 3D and accept pos.set(x, y, z) / pos.z in TypeScript, matching what already happens at runtime.

Problem

Renderable.pos is constructed as an ObservableVector3d at runtime:

// src/renderable/renderable.js
this.pos = new ObservableVector3d(x, y, 0, () => { ... });  // @type {ObservableVector3d}

…so pos.set(x, y, z) and pos.z are correct at runtime (and Renderable.depth is literally a proxy for pos.z). But the declared type of pos resolves to the 2D ObservableVector2d (inherited via the RectPolygon base chain), so TypeScript rejects the 3D form:

camera.pos.set(x, y, z);
// ts(2554): Expected 2 arguments, but got 3.

Consumers currently have to work around it with camera.pos.set(x, y); camera.depth = z; even though pos.set(x, y, z) is the natural, correct call for a 3D camera. (Came up while writing the glTF / night-city examples — Camera3d positioning.)

Why the obvious fix doesn't work

Re-declaring the property on the 3D camera:

// src/camera/camera3d.ts
declare pos: ObservableVector3d;

fails to compile:

error TS2416: Property 'pos' in type 'Camera3d' is not assignable to the same
property in base type 'Camera2d'.
  Types of property 'pos' are incompatible.

ObservableVector3d and ObservableVector2d are independent sibling classes — 3D is not assignable to 2D — so the override breaks the Liskov relationship with Camera2d (and cascades into lookAt). The fix has to happen at the vector-type level, not the camera.

Proposal

Restructure the observable (and plain) vector types so the 3D variant is a structural superset of the 2D one — i.e. ObservableVector3d is assignable to ObservableVector2d. Options to evaluate:

  1. ObservableVector3d extends ObservableVector2d (add z + 3D method overloads), so a 3D vector is-a 2D vector. Cleanest if the method signatures (set, add, clone, copy, return-this types, the Observable callback plumbing) can be made compatible. Same for Vector3d/Vector2d.
  2. Keep them separate but make the 3D set/members a superset signature that's structurally assignable to 2D (e.g. set(x?, y?, z?), z optional where the base expects 2D) — verify TS treats it as assignable.
  3. A shared base interface (Vector2dLike) that both implement and that APIs accept, if full assignability proves too invasive.

Then re-declare Camera3d.pos: ObservableVector3d (and consider Renderable.pos typed 3D generally, since it is 3D at runtime) and drop the pos.set(x,y) + depth = z workarounds.

Scope / risk

  • Touches the core math types used everywhere — needs a careful pass over Vector2d/Vector3d/ObservableVector2d/ObservableVector3d and every signature that returns this or takes a vector.
  • Must not change runtime behavior (pos is already 3D at runtime); this is a type-system correctness change.
  • Watch pool typings (vector2dPool / vector3dPool), Matrix*.apply(v) overloads, and anything doing "z" in v narrowing.
  • Verify no regression in the full type-check + test suite; check that 2D-only call sites still infer ObservableVector2d where intended.

Acceptance criteria

  • ObservableVector3d is assignable to ObservableVector2d (and Vector3d to Vector2d).
  • Camera3d (and ideally any 3D renderable) accepts pos.set(x, y, z) and pos.z with no error.
  • The pos.set(x, y) + depth = z workarounds in the glTF / night-city examples can be reverted to pos.set(x, y, z).
  • pnpm type-check + full test suite green; no runtime behavior change.

References

  • src/math/observableVector3d.ts (set(x = 0, y = 0, z = 0)), src/math/observableVector2d.ts
  • src/math/vector3d.ts, src/math/vector2d.ts
  • src/renderable/renderable.js (this.pos = new ObservableVector3d(...), @type {ObservableVector3d}; depthpos.z)
  • src/camera/camera3d.ts (the declare pos override that currently fails; lookAt)
  • Workaround sites: packages/examples/src/examples/gltf/ExampleGltf.tsx, packages/examples/src/examples/nightcity/ExampleNightCity.tsx

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions