Skip to content

Commit b0c50dc

Browse files
committed
render tests
1 parent 03ff3db commit b0c50dc

16 files changed

Lines changed: 203 additions & 42 deletions

‎docs/api-reference/core/viewport.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@ if (outDir) {
210210

211211
### Geometry Approximation
212212

213+
![subdivision](https://raw.githubusercontent.com/visgl/deck.gl-data/refs/heads/master/images/docs/viewport-resolution.png)
214+
215+
<small>Left to right: same polygons in resolution 0, 10, 60, rendering 487, 501, 591 outline vertices respectively.
216+
Notice the shape of the 49th-parallel.
217+
<br/><i>Natural Earth 1:110 country boundaries of continental Canada and United States. Albers projection, central metidian -96°, standard parallels 29.5° / 45.5°</i>
218+
</small>
219+
220+
213221
When a geometry is displayed in a different CRS from its definition, long segments are still rendered as straight lines, instead of warping along the surface of the new CRS.
214222

215223
Use `resolution` to divide long straight lines into smaller segments to better follow the shape of the target CRS. Subdivision adds vertices before projection so paths, polygon edges and bitmap meshes can follow projected curves more closely. Higher values produce finer detail, with more processing and memory overhead. Subdivision is by default disabled.

‎modules/core/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export {default as DeckRenderer} from './lib/deck-renderer';
3838

3939
// Viewports
4040
export {default as Viewport} from './viewports/viewport';
41+
export type {ProjectPositionOptions} from './viewports/viewport';
4142
export {default as WebMercatorViewport} from './viewports/web-mercator-viewport';
4243
export {default as _GlobeViewport} from './viewports/globe-viewport';
4344
export {default as OrbitViewport} from './viewports/orbit-viewport';

‎modules/layers/src/solid-polygon-layer/polygon-tesselator.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default class PolygonTesselator extends Tesselator<
4646
wrapLongitude?: boolean;
4747
preproject?: (xy: number[]) => number[];
4848
full3d?: boolean;
49+
geocentric?: boolean;
4950
}
5051
> {
5152
constructor(opts) {
@@ -215,7 +216,8 @@ export default class PolygonTesselator extends Tesselator<
215216
polygon,
216217
this.positionSize,
217218
this.opts.preproject,
218-
this.opts.full3d
219+
this.opts.full3d,
220+
this.opts.geocentric
219221
);
220222

221223
// make sure the buffer is large enough

‎modules/layers/src/solid-polygon-layer/polygon.ts‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ export function getSurfaceIndices(
309309
polygon: NormalizedPolygonGeometry,
310310
positionSize: number,
311311
preproject?: (xy: number[]) => number[],
312-
full3d?: boolean
312+
full3d?: boolean,
313+
geocentric?: boolean
313314
): number[] {
314315
let holeIndices = getHoleIndices(polygon);
315316
if (holeIndices) {
@@ -318,7 +319,15 @@ export function getSurfaceIndices(
318319

319320
let positions = getPositions(polygon);
320321

321-
const is3d = full3d && positionSize === 3;
322+
const is3d = full3d && (positionSize === 3 || geocentric);
323+
let triangulationSize = positionSize;
324+
if (is3d && positionSize === 2) {
325+
// Geocentric tessellation needs XYZ even when the packed attribute is XY.
326+
const xyz: number[] = [];
327+
for (let i = 0; i < positions.length; i += 2) xyz.push(positions[i], positions[i + 1], 0);
328+
positions = xyz;
329+
triangulationSize = 3;
330+
}
322331

323332
if (preproject) {
324333
// When tesselating lnglat coordinates, project them to the common space for accuracy
@@ -328,12 +337,12 @@ export function getSurfaceIndices(
328337
positions = positions.slice();
329338

330339
const p: number[] = [];
331-
for (let i = 0; i < n; i += positionSize) {
340+
for (let i = 0; i < n; i += triangulationSize) {
332341
p[0] = positions[i];
333342
p[1] = positions[i + 1];
334343

335344
if (is3d) {
336-
p[2] = positions[i + 2];
345+
p[2] = positions[i + 2] ?? 0;
337346
}
338347

339348
const xy = preproject(p);
@@ -347,6 +356,8 @@ export function getSurfaceIndices(
347356
}
348357
}
349358

359+
const geocentricPositions = geocentric ? positions.slice() : null;
360+
350361
if (is3d) {
351362
// calculate plane with largest area
352363
const xyArea = getPlaneArea(positions, 0, 1);
@@ -375,6 +386,23 @@ export function getSurfaceIndices(
375386
}
376387
}
377388

378-
// Let earcut triangulate the polygon
379-
return earcut(positions, holeIndices, positionSize);
389+
const indices = earcut(positions, holeIndices, triangulationSize);
390+
if (geocentricPositions) {
391+
// Globe views cull inward-facing triangles. A planar earcut winding can face inward
392+
// on either hemisphere; orient each triangle away from the geocentric origin.
393+
for (let i = 0; i < indices.length; i += 3) {
394+
const a = indices[i] * triangulationSize;
395+
const b = indices[i + 1] * triangulationSize;
396+
const c = indices[i + 2] * triangulationSize;
397+
const p = geocentricPositions;
398+
const orientation =
399+
p[a] * (p[b + 1] * p[c + 2] - p[b + 2] * p[c + 1]) +
400+
p[a + 1] * (p[b + 2] * p[c] - p[b] * p[c + 2]) +
401+
p[a + 2] * (p[b] * p[c + 1] - p[b + 1] * p[c]);
402+
if (orientation < 0) {
403+
[indices[i + 1], indices[i + 2]] = [indices[i + 2], indices[i + 1]];
404+
}
405+
}
406+
}
407+
return indices;
380408
}

‎modules/layers/src/solid-polygon-layer/solid-polygon-layer.ts‎

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -172,25 +172,9 @@ export default class SolidPolygonLayer<DataT = any, ExtraPropsT extends {} = {}>
172172
}
173173

174174
initializeState() {
175-
const {viewport} = this.context;
176-
const {_full3d} = this.props;
177-
178-
let preproject: ((xy: number[]) => number[]) | undefined;
179-
180-
if (this.state.packedCrs?.signature === 'WGS84') {
181-
if (_full3d) {
182-
preproject = viewport.projectPosition.bind(viewport);
183-
} else {
184-
preproject = viewport.projectFlat.bind(viewport);
185-
}
186-
}
187-
188175
this.setState({
189176
numInstances: 0,
190177
polygonTesselator: new PolygonTesselator({
191-
// Lnglat coordinates are usually projected non-linearly, which affects tesselation results
192-
// Provide a preproject function if the coordinates are in lnglat
193-
preproject,
194178
fp64: this.use64bitPositions(),
195179
IndexType: Uint32Array
196180
})
@@ -355,6 +339,9 @@ export default class SolidPolygonLayer<DataT = any, ExtraPropsT extends {} = {}>
355339
protected updateGeometry({props, oldProps, changeFlags}: UpdateParameters<this>) {
356340
const {viewport} = this.context;
357341
const subdivisionStep = this.getSubdivisionStep();
342+
const geocentric = Boolean(
343+
this.state.packedCrs && viewport.crs?.coordinateFrame === 'geocentric'
344+
);
358345
const projectionChanged =
359346
changeFlags.projectionChanged ||
360347
this.state.subdivisionStep !== subdivisionStep ||
@@ -380,13 +367,10 @@ export default class SolidPolygonLayer<DataT = any, ExtraPropsT extends {} = {}>
380367
buffers,
381368
getGeometry: props.getPolygon,
382369
transform: this.usePositionTransforms().transform?.bind(this),
383-
// Triangulate in the target projection even when CPU conversion packs WGS84.
384-
preproject:
385-
this.state.packedCrs?.signature === 'WGS84'
386-
? props._full3d
387-
? viewport.projectPosition.bind(viewport)
388-
: viewport.projectFlat.bind(viewport)
389-
: undefined,
370+
// Tessellation uses a temporary target-space copy; attributes retain their shader-supported CRS.
371+
preproject: this.state.packedCrs
372+
? position => viewport.projectPosition(position, {sourceCrs: this.state.packedCrs!})
373+
: undefined,
390374
positionFormat: props.positionFormat,
391375
// Longitude cutting assumes geographic input and cannot run on preprojected coordinates.
392376
wrapLongitude:
@@ -396,7 +380,9 @@ export default class SolidPolygonLayer<DataT = any, ExtraPropsT extends {} = {}>
396380
resolution: subdivisionStep,
397381
fp64: this.use64bitPositions(),
398382
dataChanged: projectionChanged ? undefined : changeFlags.dataChanged,
399-
full3d: props._full3d
383+
// A geocentric target needs a polygon plane selected from XYZ, even for flat source data.
384+
full3d: props._full3d || geocentric,
385+
geocentric
400386
});
401387

402388
this.setState({

‎test/data/crs/antarctica-coastline.geo.json‎

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

‎test/data/crs/antarctica-land.geo.json‎

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

‎test/data/crs/france-regions.geo.json‎

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

‎test/data/crs/switzerland-cantons.geo.json‎

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

‎test/modules/core/lib/source-crs.spec.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,16 @@ test('A layer in the custom viewport target CRS retains native attributes and GP
247247
manager.finalize();
248248
}
249249
});
250+
251+
test('Viewport projects a declared source CRS independently of attribute packing', () => {
252+
for (const viewport of [new WebMercatorViewport(), new GlobeViewport(), createTestViewport()]) {
253+
const geographic = [10, 20, 30];
254+
const sourcePosition = source.converter.WGS84.from(geographic);
255+
const expected = viewport.projectPosition(geographic);
256+
expect(viewport.projectPosition(sourcePosition, {sourceCrs: source})).toEqual(expected);
257+
expect(viewport.projectPosition(geographic, {sourceCrs: WGS84_CRS})).toEqual(expected);
258+
const targetPosition = viewport.crs!.converter.WGS84.from(geographic);
259+
const actual = viewport.projectPosition(targetPosition, {sourceCrs: viewport.crs!});
260+
for (let axis = 0; axis < 3; axis++) expect(actual[axis]).toBeCloseTo(expected[axis], 7);
261+
}
262+
});

0 commit comments

Comments
 (0)