Add Ml3DTilesLayer alternative with support for BKG 3D tiles from basemap.de
This commit is contained in:
93
src/lib/MlTile3DLayer/Ml3DTilesLayer.tsx
Normal file
93
src/lib/MlTile3DLayer/Ml3DTilesLayer.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// This file is inspired by @mapcomponents/deck-gl's Ml3DTileLayer
|
||||||
|
// (https://github.com/mapcomponents/react-map-components-maplibre)
|
||||||
|
//
|
||||||
|
// Original work: Copyright (c) 2021 WhereGroup GmbH — MIT License
|
||||||
|
// Modified to use the custom Tile3DLayer2 class (Tiles3DLayer2.tsx) instead of
|
||||||
|
// deck.gl's built-in Tile3DLayer, enabling in-place layer replacement without
|
||||||
|
// restarting tile loading and exposing the updateTrigger prop.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import { useContext, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useMap } from '@mapcomponents/react-maplibre';
|
||||||
|
//import { Tile3DLayer, Tile3DLayerProps } from '@deck.gl/geo-layers';
|
||||||
|
import { default as Tile3DLayer, Tile3DLayer2Props } from './Tiles3DLayer2';
|
||||||
|
import { DeckGlContext } from '@mapcomponents/deck-gl';
|
||||||
|
import type { Layer } from '@deck.gl/core';
|
||||||
|
|
||||||
|
export interface Ml3DTileLayerProps extends Tile3DLayer2Props {
|
||||||
|
/**
|
||||||
|
* Id of the target MapLibre instance in mapContext
|
||||||
|
*/
|
||||||
|
mapId?: string;
|
||||||
|
/**
|
||||||
|
* Id of an existing layer in the mapLibre instance to help specify the layer order
|
||||||
|
* This layer will be visually beneath the layer with the "beforeId" id.
|
||||||
|
*/
|
||||||
|
beforeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Ml3DTileLayer = (props: Ml3DTileLayerProps) => {
|
||||||
|
const { mapId, ...Ml3DTileProps } = props;
|
||||||
|
const mapHook = useMap({ mapId: mapId });
|
||||||
|
const deckGlContext = useContext(DeckGlContext);
|
||||||
|
const layerRef = useRef<Layer | null>(null);
|
||||||
|
|
||||||
|
const tile3dLayer = useMemo(() => {
|
||||||
|
if (!Ml3DTileProps.data) return null;
|
||||||
|
else
|
||||||
|
return new Tile3DLayer({
|
||||||
|
...Ml3DTileProps,
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
Ml3DTileProps.data,
|
||||||
|
Ml3DTileProps.id,
|
||||||
|
Ml3DTileProps.pickable,
|
||||||
|
Ml3DTileProps.onTileLoad,
|
||||||
|
Ml3DTileProps.onTileUnload,
|
||||||
|
Ml3DTileProps.loadOptions,
|
||||||
|
Ml3DTileProps.loaders,
|
||||||
|
Ml3DTileProps.visible,
|
||||||
|
Ml3DTileProps.opacity,
|
||||||
|
Ml3DTileProps.pointSize,
|
||||||
|
Ml3DTileProps.beforeId,
|
||||||
|
Ml3DTileProps.getFeatureColor,
|
||||||
|
Ml3DTileProps.onClick,
|
||||||
|
Ml3DTileProps.updateTrigger,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add or replace the layer in the deck.gl layer array in-place.
|
||||||
|
// This avoids remove+add which would destroy the tileset via MapboxOverlay.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapHook.map || !tile3dLayer) return;
|
||||||
|
|
||||||
|
const prev = layerRef.current;
|
||||||
|
layerRef.current = tile3dLayer;
|
||||||
|
|
||||||
|
deckGlContext.setDeckGlLayerArray((layers) => {
|
||||||
|
if (prev) {
|
||||||
|
// Replace old layer reference in-place
|
||||||
|
return layers.map((l) => (l === prev ? tile3dLayer : l));
|
||||||
|
}
|
||||||
|
// First mount: append
|
||||||
|
return [...layers, tile3dLayer];
|
||||||
|
});
|
||||||
|
}, [mapHook.map, tile3dLayer]);
|
||||||
|
|
||||||
|
// Remove layer only on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
const layer = layerRef.current;
|
||||||
|
if (layer) {
|
||||||
|
deckGlContext.setDeckGlLayerArray((layers) =>
|
||||||
|
layers.filter((l) => l !== layer)
|
||||||
|
);
|
||||||
|
layerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Ml3DTileLayer;
|
||||||
544
src/lib/MlTile3DLayer/Tiles3DLayer2.tsx
Normal file
544
src/lib/MlTile3DLayer/Tiles3DLayer2.tsx
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
// This file is derived from deck.gl's Tile3DLayer
|
||||||
|
// (https://github.com/visgl/deck.gl/blob/master/modules/geo-layers/src/tile-3d-layer/tile-3d-layer.ts)
|
||||||
|
//
|
||||||
|
// Original work: Copyright (c) Vis.gl contributors — MIT License
|
||||||
|
// Modified work: per-feature GPU picking, per-feature color via getFeatureColor,
|
||||||
|
// glTF→MESH conversion with EXT_structural_metadata property table support,
|
||||||
|
// and enhanced PickingInfo type (Enhanced3DTilePickingInfo).
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import {Geometry} from '@luma.gl/engine';
|
||||||
|
import {
|
||||||
|
Accessor,
|
||||||
|
Color,
|
||||||
|
CompositeLayer,
|
||||||
|
CompositeLayerProps,
|
||||||
|
COORDINATE_SYSTEM,
|
||||||
|
FilterContext,
|
||||||
|
GetPickingInfoParams,
|
||||||
|
Layer,
|
||||||
|
LayersList,
|
||||||
|
log,
|
||||||
|
PickingInfo,
|
||||||
|
UpdateParameters,
|
||||||
|
Viewport,
|
||||||
|
DefaultProps,
|
||||||
|
} from '@deck.gl/core';
|
||||||
|
import {PointCloudLayer} from '@deck.gl/layers';
|
||||||
|
import {ScenegraphLayer} from '@deck.gl/mesh-layers';
|
||||||
|
// Internal MeshLayer supports featureIds + PBR; the public SimpleMeshLayer does not.
|
||||||
|
import MeshLayer from '@deck.gl/geo-layers/dist/mesh-layer/mesh-layer.js';
|
||||||
|
import {load} from '@loaders.gl/core';
|
||||||
|
import type {MeshAttributes} from '@loaders.gl/schema';
|
||||||
|
import {Tileset3D, Tile3D, TILE_TYPE} from '@loaders.gl/tiles';
|
||||||
|
import {Tiles3DLoader} from '@loaders.gl/3d-tiles';
|
||||||
|
|
||||||
|
export type Enhanced3DTilePickingInfo = PickingInfo & {
|
||||||
|
sourceTile: Tile3D | null;
|
||||||
|
featureId?: number;
|
||||||
|
featureProperties?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SINGLE_DATA = [0];
|
||||||
|
|
||||||
|
export type Tile3DLayer2Props<DataT = unknown> = _Tile3DLayer2Props<DataT> &
|
||||||
|
CompositeLayerProps;
|
||||||
|
|
||||||
|
type _Tile3DLayer2Props<DataT> = {
|
||||||
|
data: string;
|
||||||
|
getPointColor?: Accessor<DataT, Color>;
|
||||||
|
pointSize?: number;
|
||||||
|
/** @deprecated Use `loaders` instead */
|
||||||
|
loader?: typeof Tiles3DLoader;
|
||||||
|
onTilesetLoad?: (tileset: Tileset3D) => void;
|
||||||
|
onTileLoad?: (tile: Tile3D) => void;
|
||||||
|
onTileUnload?: (tile: Tile3D) => void;
|
||||||
|
onTileError?: (tile: Tile3D, url: string, message: string) => void;
|
||||||
|
/** Per-feature color function. Receives featureId and owning Tile3D. Returns [r,g,b] or [r,g,b,a] in 0–255. */
|
||||||
|
getFeatureColor?: (featureId: number, tile: Tile3D) => Color;
|
||||||
|
/** Tile-level fallback color for MESH tiles when getFeatureColor is not provided. */
|
||||||
|
_getMeshColor?: (tile: Tile3D) => Color;
|
||||||
|
/** Bump to force a per-feature color rebuild without restarting tile loading. */
|
||||||
|
updateTrigger?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps: DefaultProps<Tile3DLayer2Props> = {
|
||||||
|
getPointColor: {type: 'accessor', value: [0, 0, 0, 255]},
|
||||||
|
pointSize: 1.0,
|
||||||
|
data: '',
|
||||||
|
loader: Tiles3DLoader,
|
||||||
|
onTilesetLoad: {type: 'function', value: () => {}},
|
||||||
|
onTileLoad: {type: 'function', value: () => {}},
|
||||||
|
onTileUnload: {type: 'function', value: () => {}},
|
||||||
|
onTileError: {type: 'function', value: () => {}},
|
||||||
|
getFeatureColor: {type: 'function', value: null, optional: true} as any,
|
||||||
|
_getMeshColor: {type: 'function', value: () => [255, 255, 255]},
|
||||||
|
updateTrigger: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop-in replacement for deck.gl's Tile3DLayer with per-feature GPU picking
|
||||||
|
* and per-feature mesh coloring. onClick/onHover receive featureId and
|
||||||
|
* featureProperties in addition to the tile object.
|
||||||
|
*/
|
||||||
|
export default class Tile3DLayer2<
|
||||||
|
DataT = any,
|
||||||
|
ExtraPropsT extends {} = {},
|
||||||
|
> extends CompositeLayer<ExtraPropsT & Required<_Tile3DLayer2Props<DataT>>> {
|
||||||
|
static defaultProps = defaultProps;
|
||||||
|
static layerName = 'Tile3DLayer2';
|
||||||
|
|
||||||
|
declare state: {
|
||||||
|
activeViewports: Record<string, Viewport>;
|
||||||
|
frameNumber?: number;
|
||||||
|
lastUpdatedViewports: Record<string, Viewport> | null;
|
||||||
|
layerMap: Record<string, {tile: Tile3D; layer?: Layer | null; needsUpdate?: boolean}>;
|
||||||
|
tileset3d: Tileset3D | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeState(): void {
|
||||||
|
if ('onTileLoadFail' in this.props) {
|
||||||
|
log.removed('onTileLoadFail', 'onTileError')();
|
||||||
|
}
|
||||||
|
this.state = {
|
||||||
|
layerMap: {},
|
||||||
|
tileset3d: null,
|
||||||
|
activeViewports: {},
|
||||||
|
lastUpdatedViewports: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLoaded(): boolean {
|
||||||
|
return Boolean(this.state?.tileset3d?.isLoaded() && super.isLoaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldUpdateState({changeFlags}: UpdateParameters<this>): boolean {
|
||||||
|
return changeFlags.somethingChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState({props, oldProps, changeFlags}: UpdateParameters<this>): void {
|
||||||
|
if (props.data && props.data !== oldProps.data) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this._loadTileset(props.data);
|
||||||
|
}
|
||||||
|
if (changeFlags.viewportChanged) {
|
||||||
|
const {activeViewports} = this.state;
|
||||||
|
if (Object.keys(activeViewports).length) {
|
||||||
|
this._updateTileset(activeViewports);
|
||||||
|
this.state.lastUpdatedViewports = activeViewports;
|
||||||
|
this.state.activeViewports = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changeFlags.propsChanged) {
|
||||||
|
for (const key in this.state.layerMap) {
|
||||||
|
this.state.layerMap[key].needsUpdate = true;
|
||||||
|
}
|
||||||
|
this.setNeedsUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activateViewport(viewport: Viewport): void {
|
||||||
|
const {activeViewports, lastUpdatedViewports} = this.state;
|
||||||
|
this.internalState!.viewport = viewport;
|
||||||
|
activeViewports[viewport.id] = viewport;
|
||||||
|
const lastViewport = lastUpdatedViewports?.[viewport.id];
|
||||||
|
if (!lastViewport || !viewport.equals(lastViewport)) {
|
||||||
|
this.setChangeFlags({viewportChanged: true});
|
||||||
|
this.setNeedsUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPickingInfo({info, sourceLayer}: GetPickingInfoParams): Enhanced3DTilePickingInfo {
|
||||||
|
const sourceTile: Tile3D | null = sourceLayer
|
||||||
|
? (sourceLayer.props as any).tile ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const result = info as Enhanced3DTilePickingInfo;
|
||||||
|
result.sourceTile = sourceTile;
|
||||||
|
|
||||||
|
if (info.picked && sourceTile) {
|
||||||
|
result.object = sourceTile;
|
||||||
|
const featureIds: ArrayLike<number> | undefined = sourceTile.content?.featureIds;
|
||||||
|
if (featureIds && featureIds.length > 0) {
|
||||||
|
const featureId = info.index;
|
||||||
|
if (featureId != null && featureId >= 0) {
|
||||||
|
result.featureId = featureId;
|
||||||
|
result.featureProperties = resolveFeatureProperties(sourceTile, featureId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterSubLayer({layer, viewport}: FilterContext): boolean {
|
||||||
|
const {tile} = layer.props as unknown as {tile: Tile3D};
|
||||||
|
return tile.selected && tile.viewportIds.includes(viewport.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _updateAutoHighlight(info: PickingInfo): void {
|
||||||
|
const sourceTile = (info as Enhanced3DTilePickingInfo).sourceTile;
|
||||||
|
const layerCache = sourceTile ? this.state.layerMap[sourceTile.id] : null;
|
||||||
|
if (layerCache?.layer) {
|
||||||
|
(layerCache.layer as any).updateAutoHighlight(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadTileset(tilesetUrl: string): Promise<void> {
|
||||||
|
const {loadOptions = {}} = this.props;
|
||||||
|
// @ts-ignore — support both deprecated `loader` and `loaders`
|
||||||
|
const loaders = this.props.loader || this.props.loaders;
|
||||||
|
const loader = Array.isArray(loaders) ? loaders[0] : loaders;
|
||||||
|
|
||||||
|
const options: any = {loadOptions: {...loadOptions}};
|
||||||
|
let actualTilesetUrl = tilesetUrl;
|
||||||
|
|
||||||
|
if (loader?.preload) {
|
||||||
|
const preloadOptions = await loader.preload(tilesetUrl, loadOptions);
|
||||||
|
if (preloadOptions.url) actualTilesetUrl = preloadOptions.url;
|
||||||
|
if (preloadOptions.headers) {
|
||||||
|
options.loadOptions.fetch = {
|
||||||
|
...options.loadOptions.fetch,
|
||||||
|
headers: preloadOptions.headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Object.assign(options, preloadOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tilesetJson = await load(actualTilesetUrl, loader, options.loadOptions);
|
||||||
|
const tileset3d = new Tileset3D(tilesetJson, {
|
||||||
|
onTileLoad: this._onTileLoad.bind(this),
|
||||||
|
onTileUnload: this._onTileUnload.bind(this),
|
||||||
|
onTileError: this.props.onTileError,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({tileset3d, layerMap: {}});
|
||||||
|
this._updateTileset(this.state.activeViewports);
|
||||||
|
this.props.onTilesetLoad(tileset3d);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onTileLoad(tileHeader: Tile3D): void {
|
||||||
|
this._convertScenegraphToMesh(tileHeader);
|
||||||
|
this.props.onTileLoad(tileHeader);
|
||||||
|
this._updateTileset(this.state.lastUpdatedViewports);
|
||||||
|
this.setNeedsUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onTileUnload(tileHeader: Tile3D): void {
|
||||||
|
delete this.state.layerMap[tileHeader.id];
|
||||||
|
this.props.onTileUnload(tileHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateTileset(viewports: Record<string, Viewport> | null): void {
|
||||||
|
if (!viewports) return;
|
||||||
|
const {tileset3d} = this.state;
|
||||||
|
const {timeline} = this.context;
|
||||||
|
if (!timeline || !Object.keys(viewports).length || !tileset3d) return;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
tileset3d.selectTiles(Object.values(viewports)).then((frameNumber) => {
|
||||||
|
if (this.state.frameNumber !== frameNumber) {
|
||||||
|
this.setState({frameNumber});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts glTF SCENEGRAPH tiles to MESH in-place to enable per-feature picking and coloring. */
|
||||||
|
private _convertScenegraphToMesh(tileHeader: Tile3D): void {
|
||||||
|
const content = tileHeader.content;
|
||||||
|
if (!content?.gltf || content.attributes) return;
|
||||||
|
|
||||||
|
const gltf = content.gltf;
|
||||||
|
const prim = gltf.meshes?.[0]?.primitives?.[0];
|
||||||
|
if (!prim?.attributes?.POSITION) return;
|
||||||
|
|
||||||
|
const posAttr = prim.attributes.POSITION;
|
||||||
|
const normAttr = prim.attributes.NORMAL;
|
||||||
|
const featureIdRaw =
|
||||||
|
prim.attributes._FEATURE_ID_0 ||
|
||||||
|
prim.attributes._BATCHID ||
|
||||||
|
prim.attributes.BATCHID;
|
||||||
|
|
||||||
|
const rawPositions: Float32Array = posAttr.value || posAttr;
|
||||||
|
const rawNormals: Float32Array | undefined = normAttr?.value || normAttr;
|
||||||
|
const indices = prim.indices?.value || prim.indices;
|
||||||
|
|
||||||
|
const node = gltf.nodes?.find((n: any) => n.mesh != null);
|
||||||
|
const nodeMatrix: number[] | undefined = node?.matrix;
|
||||||
|
const positions = new Float32Array(rawPositions.length);
|
||||||
|
const normals = rawNormals ? new Float32Array(rawNormals.length) : undefined;
|
||||||
|
|
||||||
|
if (nodeMatrix && !isIdentityMatrix(nodeMatrix)) {
|
||||||
|
// Apply glTF node transform (affine: M * [x, y, z, 1])
|
||||||
|
for (let i = 0; i < rawPositions.length; i += 3) {
|
||||||
|
const x = rawPositions[i], y = rawPositions[i + 1], z = rawPositions[i + 2];
|
||||||
|
positions[i] = nodeMatrix[0] * x + nodeMatrix[4] * y + nodeMatrix[8] * z + nodeMatrix[12];
|
||||||
|
positions[i + 1] = nodeMatrix[1] * x + nodeMatrix[5] * y + nodeMatrix[9] * z + nodeMatrix[13];
|
||||||
|
positions[i + 2] = nodeMatrix[2] * x + nodeMatrix[6] * y + nodeMatrix[10] * z + nodeMatrix[14];
|
||||||
|
}
|
||||||
|
// Apply upper-3x3 to normals (no translation)
|
||||||
|
if (rawNormals && normals) {
|
||||||
|
for (let i = 0; i < rawNormals.length; i += 3) {
|
||||||
|
const x = rawNormals[i], y = rawNormals[i + 1], z = rawNormals[i + 2];
|
||||||
|
normals[i] = nodeMatrix[0] * x + nodeMatrix[4] * y + nodeMatrix[8] * z;
|
||||||
|
normals[i + 1] = nodeMatrix[1] * x + nodeMatrix[5] * y + nodeMatrix[9] * z;
|
||||||
|
normals[i + 2] = nodeMatrix[2] * x + nodeMatrix[6] * y + nodeMatrix[10] * z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
positions.set(rawPositions);
|
||||||
|
if (rawNormals && normals) normals.set(rawNormals);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.attributes = {
|
||||||
|
positions: {size: 3, value: positions},
|
||||||
|
...(normals ? {normals: {size: 3, value: normals}} : {}),
|
||||||
|
};
|
||||||
|
content.indices = indices || undefined;
|
||||||
|
if (prim.material) content.material = prim.material;
|
||||||
|
|
||||||
|
if (featureIdRaw) {
|
||||||
|
const rawValues = featureIdRaw.value || featureIdRaw;
|
||||||
|
const featureIds = new Uint32Array(rawValues.length);
|
||||||
|
for (let i = 0; i < rawValues.length; i++) featureIds[i] = rawValues[i];
|
||||||
|
content.featureIds = featureIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build per-feature property table from EXT_structural_metadata.
|
||||||
|
// loaders.gl resolves the binary arrays into typed JS arrays at load time.
|
||||||
|
const pt = gltf.extensions?.EXT_structural_metadata?.propertyTables?.[0];
|
||||||
|
if (pt) {
|
||||||
|
content.propertyTable = Array.from({length: pt.count || 0}, (_, i) => {
|
||||||
|
const row: Record<string, unknown> = {};
|
||||||
|
for (const [key, prop] of Object.entries(pt.properties || {})) {
|
||||||
|
const data = (prop as any).data;
|
||||||
|
if (Array.isArray(data)) row[key] = data[i];
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(tileHeader as any).type = TILE_TYPE.MESH;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getSubLayer(
|
||||||
|
tileHeader: Tile3D,
|
||||||
|
oldLayer?: Layer,
|
||||||
|
): MeshLayer<DataT> | PointCloudLayer<DataT> | ScenegraphLayer<DataT> | null {
|
||||||
|
if (!tileHeader.content) return null;
|
||||||
|
switch (tileHeader.type as TILE_TYPE) {
|
||||||
|
case TILE_TYPE.POINTCLOUD:
|
||||||
|
return this._makePointCloudLayer(tileHeader, oldLayer as PointCloudLayer<DataT>);
|
||||||
|
case TILE_TYPE.SCENEGRAPH:
|
||||||
|
return this._make3DModelLayer(tileHeader);
|
||||||
|
case TILE_TYPE.MESH:
|
||||||
|
return this._makeSimpleMeshLayer(tileHeader, oldLayer as MeshLayer<DataT>);
|
||||||
|
default:
|
||||||
|
throw new Error(`Tile3DLayer2: unsupported tile type ${tileHeader.content.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _makePointCloudLayer(
|
||||||
|
tileHeader: Tile3D,
|
||||||
|
oldLayer?: PointCloudLayer<DataT>,
|
||||||
|
): PointCloudLayer<DataT> | null {
|
||||||
|
const {attributes, pointCount, constantRGBA, cartographicOrigin, modelMatrix} =
|
||||||
|
tileHeader.content;
|
||||||
|
const {positions, normals, colors} = attributes;
|
||||||
|
if (!positions) return null;
|
||||||
|
|
||||||
|
const data = (oldLayer && oldLayer.props.data) || {
|
||||||
|
header: {vertexCount: pointCount},
|
||||||
|
attributes: {POSITION: positions, NORMAL: normals, COLOR_0: colors},
|
||||||
|
};
|
||||||
|
|
||||||
|
const {pointSize, getPointColor} = this.props;
|
||||||
|
const SubLayerClass = this.getSubLayerClass('pointcloud', PointCloudLayer);
|
||||||
|
return new SubLayerClass(
|
||||||
|
{pointSize},
|
||||||
|
this.getSubLayerProps({id: 'pointcloud'}),
|
||||||
|
{
|
||||||
|
id: `${this.id}-pointcloud-${tileHeader.id}`,
|
||||||
|
tile: tileHeader,
|
||||||
|
data,
|
||||||
|
coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS,
|
||||||
|
coordinateOrigin: cartographicOrigin,
|
||||||
|
modelMatrix,
|
||||||
|
getColor: constantRGBA || getPointColor,
|
||||||
|
_offset: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _make3DModelLayer(tileHeader: Tile3D): ScenegraphLayer<DataT> {
|
||||||
|
const {gltf, instances, cartographicOrigin, modelMatrix} = tileHeader.content;
|
||||||
|
const SubLayerClass = this.getSubLayerClass('scenegraph', ScenegraphLayer);
|
||||||
|
return new SubLayerClass(
|
||||||
|
{_lighting: 'pbr'},
|
||||||
|
this.getSubLayerProps({id: 'scenegraph'}),
|
||||||
|
{
|
||||||
|
id: `${this.id}-scenegraph-${tileHeader.id}`,
|
||||||
|
tile: tileHeader,
|
||||||
|
data: instances || SINGLE_DATA,
|
||||||
|
scenegraph: gltf,
|
||||||
|
coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS,
|
||||||
|
coordinateOrigin: cartographicOrigin,
|
||||||
|
modelMatrix,
|
||||||
|
getTransformMatrix: (instance: any) => instance.modelMatrix,
|
||||||
|
getPosition: [0, 0, 0],
|
||||||
|
_offset: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _makeSimpleMeshLayer(
|
||||||
|
tileHeader: Tile3D,
|
||||||
|
oldLayer?: MeshLayer<DataT>,
|
||||||
|
): MeshLayer<DataT> {
|
||||||
|
const content = tileHeader.content;
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
indices,
|
||||||
|
modelMatrix,
|
||||||
|
cartographicOrigin,
|
||||||
|
coordinateSystem = COORDINATE_SYSTEM.METER_OFFSETS,
|
||||||
|
material,
|
||||||
|
featureIds,
|
||||||
|
} = content;
|
||||||
|
|
||||||
|
const {getFeatureColor, _getMeshColor} = this.props;
|
||||||
|
const mergedAttributes = buildMeshGeometry(attributes, featureIds, getFeatureColor, tileHeader);
|
||||||
|
const geometry =
|
||||||
|
(!getFeatureColor && oldLayer && oldLayer.props.mesh) ||
|
||||||
|
new Geometry({topology: 'triangle-list', attributes: mergedAttributes, indices});
|
||||||
|
|
||||||
|
const SubLayerClass = this.getSubLayerClass('mesh', MeshLayer);
|
||||||
|
return new SubLayerClass(
|
||||||
|
this.getSubLayerProps({id: 'mesh'}),
|
||||||
|
{
|
||||||
|
id: `${this.id}-mesh-${tileHeader.id}`,
|
||||||
|
tile: tileHeader,
|
||||||
|
mesh: geometry,
|
||||||
|
data: SINGLE_DATA,
|
||||||
|
getColor: getFeatureColor != null ? [255, 255, 255, 255] : _getMeshColor(tileHeader),
|
||||||
|
// Spread material into a new object so MeshLayer.updateState always calls
|
||||||
|
// updatePbrMaterialUniforms() after a geometry rebuild (binds GPU sampler to new model).
|
||||||
|
pbrMaterial: {...(material || {})},
|
||||||
|
modelMatrix,
|
||||||
|
coordinateOrigin: cartographicOrigin,
|
||||||
|
coordinateSystem,
|
||||||
|
featureIds,
|
||||||
|
_offset: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLayers(): Layer | null | LayersList {
|
||||||
|
const {tileset3d, layerMap} = this.state;
|
||||||
|
if (!tileset3d) return null;
|
||||||
|
|
||||||
|
return (tileset3d.tiles as Tile3D[])
|
||||||
|
.map((tile) => {
|
||||||
|
const layerCache = (layerMap[tile.id] = layerMap[tile.id] || {tile});
|
||||||
|
let {layer} = layerCache;
|
||||||
|
if (tile.selected) {
|
||||||
|
if (!layer) {
|
||||||
|
layer = this._getSubLayer(tile);
|
||||||
|
} else if (layerCache.needsUpdate) {
|
||||||
|
layer = this._getSubLayer(tile, layer as Layer);
|
||||||
|
layerCache.needsUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layerCache.layer = layer;
|
||||||
|
return layer;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as LayersList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFeatureProperties(
|
||||||
|
tile: Tile3D,
|
||||||
|
featureId: number,
|
||||||
|
): Record<string, unknown> | undefined {
|
||||||
|
const content = tile.content;
|
||||||
|
if (!content) return undefined;
|
||||||
|
|
||||||
|
// 3D Tiles 1.1 — propertyTable built from EXT_structural_metadata
|
||||||
|
const propertyTable: Record<string, unknown>[] | undefined = content.propertyTable;
|
||||||
|
if (propertyTable) return propertyTable[featureId] as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
// 3D Tiles 1.0 — batchTableJson (column-oriented arrays indexed by featureId)
|
||||||
|
const batchTableJson: Record<string, ArrayLike<unknown>> | undefined = content.batchTableJson;
|
||||||
|
if (batchTableJson) {
|
||||||
|
const properties: Record<string, unknown> = {};
|
||||||
|
for (const key of Object.keys(batchTableJson)) {
|
||||||
|
const col = batchTableJson[key];
|
||||||
|
if (Array.isArray(col) || ArrayBuffer.isView(col)) {
|
||||||
|
properties[key] = (col as any)[featureId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMeshGeometry(
|
||||||
|
contentAttributes: MeshAttributes,
|
||||||
|
featureIds: ArrayLike<number> | undefined,
|
||||||
|
getFeatureColor: ((featureId: number, tile: Tile3D) => Color) | null | undefined,
|
||||||
|
tileHeader: Tile3D,
|
||||||
|
): MeshAttributes {
|
||||||
|
const attributes: MeshAttributes = {
|
||||||
|
positions: {
|
||||||
|
...contentAttributes.positions,
|
||||||
|
// Always copy into a fresh array — geometry creation may mutate the buffer
|
||||||
|
value: new Float32Array(contentAttributes.positions.value as ArrayLike<number>),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (contentAttributes.normals) attributes.normals = contentAttributes.normals;
|
||||||
|
if (contentAttributes.texCoords) attributes.texCoords = contentAttributes.texCoords;
|
||||||
|
if (contentAttributes.uvRegions) attributes.uvRegions = contentAttributes.uvRegions;
|
||||||
|
|
||||||
|
if (getFeatureColor && featureIds && featureIds.length > 0) {
|
||||||
|
const colorBuffer = new Uint8Array(featureIds.length * 4);
|
||||||
|
for (let i = 0; i < featureIds.length; i++) {
|
||||||
|
const color = getFeatureColor(featureIds[i], tileHeader);
|
||||||
|
colorBuffer[i * 4] = color[0];
|
||||||
|
colorBuffer[i * 4 + 1] = color[1];
|
||||||
|
colorBuffer[i * 4 + 2] = color[2];
|
||||||
|
colorBuffer[i * 4 + 3] = color[3] ?? 255;
|
||||||
|
}
|
||||||
|
attributes.colors = {size: 4, value: colorBuffer, normalized: true};
|
||||||
|
} else if (getFeatureColor) {
|
||||||
|
// No featureIds — color all vertices uniformly via featureId 0
|
||||||
|
const vertexCount =
|
||||||
|
(contentAttributes.positions.value as ArrayLike<number>).length /
|
||||||
|
(contentAttributes.positions.size || 3);
|
||||||
|
const color = getFeatureColor(0, tileHeader);
|
||||||
|
const colorBuffer = new Uint8Array(vertexCount * 4);
|
||||||
|
for (let i = 0; i < vertexCount; i++) {
|
||||||
|
colorBuffer[i * 4] = color[0];
|
||||||
|
colorBuffer[i * 4 + 1] = color[1];
|
||||||
|
colorBuffer[i * 4 + 2] = color[2];
|
||||||
|
colorBuffer[i * 4 + 3] = color[3] ?? 255;
|
||||||
|
}
|
||||||
|
attributes.colors = {size: 4, value: colorBuffer, normalized: true};
|
||||||
|
} else if (contentAttributes.colors) {
|
||||||
|
attributes.colors = contentAttributes.colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IDENTITY_4X4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
||||||
|
|
||||||
|
function isIdentityMatrix(m: ArrayLike<number>): boolean {
|
||||||
|
if (m.length !== 16) return false;
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
if (Math.abs(m[i] - IDENTITY_4X4[i]) > 1e-6) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user