2026-02-15 12:11:39 +09:00
|
|
|
import { Deck, MapController, View, Viewport, type DeckProps } from "@deck.gl/core";
|
|
|
|
|
import type maplibregl from "maplibre-gl";
|
|
|
|
|
|
|
|
|
|
type MatrixViewState = {
|
|
|
|
|
// MapLibre provides a full world->clip matrix as `modelViewProjectionMatrix`.
|
|
|
|
|
// We treat it as the viewport's projection matrix and keep viewMatrix identity.
|
|
|
|
|
projectionMatrix: number[];
|
|
|
|
|
viewMatrix?: number[];
|
|
|
|
|
// Deck's View state is constrained to include transition props. We only need one overlapping key
|
|
|
|
|
// to satisfy TS structural checks without pulling in internal deck.gl types.
|
|
|
|
|
transitionDuration?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class MatrixView extends View<MatrixViewState> {
|
|
|
|
|
getViewportType(viewState: MatrixViewState): typeof Viewport {
|
|
|
|
|
void viewState;
|
|
|
|
|
return Viewport;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Controller isn't used (Deck is created with `controller: false`) but View requires one.
|
|
|
|
|
protected get ControllerType(): typeof MapController {
|
|
|
|
|
return MapController;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const IDENTITY_4x4: number[] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
|
|
|
|
|
|
|
|
function readMat4(m: ArrayLike<number>): number[] {
|
|
|
|
|
const out = new Array<number>(16);
|
|
|
|
|
for (let i = 0; i < 16; i++) out[i] = m[i] as number;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mat4Changed(a: number[] | undefined, b: ArrayLike<number>): boolean {
|
|
|
|
|
if (!a || a.length !== 16) return true;
|
|
|
|
|
// The matrix values change on map move/rotate. A strict compare is fine.
|
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
|
|
|
if (a[i] !== (b[i] as number)) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface {
|
|
|
|
|
id: string;
|
|
|
|
|
type = "custom" as const;
|
|
|
|
|
renderingMode = "3d" as const;
|
|
|
|
|
|
|
|
|
|
private _map: maplibregl.Map | null = null;
|
|
|
|
|
private _deck: Deck<MatrixView[]> | null = null;
|
|
|
|
|
private _deckProps: Partial<DeckProps<MatrixView[]>> = {};
|
|
|
|
|
private _viewId: string;
|
|
|
|
|
private _lastMvp: number[] | undefined;
|
2026-02-15 12:36:25 +09:00
|
|
|
private _finalizeOnRemove: boolean = false;
|
2026-02-15 12:11:39 +09:00
|
|
|
|
|
|
|
|
constructor(opts: { id: string; viewId: string; deckProps?: Partial<DeckProps<MatrixView[]>> }) {
|
|
|
|
|
this.id = opts.id;
|
|
|
|
|
this._viewId = opts.viewId;
|
|
|
|
|
this._deckProps = opts.deckProps ?? {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get deck(): Deck<MatrixView[]> | null {
|
|
|
|
|
return this._deck;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 12:36:25 +09:00
|
|
|
requestFinalize() {
|
|
|
|
|
this._finalizeOnRemove = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 12:11:39 +09:00
|
|
|
setProps(next: Partial<DeckProps<MatrixView[]>>) {
|
|
|
|
|
this._deckProps = { ...this._deckProps, ...next };
|
|
|
|
|
if (this._deck) this._deck.setProps(this._deckProps as DeckProps<MatrixView[]>);
|
|
|
|
|
this._map?.triggerRepaint();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onAdd(map: maplibregl.Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void {
|
2026-02-15 12:36:25 +09:00
|
|
|
this._finalizeOnRemove = false;
|
2026-02-15 12:11:39 +09:00
|
|
|
this._map = map;
|
|
|
|
|
|
2026-02-15 12:36:25 +09:00
|
|
|
if (this._deck) {
|
|
|
|
|
// Re-attached after a style change; keep the existing Deck instance so we don't reuse
|
|
|
|
|
// finalized Layer objects (Deck does not allow that).
|
|
|
|
|
this._lastMvp = undefined;
|
|
|
|
|
this._deck.setProps({
|
|
|
|
|
...this._deckProps,
|
|
|
|
|
canvas: map.getCanvas(),
|
|
|
|
|
// Ensure any pending redraw requests trigger a map repaint again.
|
|
|
|
|
_customRender: () => map.triggerRepaint(),
|
|
|
|
|
} as DeckProps<MatrixView[]>);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 12:11:39 +09:00
|
|
|
const deck = new Deck<MatrixView[]>({
|
|
|
|
|
...this._deckProps,
|
|
|
|
|
// Share MapLibre's WebGL context + canvas (single context).
|
|
|
|
|
gl: gl as WebGL2RenderingContext,
|
|
|
|
|
canvas: map.getCanvas(),
|
|
|
|
|
width: null,
|
|
|
|
|
height: null,
|
|
|
|
|
// Let MapLibre own pointer/touch behaviors on the shared canvas.
|
|
|
|
|
touchAction: "none",
|
|
|
|
|
controller: false,
|
|
|
|
|
views: this._deckProps.views ?? [new MatrixView({ id: this._viewId })],
|
|
|
|
|
viewState: this._deckProps.viewState ?? { [this._viewId]: { projectionMatrix: IDENTITY_4x4 } },
|
|
|
|
|
// Only request a repaint when Deck thinks it needs one. Drawing happens in `render()`.
|
|
|
|
|
_customRender: () => map.triggerRepaint(),
|
|
|
|
|
parameters: {
|
|
|
|
|
// Match @deck.gl/mapbox interleaved defaults (premultiplied blending).
|
|
|
|
|
depthWriteEnabled: true,
|
|
|
|
|
depthCompare: "less-equal",
|
|
|
|
|
depthBias: 0,
|
|
|
|
|
blend: true,
|
|
|
|
|
blendColorSrcFactor: "src-alpha",
|
|
|
|
|
blendColorDstFactor: "one-minus-src-alpha",
|
|
|
|
|
blendAlphaSrcFactor: "one",
|
|
|
|
|
blendAlphaDstFactor: "one-minus-src-alpha",
|
|
|
|
|
blendColorOperation: "add",
|
|
|
|
|
blendAlphaOperation: "add",
|
|
|
|
|
...this._deckProps.parameters,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this._deck = deck;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onRemove(): void {
|
2026-02-15 12:36:25 +09:00
|
|
|
const deck = this._deck;
|
|
|
|
|
const map = this._map;
|
2026-02-15 12:11:39 +09:00
|
|
|
this._map = null;
|
|
|
|
|
this._lastMvp = undefined;
|
2026-02-15 12:36:25 +09:00
|
|
|
|
|
|
|
|
if (!deck) return;
|
|
|
|
|
|
|
|
|
|
if (this._finalizeOnRemove) {
|
|
|
|
|
deck.finalize();
|
|
|
|
|
this._deck = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Likely a base style swap; keep Deck instance alive and re-attach in onAdd().
|
|
|
|
|
// Disable repaint requests until we get re-attached.
|
|
|
|
|
try {
|
|
|
|
|
deck.setProps({ _customRender: () => void 0 } as Partial<DeckProps<MatrixView[]>>);
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
map?.triggerRepaint();
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
2026-02-15 12:11:39 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render(_gl: WebGLRenderingContext | WebGL2RenderingContext, options: maplibregl.CustomRenderMethodInput): void {
|
|
|
|
|
const deck = this._deck;
|
2026-02-15 13:03:05 +09:00
|
|
|
if (!this._map) return;
|
2026-02-15 12:36:25 +09:00
|
|
|
if (!deck || !deck.isInitialized) return;
|
2026-02-15 13:03:05 +09:00
|
|
|
// Deck reports `isInitialized` once `viewManager` exists, but we still see rare cases during
|
|
|
|
|
// style/projection transitions where internal managers are temporarily null (or tearing down).
|
|
|
|
|
// Guard before calling the internal `_drawLayers` to avoid crashing the whole map render.
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
const internal = deck as any;
|
|
|
|
|
if (!internal.layerManager || !internal.viewManager) return;
|
2026-02-15 12:11:39 +09:00
|
|
|
|
|
|
|
|
// MapLibre gives us a world->clip matrix for the current projection (mercator/globe).
|
|
|
|
|
// For globe, this matrix expects unit-sphere world coordinates (see MapLibre's globe transform).
|
|
|
|
|
if (mat4Changed(this._lastMvp, options.modelViewProjectionMatrix)) {
|
|
|
|
|
const projectionMatrix = readMat4(options.modelViewProjectionMatrix);
|
|
|
|
|
this._lastMvp = projectionMatrix;
|
|
|
|
|
deck.setProps({ viewState: { [this._viewId]: { projectionMatrix, viewMatrix: IDENTITY_4x4 } } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deck._drawLayers("maplibre-custom", {
|
|
|
|
|
clearCanvas: false,
|
|
|
|
|
clearStack: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|