Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@luma.gl/engine": "^9.2.4",
"@luma.gl/shadertools": "^9.2.4",
"@luma.gl/webgl": "^9.2.4",
"@math.gl/culling": "^4.1.0",
"@math.gl/core": "^4.1.0",
"@math.gl/sun": "^4.1.0",
"@math.gl/types": "^4.1.0",
Expand Down
1 change: 1 addition & 0 deletions modules/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {default as AttributeManager} from './lib/attribute/attribute-manager';
export {default as Layer} from './lib/layer';
export {default as CompositeLayer} from './lib/composite-layer';
export {default as DeckRenderer} from './lib/deck-renderer';
export {transformBoundsToWorld} from './lib/world-bounds';

// Viewports
export {default as Viewport} from './viewports/viewport';
Expand Down
74 changes: 74 additions & 0 deletions modules/core/src/lib/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import memoize from '../utils/memoize';
import {mergeShaders} from '../utils/shader';
import {projectPosition, getWorldPosition} from '../shaderlib/project/project-functions';
import typedArrayManager from '../utils/typed-array-manager';
import {deepEqual} from '../utils/deep-equal';

import Component from '../lifecycle/component';
import LayerState, {ChangeFlags} from './layer-state';

import {worldToPixels} from '@math.gl/web-mercator';
import {OrientedBoundingBox} from '@math.gl/culling';

import {load} from '@loaders.gl/core';

Expand All @@ -39,6 +41,7 @@ import type {LayerContext} from './layer-manager';
import type {BinaryAttribute} from './attribute/attribute';
import {RenderPass} from '@luma.gl/core';
import {PickingProps} from '@luma.gl/shadertools';
import {transformBoundsToWorld, WorldBoundsTransformOptions} from './world-bounds';

const TRACE_CHANGE_FLAG = 'layer.changeFlag';
const TRACE_INITIALIZE = 'layer.initialize';
Expand Down Expand Up @@ -201,6 +204,13 @@ export default abstract class Layer<PropsT extends {} = {}> extends Component<
state!: SharedLayerState; // Will be set to the shared layer state object during layer matching

parent: Layer | null = null;
private _cachedLocalBounds: [number[], number[]] | null = null;
private _cachedWorldBounds: OrientedBoundingBox | null = null;
private _cachedWorldBoundsCacheKey: unknown;
private _cachedWorldBoundsProps: Omit<WorldBoundsTransformOptions, 'bounds' | 'viewport'> | null =
null;
private _worldBoundsViewportId?: string;
private _worldBoundsDirty: boolean = true;

get root(): Layer {
// eslint-disable-next-line
Expand Down Expand Up @@ -446,6 +456,67 @@ export default abstract class Layer<PropsT extends {} = {}> extends Component<
return this.getAttributeManager()?.getBounds(['positions', 'instancePositions']);
}

// Default implementation
getWorldBounds(): OrientedBoundingBox | null {
const options = this.getWorldBoundsOptions();
if (!options) {
return null;
}

const {bounds, viewport, worldBoundsCacheKey, ...transformOptions} = options;
const cacheKey = worldBoundsCacheKey ?? null;
const viewportId = viewport?.id;
const useCache =
!this._worldBoundsDirty &&
this._cachedWorldBounds &&
deepEqual(bounds, this._cachedLocalBounds, 3) &&
deepEqual(transformOptions, this._cachedWorldBoundsProps, 3) &&
deepEqual(cacheKey, this._cachedWorldBoundsCacheKey, 3) &&
viewportId === this._worldBoundsViewportId;

if (useCache) {
return this._cachedWorldBounds;
}

const worldBounds = transformBoundsToWorld({
bounds,
viewport,
...transformOptions
});

this._cachedWorldBounds = worldBounds;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core has a memoize utility function.

this._cachedLocalBounds = bounds;
this._cachedWorldBoundsProps = transformOptions;
this._cachedWorldBoundsCacheKey = cacheKey;
this._worldBoundsViewportId = viewportId;
this._worldBoundsDirty = false;

return worldBounds;
}

