Skip to content

Types: reconcile the 2D/3D position vector split across Renderable and the shape chain #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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions