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 Rect → Polygon 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:
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.
- 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.
- 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}; depth ↔ pos.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
Summary
Make
ObservableVector3d(andVector3d) assignable toObservableVector2d(Vector2d) so 3D renderables — notablyCamera3d— can re-declareposas 3D and acceptpos.set(x, y, z)/pos.zin TypeScript, matching what already happens at runtime.Problem
Renderable.posis constructed as anObservableVector3dat runtime:…so
pos.set(x, y, z)andpos.zare correct at runtime (andRenderable.depthis literally a proxy forpos.z). But the declared type ofposresolves to the 2DObservableVector2d(inherited via theRect→Polygonbase chain), so TypeScript rejects the 3D form:Consumers currently have to work around it with
camera.pos.set(x, y); camera.depth = z;even thoughpos.set(x, y, z)is the natural, correct call for a 3D camera. (Came up while writing the glTF / night-city examples —Camera3dpositioning.)Why the obvious fix doesn't work
Re-declaring the property on the 3D camera:
fails to compile:
ObservableVector3dandObservableVector2dare independent sibling classes — 3D is not assignable to 2D — so the override breaks the Liskov relationship withCamera2d(and cascades intolookAt). 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.
ObservableVector3dis assignable toObservableVector2d. Options to evaluate:ObservableVector3d extends ObservableVector2d(addz+ 3D method overloads), so a 3D vector is-a 2D vector. Cleanest if the method signatures (set,add,clone,copy, return-thistypes, the Observable callback plumbing) can be made compatible. Same forVector3d/Vector2d.set/members a superset signature that's structurally assignable to 2D (e.g.set(x?, y?, z?),zoptional where the base expects 2D) — verify TS treats it as assignable.Vector2dLike) that both implement and that APIs accept, if full assignability proves too invasive.Then re-declare
Camera3d.pos: ObservableVector3d(and considerRenderable.postyped 3D generally, since it is 3D at runtime) and drop thepos.set(x,y) + depth = zworkarounds.Scope / risk
Vector2d/Vector3d/ObservableVector2d/ObservableVector3dand every signature that returnsthisor takes a vector.pooltypings (vector2dPool/vector3dPool),Matrix*.apply(v)overloads, and anything doing"z" in vnarrowing.ObservableVector2dwhere intended.Acceptance criteria
ObservableVector3dis assignable toObservableVector2d(andVector3dtoVector2d).Camera3d(and ideally any 3D renderable) acceptspos.set(x, y, z)andpos.zwith no error.pos.set(x, y) + depth = zworkarounds in the glTF / night-city examples can be reverted topos.set(x, y, z).pnpmtype-check + full test suite green; no runtime behavior change.References
src/math/observableVector3d.ts(set(x = 0, y = 0, z = 0)),src/math/observableVector2d.tssrc/math/vector3d.ts,src/math/vector2d.tssrc/renderable/renderable.js(this.pos = new ObservableVector3d(...),@type {ObservableVector3d};depth↔pos.z)src/camera/camera3d.ts(thedeclare posoverride that currently fails;lookAt)packages/examples/src/examples/gltf/ExampleGltf.tsx,packages/examples/src/examples/nightcity/ExampleNightCity.tsx