@itwin/core-frontend
Version:
iTwin.js frontend components
235 lines • 12.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Tiles
*/
import { assert, ByteStream } from "@itwin/core-bentley";
import { Point2d, Point3d, Transform } from "@itwin/core-geometry";
import { BatchType, CompositeTileHeader, TileFormat } from "@itwin/core-common";
import { IModelApp } from "../../IModelApp";
import { GraphicBranch } from "../../render/GraphicBranch";
import { ScreenViewport } from "../../Viewport";
import { GltfWrapMode } from "../../common/gltf/GltfSchema";
import { B3dmReader, createDefaultViewFlagOverrides, GltfGraphicsReader, GltfReader, GltfReaderProps, I3dmReader, ImdlReader, readPointCloudTileContent, } from "../../tile/internal";
const defaultViewFlagOverrides = createDefaultViewFlagOverrides({});
const scratchTileCenterWorld = new Point3d();
const scratchTileCenterView = new Point3d();
/** Serves as a "handler" for a specific type of [[TileTree]]. Its primary responsibilities involve loading tile content.
* @internal
*/
export class RealityTileLoader {
_produceGeometry;
_containsPointClouds = false;
preloadRealityParentDepth;
preloadRealityParentSkip;
constructor(_produceGeometry) {
this._produceGeometry = _produceGeometry;
this.preloadRealityParentDepth = IModelApp.tileAdmin.contextPreloadParentDepth;
this.preloadRealityParentSkip = IModelApp.tileAdmin.contextPreloadParentSkip;
}
computeTilePriority(tile, viewports, _users) {
// ###TODO: Handle case where tile tree reference(s) have a transform different from tree's (background map with ground bias).
return RealityTileLoader.computeTileLocationPriority(tile, viewports, tile.tree.iModelTransform);
}
get wantDeduplicatedVertices() { return false; }
get _batchType() { return BatchType.Primary; }
get _loadEdges() { return true; }
getBatchIdMap() { return undefined; }
get isContentUnbounded() { return false; }
get containsPointClouds() { return this._containsPointClouds; }
get parentsAndChildrenExclusive() { return true; }
forceTileLoad(_tile) { return false; }
get maximumScreenSpaceError() { return undefined; }
processSelectedTiles(selected, _args) { return selected; }
// NB: The isCanceled arg is chiefly for tests...in usual case it just returns false if the tile is no longer in 'loading' state.
async loadTileContent(tile, data, system, isCanceled) {
assert(data instanceof Uint8Array);
const blob = data;
const streamBuffer = ByteStream.fromUint8Array(blob);
const realityTile = tile;
return (this._produceGeometry && this._produceGeometry !== "no") ? this.loadGeometryFromStream(realityTile, streamBuffer, system) : this.loadGraphicsFromStream(realityTile, streamBuffer, system, isCanceled);
}
_getFormat(streamBuffer) {
const position = streamBuffer.curPos;
const format = streamBuffer.readUint32();
streamBuffer.curPos = position;
return format;
}
async loadGeometryFromStream(tile, streamBuffer, system) {
const format = this._getFormat(streamBuffer);
if (format !== TileFormat.B3dm)
return {};
const { is3d, yAxisUp, iModel, modelId } = tile.realityRoot;
const reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, undefined, this.getBatchIdMap());
if (reader)
reader.defaultWrapMode = GltfWrapMode.ClampToEdge;
const geom = reader?.readGltfAndCreateGeometry(tile.tree.iModelTransform);
const xForm = tile.reprojectionTransform;
if (tile.tree.reprojectGeometry && geom?.polyfaces && xForm) {
const polyfaces = geom.polyfaces.map((pf) => pf.cloneTransformed(xForm));
return { geometry: { polyfaces } };
}
else {
return { geometry: geom };
}
}
async loadGraphicsFromStream(tile, streamBuffer, system, isCanceled) {
const format = this._getFormat(streamBuffer);
if (undefined === isCanceled)
isCanceled = () => !tile.isLoading;
const { is3d, yAxisUp, iModel, modelId } = tile.realityRoot;
let reader;
const ecefTransform = tile.tree.iModel.isGeoLocated ? tile.tree.iModel.getEcefTransform() : Transform.createIdentity();
const tileData = {
ecefTransform,
range: tile.range,
layerClassifiers: tile.tree.layerHandler?.layerClassifiers,
};
switch (format) {
case TileFormat.IModel:
reader = ImdlReader.create({
stream: streamBuffer,
iModel,
modelId,
is3d,
system,
isCanceled,
});
break;
case TileFormat.Pnts:
this._containsPointClouds = true;
const res = await readPointCloudTileContent(streamBuffer, iModel, modelId, is3d, tile, system);
let graphic = res.graphic;
const rtcCenter = res.rtcCenter;
if (graphic && (rtcCenter || tile.transformToRoot && !tile.transformToRoot.isIdentity)) {
const transformBranch = new GraphicBranch(true);
transformBranch.add(graphic);
let xform;
if (!tile.transformToRoot && rtcCenter)
xform = Transform.createTranslation(rtcCenter);
else {
if (rtcCenter)
xform = Transform.createOriginAndMatrix(rtcCenter.plus(tile.transformToRoot.origin), tile.transformToRoot.matrix);
else
xform = tile.transformToRoot;
}
graphic = system.createBranch(transformBranch, xform);
}
return { graphic };
case TileFormat.B3dm:
reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, isCanceled, this.getBatchIdMap(), this.wantDeduplicatedVertices, tileData);
if (reader) {
// glTF spec defaults wrap mode to "repeat" but many reality tiles omit the wrap mode and should not repeat.
// The render system also currently only produces mip-maps for repeating textures, and we don't want mip-maps for reality tile textures.
assert(reader instanceof GltfReader);
reader.defaultWrapMode = GltfWrapMode.ClampToEdge;
}
break;
case TileFormat.I3dm:
reader = I3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, isCanceled, undefined, this.wantDeduplicatedVertices, tileData);
break;
case TileFormat.Gltf:
const tree = tile.tree;
const baseUrl = tree.baseUrl;
const props = createReaderPropsWithBaseUrl(streamBuffer, yAxisUp, baseUrl);
if (props) {
reader = new GltfGraphicsReader(props, {
iModel,
gltf: props.glTF,
contentRange: tile.contentRange,
transform: tile.transformToRoot,
hasChildren: !tile.isLeaf,
pickableOptions: { id: modelId },
idMap: this.getBatchIdMap(),
tileData
});
}
break;
case TileFormat.Cmpt:
const header = new CompositeTileHeader(streamBuffer);
if (!header.isValid)
return {};
const branch = new GraphicBranch(true);
for (let i = 0; i < header.tileCount; i++) {
const tilePosition = streamBuffer.curPos;
streamBuffer.advance(8); // Skip magic and version.
const tileBytes = streamBuffer.readUint32();
streamBuffer.curPos = tilePosition;
const result = await this.loadGraphicsFromStream(tile, streamBuffer, system, isCanceled);
if (result.graphic)
branch.add(result.graphic);
streamBuffer.curPos = tilePosition + tileBytes;
}
return { graphic: branch.isEmpty ? undefined : system.createBranch(branch, Transform.createIdentity()), isLeaf: tile.isLeaf };
default:
assert(false, `unknown tile format ${format}`);
break;
}
let content = {};
if (undefined !== reader) {
try {
content = await reader.read();
if (content.containsPointCloud)
this._containsPointClouds = true;
}
catch {
// Failure to load should prevent us from trying to load children
content.isLeaf = true;
}
}
return content;
}
get viewFlagOverrides() { return defaultViewFlagOverrides; }
static computeTileLocationPriority(tile, viewports, location) {
// Compute a priority value for tiles that are:
// * Closer to the eye;
// * Closer to the center of attention (center of the screen or zoom target).
// This way, we can load in priority tiles that are more likely to be important.
let center;
let minDistance = 1.0;
const currentInputState = IModelApp.toolAdmin.currentInputState;
const now = Date.now();
const wheelEventRelevanceTimeout = 1000; // Wheel events older than this value will not be considered
for (const viewport of viewports) {
center = center ?? location.multiplyPoint3d(tile.center, scratchTileCenterWorld);
const npc = viewport.worldToNpc(center, scratchTileCenterView);
let focusPoint = new Point2d(0.5, 0.5);
if (currentInputState.viewport === viewport && viewport instanceof ScreenViewport) {
// Try to get a better target point from the last zoom target
const { lastWheelEvent } = currentInputState;
if (lastWheelEvent !== undefined && now - lastWheelEvent.time < wheelEventRelevanceTimeout) {
const focusPointCandidate = Point2d.fromJSON(viewport.worldToNpc(lastWheelEvent.point));
if (focusPointCandidate.x > 0 && focusPointCandidate.x < 1 && focusPointCandidate.y > 0 && focusPointCandidate.y < 1)
focusPoint = focusPointCandidate;
}
}
// NB: In NPC coords, 0 = far plane, 1 = near plane.
const distanceToEye = 1.0 - npc.z;
const distanceToCenter = Math.min(npc.distanceXY(focusPoint) / 0.707, 1.0); // Math.sqrt(0.5) = 0.707
// Distance is a mix of the two previously computed values, still in range [0; 1]
// We use this factor to determine how much the distance to the center of attention is important compared to distance to the eye
const distanceToCenterWeight = 0.3;
const distance = distanceToEye * (1.0 - distanceToCenterWeight) + distanceToCenter * distanceToCenterWeight;
minDistance = Math.min(distance, minDistance);
}
return minDistance;
}
}
/** Exposed strictly for testing purposes.
* @internal
*/
export function createReaderPropsWithBaseUrl(streamBuffer, yAxisUp, baseUrl) {
let url;
if (baseUrl) {
try {
url = new URL(baseUrl);
}
catch {
url = undefined;
}
}
return GltfReaderProps.create(streamBuffer.nextBytes(streamBuffer.arrayBuffer.byteLength), yAxisUp, url);
}
//# sourceMappingURL=RealityTileLoader.js.map