/**
* Override to customize how world bounds are generated and cached.
* Add `worldBoundsCacheKey` to the returned object if additional props affect bounds,
* e.g. radius or scale.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected getWorldBoundsOptions(): WorldBoundsTransformOptions | null {
const bounds = this.getBounds();
const viewport = this.internalState?.viewport || this.context.viewport;
if (!bounds || !viewport) {
return null;
}

const {coordinateOrigin, coordinateSystem, modelMatrix} = this.props;
return {
bounds,
viewport,
coordinateOrigin,
coordinateSystem,
modelMatrix
};
}

// / LIFECYCLE METHODS - overridden by the layer subclasses

/** Called once to set up the initial state. Layers can create WebGL resources here. */
Expand All @@ -472,6 +543,9 @@ export default abstract class Layer<PropsT extends {} = {}> extends Component<
updateState(params: UpdateParameters<Layer<PropsT>>): void {
const attributeManager = this.getAttributeManager();
const {dataChanged} = params.changeFlags;
if (params.changeFlags.propsOrDataChanged || params.changeFlags.viewportChanged) {
this._worldBoundsDirty = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically incorrect.

There are multiple existing caching mechanism (e.g. in layer.getBounds()) you should not have to manage a dirty flag manually.

}
if (dataChanged && attributeManager) {
if (Array.isArray(dataChanged)) {
// is partial update
Expand Down
54 changes: 54 additions & 0 deletions modules/core/src/lib/world-bounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {projectPosition} from '../shaderlib/project/project-functions';
import {makeOrientedBoundingBoxFromPoints, OrientedBoundingBox} from '@math.gl/culling';

import type Viewport from '../viewports/viewport';
import type {CoordinateSystem} from './constants';
import type {NumericArray} from '../types/types';

export type WorldBoundsTransformOptions = {
bounds: [number[], number[]];
viewport: Viewport;
coordinateSystem: CoordinateSystem;
coordinateOrigin: [number, number, number];
modelMatrix?: NumericArray | null;
/** Additional props that affect bounds transformation and caching */
worldBoundsCacheKey?: unknown;
};

export function transformBoundsToWorld(options: WorldBoundsTransformOptions): OrientedBoundingBox {
const {bounds, viewport, coordinateOrigin, coordinateSystem, modelMatrix} = options;
const corners = getBoundingBoxCorners(bounds);

const worldPositions = corners.map(position =>
projectPosition(position, {
viewport,
coordinateSystem,
coordinateOrigin,
modelMatrix
})
);

return makeOrientedBoundingBoxFromPoints(worldPositions);
}

function getBoundingBoxCorners(bounds: [number[], number[]]): number[][] {
const [min, max] = bounds;
const dimension = Math.max(min.length, max.length, 3);
const corners: number[][] = [];

for (let i = 0; i < 8; i++) {
const corner: number[] = [];
for (let axis = 0; axis < 3; axis++) {
const minValue = axis < dimension ? (min[axis] ?? 0) : 0;
const maxValue = axis < dimension ? (max[axis] ?? minValue) : minValue;
corner[axis] = i & (1 << axis) ? maxValue : minValue;
}
corners.push(corner);
}

return corners;
}
1 change: 1 addition & 0 deletions modules/main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export {
fp64LowPart,
createIterable,
getShaderAssembler,
transformBoundsToWorld,
// Widgets
Widget
} from '@deck.gl/core';
Expand Down
1 change: 1 addition & 0 deletions test/modules/core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ import './pick-layers.spec';
import './picking.spec';
import './view-manager.spec';
import './widget-manager.spec';
import './world-bounds.spec';
106 changes: 106 additions & 0 deletions test/modules/core/lib/world-bounds.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import test from 'tape-catch';
import {Layer, COORDINATE_SYSTEM, transformBoundsToWorld, Viewport} from '@deck.gl/core';
import {Matrix4} from '@math.gl/core';
import LayerState from '../../../../modules/core/src/lib/layer-state';

class WorldBoundsLayer extends Layer<{customKey?: number}> {
initializeState() {}

override getBounds() {
return [
[-1, 0, 0],
[1, 0, 0]
];
}

protected override getWorldBoundsOptions() {
const options = super.getWorldBoundsOptions();
if (!options) {
return null;
}
return {...options, worldBoundsCacheKey: this.props.customKey};
}
}

WorldBoundsLayer.defaultProps = {
customKey: 0
};

const VIEWPORT = new Viewport({
id: 'identity',
width: 1,
height: 1,
position: [0, 0, 0],
viewMatrix: new Matrix4().identity(),
projectionMatrix: new Matrix4().identity()
});

test('transformBoundsToWorld#modelMatrix applied to corners', t => {
const modelMatrix = new Matrix4().rotateZ(Math.PI / 2);
const bounds = [
[-1, 0, 0],
[1, 0, 0]
] as [number[], number[]];

const obb = transformBoundsToWorld({
bounds,
coordinateOrigin: [0, 0, 0],
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
modelMatrix,
viewport: VIEWPORT
});

const halfAxes = obb.halfAxes;
t.ok(Math.abs(halfAxes[0]) < 1e-6, 'x component is rotated to zero');
t.ok(Math.abs(halfAxes[1] - 1) < 1e-6, 'y component reflects rotation');
t.deepEqual([...obb.center], [0, 0, 0], 'center is preserved');
t.end();
});

test('Layer#getWorldBounds#cache invalidation and key', t => {
const layer = new WorldBoundsLayer({
id: 'world-bounds-layer',
data: [],
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
customKey: 0
});

layer.context = {viewport: VIEWPORT} as any;
layer.internalState = new LayerState({attributeManager: null, layer});
layer.internalState.viewport = VIEWPORT;

const first = layer.getWorldBounds();
const second = layer.getWorldBounds();
t.equal(first, second, 'bounds are cached when inputs are unchanged');

layer.updateState({
props: layer.props,
oldProps: layer.props,
context: layer.context,
changeFlags: {
dataChanged: false,
propsChanged: false,
updateTriggersChanged: false,
extensionsChanged: false,
viewportChanged: true,
stateChanged: false,
propsOrDataChanged: false,
somethingChanged: true
}
});

const afterViewportChange = layer.getWorldBounds();
t.notEqual(afterViewportChange, first, 'cache invalidates when viewport changes');

const cachedAgain = layer.getWorldBounds();
t.equal(afterViewportChange, cachedAgain, 'bounds cache is reused after recompute');

layer.props = {...layer.props, customKey: 1};
const afterKeyChange = layer.getWorldBounds();
t.notEqual(afterKeyChange, cachedAgain, 'cache key forces recomputation');
t.end();
});
Loading