From 0ab5914ab1adbf7b3d4158dc054d77b22b993905 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Thu, 18 Dec 2025 22:29:29 -0800 Subject: [PATCH 1/6] reproduceable viewports --- .../src/viewports/first-person-viewport.ts | 4 + modules/core/src/viewports/globe-viewport.ts | 8 +- modules/core/src/viewports/orbit-viewport.ts | 8 ++ .../src/viewports/orthographic-viewport.ts | 4 + .../core/viewports/conformance.spec.ts | 124 ++++++++++++++++++ test/modules/core/viewports/index.ts | 1 + 6 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 test/modules/core/viewports/conformance.spec.ts diff --git a/modules/core/src/viewports/first-person-viewport.ts b/modules/core/src/viewports/first-person-viewport.ts index a60a71f76ef..c7769938104 100644 --- a/modules/core/src/viewports/first-person-viewport.ts +++ b/modules/core/src/viewports/first-person-viewport.ts @@ -46,6 +46,8 @@ export type FirstPersonViewportOptions = { export default class FirstPersonViewport extends Viewport { longitude?: number; latitude?: number; + pitch: number; + bearing: number; constructor(props: FirstPersonViewportOptions) { // TODO - push direction handling into Matrix4.lookAt @@ -75,5 +77,7 @@ export default class FirstPersonViewport extends Viewport { this.latitude = latitude; this.longitude = longitude; + this.pitch = pitch; + this.bearing = bearing; } } diff --git a/modules/core/src/viewports/globe-viewport.ts b/modules/core/src/viewports/globe-viewport.ts index 6275ea1a148..e700bd0e24e 100644 --- a/modules/core/src/viewports/globe-viewport.ts +++ b/modules/core/src/viewports/globe-viewport.ts @@ -67,9 +67,10 @@ export type GlobeViewportOptions = { }; export default class GlobeViewport extends Viewport { - longitude!: number; - latitude!: number; - resolution!: number; + longitude: number; + latitude: number; + fovy: number; + resolution: number; constructor(opts: GlobeViewportOptions = {}) { const { @@ -129,6 +130,7 @@ export default class GlobeViewport extends Viewport { this.scale = scale; this.latitude = latitude; this.longitude = longitude; + this.fovy = fovy; this.resolution = resolution; } diff --git a/modules/core/src/viewports/orbit-viewport.ts b/modules/core/src/viewports/orbit-viewport.ts index 333f6afaaf4..792ec59ecbb 100644 --- a/modules/core/src/viewports/orbit-viewport.ts +++ b/modules/core/src/viewports/orbit-viewport.ts @@ -87,6 +87,10 @@ export type OrbitViewportOptions = { export default class OrbitViewport extends Viewport { projectedCenter: number[]; + orbitAxis: 'Y' | 'Z'; + rotationOrbit: number; + rotationX: number; + target: [number, number, number]; constructor(props: OrbitViewportOptions) { const { @@ -125,6 +129,10 @@ export default class OrbitViewport extends Viewport { zoom }); + this.target = target; + this.orbitAxis = orbitAxis; + this.rotationX = rotationX; + this.rotationOrbit = rotationOrbit; this.projectedCenter = this.project(this.center); } diff --git a/modules/core/src/viewports/orthographic-viewport.ts b/modules/core/src/viewports/orthographic-viewport.ts index c97635c339a..3ca27dc910f 100644 --- a/modules/core/src/viewports/orthographic-viewport.ts +++ b/modules/core/src/viewports/orthographic-viewport.ts @@ -75,6 +75,8 @@ export type OrthographicViewportOptions = { }; export default class OrthographicViewport extends Viewport { + target: [number, number, number]; + constructor(props: OrthographicViewportOptions) { const { width, @@ -119,6 +121,8 @@ export default class OrthographicViewport extends Viewport { zoom: zoom_, distanceScales }); + + this.target = [target[0], target[1], target[2] ?? 0]; } projectFlat([X, Y]: number[]): [number, number] { diff --git a/test/modules/core/viewports/conformance.spec.ts b/test/modules/core/viewports/conformance.spec.ts new file mode 100644 index 00000000000..bd12288a22e --- /dev/null +++ b/test/modules/core/viewports/conformance.spec.ts @@ -0,0 +1,124 @@ +import test from 'tape-promise/tape'; +import { + type Viewport, + WebMercatorViewport, + OrthographicViewport, + OrbitViewport, + _GlobeViewport as GlobeViewport, + FirstPersonViewport +} from '@deck.gl/core'; + +test('Viewport#recreate', t => { + const TEST_CASES = [ + { + Type: WebMercatorViewport, + props: { + width: 100, + height: 100 + } + }, + { + Type: WebMercatorViewport, + props: { + width: 400, + height: 300, + longitude: -122.4, + latitude: 37.8, + fovy: 50, + zoom: 12, + pitch: 24, + bearing: -160, + position: [0, 0, 2] + } + }, + { + Type: WebMercatorViewport, + props: { + width: 400, + height: 300, + longitude: -122.4, + latitude: 37.8, + zoom: 12, + nearZ: 0.01, + farZMultiplier: 10 + } + }, + { + Type: OrbitViewport, + props: { + width: 100, + height: 100 + } + }, + { + Type: OrbitViewport, + props: { + width: 400, + height: 300, + target: [-10.24, 2833, 47.2], + orbitAxis: 'Y', + rotationX: 45, + rotationAxis: -111, + zoom: -3 + } + }, + { + Type: OrthographicViewport, + props: { + width: 100, + height: 100 + } + }, + { + Type: OrthographicViewport, + props: { + width: 400, + height: 300, + target: [100, -50], + zoom: 4.3 + } + }, + { + Type: GlobeViewport, + props: { + width: 100, + height: 100 + } + }, + { + Type: GlobeViewport, + props: { + width: 400, + height: 300, + longitude: -122.4, + latitude: 37.8, + fovy: 50, + zoom: 12 + } + }, + { + Type: FirstPersonViewport, + props: { + width: 100, + height: 100 + } + }, + { + Type: FirstPersonViewport, + props: { + width: 400, + height: 300, + longitude: -122.4, + latitude: 37.8, + pitch: 35, + bearing: -140, + zoom: 12 + } + } + ]; + for (const {Type, props} of TEST_CASES) { + const viewport1 = new Type(props as any) as Viewport; + const viewport2 = new Type({...viewport1}) as Viewport; + t.ok(viewport1.equals(viewport2), String(Type.name)); + } +}); diff --git a/test/modules/core/viewports/index.ts b/test/modules/core/viewports/index.ts index 5abc9a3b7a3..e29514a9d24 100644 --- a/test/modules/core/viewports/index.ts +++ b/test/modules/core/viewports/index.ts @@ -6,3 +6,4 @@ import './viewport.spec'; import './globe-viewport.spec'; import './web-mercator-project-unproject.spec'; import './web-mercator-viewport.spec'; +import './conformance.spec'; From 9775badf7d0eb84dcebcd24ad77bac7d0bb17b5b Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Thu, 18 Dec 2025 23:01:21 -0800 Subject: [PATCH 2/6] add displayname --- .../src/viewports/first-person-viewport.ts | 2 + modules/core/src/viewports/globe-viewport.ts | 2 + modules/core/src/viewports/orbit-viewport.ts | 2 + .../src/viewports/orthographic-viewport.ts | 18 +- .../core/viewports/conformance.spec.ts | 185 +++++++----------- 5 files changed, 96 insertions(+), 113 deletions(-) diff --git a/modules/core/src/viewports/first-person-viewport.ts b/modules/core/src/viewports/first-person-viewport.ts index c7769938104..7fa045b39b8 100644 --- a/modules/core/src/viewports/first-person-viewport.ts +++ b/modules/core/src/viewports/first-person-viewport.ts @@ -44,6 +44,8 @@ export type FirstPersonViewportOptions = { }; export default class FirstPersonViewport extends Viewport { + static displayName = 'FirstPersonViewport'; + longitude?: number; latitude?: number; pitch: number; diff --git a/modules/core/src/viewports/globe-viewport.ts b/modules/core/src/viewports/globe-viewport.ts index e700bd0e24e..5a064a09e06 100644 --- a/modules/core/src/viewports/globe-viewport.ts +++ b/modules/core/src/viewports/globe-viewport.ts @@ -67,6 +67,8 @@ export type GlobeViewportOptions = { }; export default class GlobeViewport extends Viewport { + static displayName = 'GlobeViewport'; + longitude: number; latitude: number; fovy: number; diff --git a/modules/core/src/viewports/orbit-viewport.ts b/modules/core/src/viewports/orbit-viewport.ts index 792ec59ecbb..c080a7c9074 100644 --- a/modules/core/src/viewports/orbit-viewport.ts +++ b/modules/core/src/viewports/orbit-viewport.ts @@ -86,6 +86,8 @@ export type OrbitViewportOptions = { }; export default class OrbitViewport extends Viewport { + static displayName = 'OrbitViewport'; + projectedCenter: number[]; orbitAxis: 'Y' | 'Z'; rotationOrbit: number; diff --git a/modules/core/src/viewports/orthographic-viewport.ts b/modules/core/src/viewports/orthographic-viewport.ts index 3ca27dc910f..63544bb849a 100644 --- a/modules/core/src/viewports/orthographic-viewport.ts +++ b/modules/core/src/viewports/orthographic-viewport.ts @@ -64,6 +64,10 @@ export type OrthographicViewportOptions = { /** The zoom level of the viewport. `zoom: 0` maps one unit distance to one pixel on screen, and increasing `zoom` by `1` scales the same object to twice as large. * To apply independent zoom levels to the X and Y axes, supply an array `[zoomX, zoomY]`. Default `0`. */ zoom?: number | [number, number]; + /** Independent zoom along the X axis. Overrides `zoom`. */ + zoomX?: number; + /** Independent zoom along the Y axis. Overrides `zoom`. */ + zoomY?: number; /** Padding around the viewport, in pixels. */ padding?: Padding | null; /** Distance of near clipping plane. Default `0.1`. */ @@ -75,7 +79,11 @@ export type OrthographicViewportOptions = { }; export default class OrthographicViewport extends Viewport { - target: [number, number, number]; + static displayName = 'OrthographicViewport'; + + target: [number, number, number] | [number, number]; + zoomX: number; + zoomY: number; constructor(props: OrthographicViewportOptions) { const { @@ -88,8 +96,8 @@ export default class OrthographicViewport extends Viewport { padding = null, flipY = true } = props; - const zoomX = Array.isArray(zoom) ? zoom[0] : zoom; - const zoomY = Array.isArray(zoom) ? zoom[1] : zoom; + const zoomX = props.zoomX ?? (Array.isArray(zoom) ? zoom[0] : zoom); + const zoomY = props.zoomY ?? (Array.isArray(zoom) ? zoom[1] : zoom); const zoom_ = Math.min(zoomX, zoomY); const scale = Math.pow(2, zoom_); @@ -122,7 +130,9 @@ export default class OrthographicViewport extends Viewport { distanceScales }); - this.target = [target[0], target[1], target[2] ?? 0]; + this.target = target; + this.zoomX = zoomX; + this.zoomY = zoomY; } projectFlat([X, Y]: number[]): [number, number] { diff --git a/test/modules/core/viewports/conformance.spec.ts b/test/modules/core/viewports/conformance.spec.ts index bd12288a22e..a31925c72cf 100644 --- a/test/modules/core/viewports/conformance.spec.ts +++ b/test/modules/core/viewports/conformance.spec.ts @@ -10,115 +10,82 @@ import { test('Viewport#recreate', t => { const TEST_CASES = [ - { - Type: WebMercatorViewport, - props: { - width: 100, - height: 100 - } - }, - { - Type: WebMercatorViewport, - props: { - width: 400, - height: 300, - longitude: -122.4, - latitude: 37.8, - fovy: 50, - zoom: 12, - pitch: 24, - bearing: -160, - position: [0, 0, 2] - } - }, - { - Type: WebMercatorViewport, - props: { - width: 400, - height: 300, - longitude: -122.4, - latitude: 37.8, - zoom: 12, - nearZ: 0.01, - farZMultiplier: 10 - } - }, - { - Type: OrbitViewport, - props: { - width: 100, - height: 100 - } - }, - { - Type: OrbitViewport, - props: { - width: 400, - height: 300, - target: [-10.24, 2833, 47.2], - orbitAxis: 'Y', - rotationX: 45, - rotationAxis: -111, - zoom: -3 - } - }, - { - Type: OrthographicViewport, - props: { - width: 100, - height: 100 - } - }, - { - Type: OrthographicViewport, - props: { - width: 400, - height: 300, - target: [100, -50], - zoom: 4.3 - } - }, - { - Type: GlobeViewport, - props: { - width: 100, - height: 100 - } - }, - { - Type: GlobeViewport, - props: { - width: 400, - height: 300, - longitude: -122.4, - latitude: 37.8, - fovy: 50, - zoom: 12 - } - }, - { - Type: FirstPersonViewport, - props: { - width: 100, - height: 100 - } - }, - { - Type: FirstPersonViewport, - props: { - width: 400, - height: 300, - longitude: -122.4, - latitude: 37.8, - pitch: 35, - bearing: -140, - zoom: 12 - } - } + new WebMercatorViewport({ + width: 100, + height: 100 + }), + new WebMercatorViewport({ + width: 400, + height: 300, + longitude: -122.4, + latitude: 37.8, + fovy: 50, + zoom: 12, + pitch: 24, + bearing: -160, + position: [0, 0, 2] + }), + new WebMercatorViewport({ + width: 400, + height: 300, + longitude: -122.4, + latitude: 37.8, + zoom: 12, + nearZ: 0.01, + farZMultiplier: 10 + }), + new OrbitViewport({ + width: 100, + height: 100 + }), + new OrbitViewport({ + width: 400, + height: 300, + target: [-10.24, 2833, 47.2], + orbitAxis: 'Y', + rotationX: 45, + rotationOrbit: -111, + zoom: -3 + }), + new OrthographicViewport({ + width: 100, + height: 100 + }), + new OrthographicViewport({ + width: 400, + height: 300, + target: [100, 500], + zoom: [1, -4] + }), + new GlobeViewport({ + width: 100, + height: 100 + }), + new GlobeViewport({ + width: 400, + height: 300, + longitude: -122.4, + latitude: 37.8, + fovy: 50, + zoom: 12 + }), + new FirstPersonViewport({ + width: 100, + height: 100 + }), + new FirstPersonViewport({ + width: 400, + height: 300, + longitude: -122.4, + latitude: 37.8, + pitch: 35, + bearing: -140, + focalDistance: 2 + }) ]; - for (const {Type, props} of TEST_CASES) { - const viewport1 = new Type(props as any) as Viewport; - const viewport2 = new Type({...viewport1}) as Viewport; - t.ok(viewport1.equals(viewport2), String(Type.name)); + for (const viewport of TEST_CASES) { + const ViewportType = viewport.constructor as {new (props: unknown): Viewport}; + const clone = new ViewportType({...viewport}); + t.ok(viewport.equals(clone), String(viewport.id)); } }); From 6924e5b52d8dea945f0c54b08b62989b1317af2a Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 19 Dec 2025 11:16:48 -0800 Subject: [PATCH 3/6] Apply suggestion from @chrisgervang --- test/modules/core/viewports/conformance.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/modules/core/viewports/conformance.spec.ts b/test/modules/core/viewports/conformance.spec.ts index a31925c72cf..10be5944e4d 100644 --- a/test/modules/core/viewports/conformance.spec.ts +++ b/test/modules/core/viewports/conformance.spec.ts @@ -88,4 +88,5 @@ test('Viewport#recreate', t => { const clone = new ViewportType({...viewport}); t.ok(viewport.equals(clone), String(viewport.id)); } + t.end(); }); From dc19b44a52e39a920c3e703298481b631f20e57d Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Mon, 5 Jan 2026 10:10:12 -0800 Subject: [PATCH 4/6] Update test/modules/core/viewports/conformance.spec.ts Co-authored-by: Chris Gervang --- test/modules/core/viewports/conformance.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/modules/core/viewports/conformance.spec.ts b/test/modules/core/viewports/conformance.spec.ts index 10be5944e4d..5f242a4baf1 100644 --- a/test/modules/core/viewports/conformance.spec.ts +++ b/test/modules/core/viewports/conformance.spec.ts @@ -55,7 +55,8 @@ test('Viewport#recreate', t => { width: 400, height: 300, target: [100, 500], - zoom: [1, -4] + zoom: [1, -4], + flipY: false }), new GlobeViewport({ width: 100, From 3a0b84db1aa08864f6e94cbffa58fd0114fa3204 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Mon, 5 Jan 2026 10:10:25 -0800 Subject: [PATCH 5/6] Update test/modules/core/viewports/conformance.spec.ts Co-authored-by: Chris Gervang --- test/modules/core/viewports/conformance.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/modules/core/viewports/conformance.spec.ts b/test/modules/core/viewports/conformance.spec.ts index 5f242a4baf1..1278ba39681 100644 --- a/test/modules/core/viewports/conformance.spec.ts +++ b/test/modules/core/viewports/conformance.spec.ts @@ -45,7 +45,8 @@ test('Viewport#recreate', t => { orbitAxis: 'Y', rotationX: 45, rotationOrbit: -111, - zoom: -3 + zoom: -3, + fovy: 60 }), new OrthographicViewport({ width: 100, From e37e0c845646f24bb8313267aa52d786ba392d4f Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Mon, 5 Jan 2026 10:27:34 -0800 Subject: [PATCH 6/6] additional tests --- modules/core/src/viewports/first-person-viewport.ts | 2 ++ modules/core/src/viewports/orbit-viewport.ts | 2 ++ modules/core/src/viewports/orthographic-viewport.ts | 2 ++ 3 files changed, 6 insertions(+) diff --git a/modules/core/src/viewports/first-person-viewport.ts b/modules/core/src/viewports/first-person-viewport.ts index 7fa045b39b8..79e0d8fb958 100644 --- a/modules/core/src/viewports/first-person-viewport.ts +++ b/modules/core/src/viewports/first-person-viewport.ts @@ -50,6 +50,7 @@ export default class FirstPersonViewport extends Viewport { latitude?: number; pitch: number; bearing: number; + up: [number, number, number]; constructor(props: FirstPersonViewportOptions) { // TODO - push direction handling into Matrix4.lookAt @@ -81,5 +82,6 @@ export default class FirstPersonViewport extends Viewport { this.longitude = longitude; this.pitch = pitch; this.bearing = bearing; + this.up = up; } } diff --git a/modules/core/src/viewports/orbit-viewport.ts b/modules/core/src/viewports/orbit-viewport.ts index c080a7c9074..0e615c68d4a 100644 --- a/modules/core/src/viewports/orbit-viewport.ts +++ b/modules/core/src/viewports/orbit-viewport.ts @@ -93,6 +93,7 @@ export default class OrbitViewport extends Viewport { rotationOrbit: number; rotationX: number; target: [number, number, number]; + fovy: number; constructor(props: OrbitViewportOptions) { const { @@ -135,6 +136,7 @@ export default class OrbitViewport extends Viewport { this.orbitAxis = orbitAxis; this.rotationX = rotationX; this.rotationOrbit = rotationOrbit; + this.fovy = fovy; this.projectedCenter = this.project(this.center); } diff --git a/modules/core/src/viewports/orthographic-viewport.ts b/modules/core/src/viewports/orthographic-viewport.ts index 63544bb849a..bef4a6ff856 100644 --- a/modules/core/src/viewports/orthographic-viewport.ts +++ b/modules/core/src/viewports/orthographic-viewport.ts @@ -84,6 +84,7 @@ export default class OrthographicViewport extends Viewport { target: [number, number, number] | [number, number]; zoomX: number; zoomY: number; + flipY: boolean; constructor(props: OrthographicViewportOptions) { const { @@ -133,6 +134,7 @@ export default class OrthographicViewport extends Viewport { this.target = target; this.zoomX = zoomX; this.zoomY = zoomY; + this.flipY = flipY; } projectFlat([X, Y]: number[]): [number, number] {