@itwin/core-frontend
Version:
iTwin.js frontend components
565 lines • 27.6 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, dispose } from "@itwin/core-bentley";
import { Arc3d, ClipPlaneContainment, Point2d, Point4d, Vector3d } from "@itwin/core-geometry";
import { BoundingSphere, ColorDef, Frustum, FrustumPlanes } from "@itwin/core-common";
import { IModelApp } from "../IModelApp";
import { TileRequest, TileTreeLoadStatus, TileUsageMarker, } from "./internal";
// cSpell:ignore undisplayable bitfield
const scratchRange2d = [new Point2d(), new Point2d(), new Point2d(), new Point2d()];
/** @internal */
export function addRangeGraphic(builder, range, is2d) {
if (!is2d) {
builder.addRangeBox(range);
return;
}
// 3d box is useless in 2d and will be clipped by near/far planes anyway
const pts = scratchRange2d;
pts[0].set(range.low.x, range.low.y);
pts[1].set(range.high.x, range.low.y);
pts[2].set(range.high.x, range.high.y);
pts[3].set(range.low.x, range.high.y);
builder.addLineString2d(pts, 0);
}
const scratchWorldFrustum = new Frustum();
const scratchRootFrustum = new Frustum();
const scratchWorldSphere = new BoundingSphere();
const scratchPoint4d = Point4d.createZero();
const scratchFrustum = new Frustum();
/** A 3d tile within a [[TileTree]].
*
* A tile represents the contents of some sub-volume of the tile tree's volume. It may produce graphics representing those contents, or may have no graphics.
* A tile can have child tiles that further sub-divide its own volume, providing higher-resolution representations of its contents. A tile that has no children is
* referred to as a "leaf" of the tile tree. A non-leaf tile's children are produced when they are needed, and discarded when no longer needed.
* A tile's contents can be discarded at any time by [[TileAdmin]] when GPU memory needs to be reclaimed; or when the Tile itself is discarded via
* [[Tile.dispose]].
*
* Several public [[Tile]] methods carry a warning that they should **not** be overridden by subclasses; typically a protected method exists that can be overridden instead.
* For example, [[loadChildren]] should not be overridden, but it calls [[_loadChildren]], which must be overridden because it is abstract.
* @public
* @extensions
*/
export class Tile {
_state = 0 /* TileState.NotReady */;
_children;
_rangeGraphic;
_rangeGraphicType = TileBoundingBoxes.None;
/** This tile's renderable content. */
_graphic;
/** True if this tile ever had graphics loaded. Used to determine when a tile's graphics were later freed to conserve memory. */
_hadGraphics = false;
/** Uniquely identifies this tile's content in the context of its tree. */
_contentId;
/** The current loading state of this tile's children. Child tiles are loaded on-demand, potentially asynchronously. */
_childrenLoadStatus;
/** @internal */
_request;
/** @internal */
_isLeaf;
/** A volume no larger than this tile's `range`, and optionally more tightly encompassing its contents, used for more accurate culling.
* [[contentRange]] uses this range if defined; otherwise it uses [[range]].
*/
_contentRange;
/** The maximum size in pixels this tile can be drawn. If the size of the tile on screen exceeds this maximum, a higher-resolution tile should be drawn in its place. */
_maximumSize;
/** The [[TileTree]] to which this tile belongs. */
tree;
/** The volume of space occupied by this tile. Its children are guaranteed to also be contained within this volume. */
range;
/** The parent of this tile, or undefined if it is the [[TileTree]]'s root tile. */
parent;
/** The depth of this tile within its [[TileTree]]. The root tile has a depth of zero. */
depth;
/** The bounding sphere for this tile. */
boundingSphere;
/** The point at the center of this tile's volume. */
get center() { return this.boundingSphere.center; }
/** The radius of a sphere fully encompassing this tile's volume - used for culling. */
get radius() { return this.boundingSphere.radius; }
/** Tracks the usage of this tile. After a period of disuse, the tile may be [[prune]]d to free up memory. */
usageMarker = new TileUsageMarker();
/** Exclusively for use by LRUTileList. @internal */
previous;
/** Exclusively for use by LRUTileList. @internal */
next;
/** Exclusively for use by LRUTileList. @internal */
bytesUsed = 0;
/** Exclusively for use by LRUTileList. @internal */
tileUserIds;
/** Constructor */
constructor(params, tree) {
this.tree = tree;
this.parent = params.parent;
this.depth = undefined !== this.parent ? this.parent.depth + 1 : 0;
this.range = params.range;
this._maximumSize = params.maximumSize;
this._contentRange = params.contentRange;
this._contentId = params.contentId;
const center = this.range.low.interpolate(0.5, this.range.high);
const radius = 0.5 * this.range.low.distance(this.range.high);
this.boundingSphere = new BoundingSphere(center, radius);
if (params.maximumSize <= 0)
this.setIsReady();
this._isLeaf = true === params.isLeaf;
this._childrenLoadStatus = (undefined === tree.maxDepth || this.depth < tree.maxDepth) ? TileTreeLoadStatus.NotLoaded : TileTreeLoadStatus.Loaded;
}
/** Free memory-consuming resources owned by this tile to reduce memory pressure.
* By default, this calls [[disposeContents]]. Problematic subclasses (MapTile, ImageryMapTile) may opt out for now by overriding this method to do nothing.
* That option may be removed in the future.
* @alpha
*/
freeMemory() {
this.disposeContents();
}
/** Dispose of resources held by this tile. */
disposeContents() {
this._state = 0 /* TileState.NotReady */;
this._graphic = dispose(this._graphic);
this._rangeGraphic = dispose(this._rangeGraphic);
this._rangeGraphicType = TileBoundingBoxes.None;
IModelApp.tileAdmin.onTileContentDisposed(this);
}
/** Dispose of resources held by this tile and all of its children, marking it and all of its children as "abandoned". */
[Symbol.dispose]() {
this.disposeContents();
this._state = 5 /* TileState.Abandoned */;
this.disposeChildren();
}
/** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */
dispose() {
this[Symbol.dispose]();
}
/** This tile's child tiles, if they exist and are loaded. The children are fully contained within this tile's volume and provide higher-resolution graphics than this tile.
* @see [[loadChildren]]
*/
get children() { return this._children; }
/** The [[IModelConnection]] to which this tile belongs. */
get iModel() { return this.tree.iModel; }
/** Uniquely identifies this tile's content. */
get contentId() { return this._contentId; }
/** True if this tile's content is currently being loaded. */
get isLoading() { return TileLoadStatus.Loading === this.loadStatus; }
/** True if a request for this tile's content has been enqueued. */
get isQueued() { return TileLoadStatus.Queued === this.loadStatus; }
/** True if an attempt to load this tile's content failed. */
get isNotFound() { return TileLoadStatus.NotFound === this.loadStatus; }
/** True if this tile's content has been loaded and is ready to be drawn. */
get isReady() { return TileLoadStatus.Ready === this.loadStatus; }
/** Indicates the tile should not be selected for display because it is out of the range of LODs supported by the tile provider.
* @see [[ImageryMapTile.isOutOfLodRange]].
* @alpha
*/
get isOutOfLodRange() { return false; }
/** @public */
setNotFound() {
this._state = 4 /* TileState.NotFound */;
}
/** @public */
setIsReady() {
if (this.hasGraphics)
this._hadGraphics = true;
this._state = 3 /* TileState.Ready */;
IModelApp.tileAdmin.onTileContentLoaded(this);
}
/** @public */
setLeaf() {
// Don't potentially re-request the children later.
this.disposeChildren();
this._isLeaf = true;
this._childrenLoadStatus = TileTreeLoadStatus.Loaded;
}
/** True if this tile has no child tiles. */
get isLeaf() { return this._isLeaf; }
/** @internal */
get isEmpty() { return this.isReady && !this.hasGraphics && this.isLeaf; }
/** @internal */
get isDisplayable() { return 0 < this.maximumSize; }
/** The maximum size in pixels this tile can be drawn. If the size of the tile on screen exceeds this maximum, a higher-resolution tile should be drawn in its place. */
get maximumSize() { return this._maximumSize; }
/** @internal */
get isParentDisplayable() { return undefined !== this.parent && this.parent.isDisplayable; }
/** @internal */
get isUndisplayableRootTile() { return undefined === this.parent && !this.isDisplayable; }
/** @internal */
get request() { return this._request; }
set request(request) {
assert(undefined === request || undefined === this.request);
this._request = request;
}
/** Compute the load priority of this tile. This determines which tiles' contents are requested first.
* @param _viewports The subset of `users` that are [[Viewport]]s - typically, these viewports want to display the tile's content.
* @param users The [[TileUser]]s that are currently using the tile for some purpose, such as displaying its content.
* @returns The priority.
* @see [[TileLoadPriority]] for suggested priority values.
*/
computeLoadPriority(_viewports, _users) {
return this.depth;
}
/** True if this tile has graphics ready to draw. */
get hasGraphics() { return undefined !== this._graphic; }
/** True if this tile has a known volume tightly encompassing its graphics. */
get hasContentRange() { return undefined !== this._contentRange; }
/** A volume no larger than this tile's `range`, and optionally more tightly encompassing its contents, used for more accurate culling. */
get contentRange() {
if (undefined !== this._contentRange)
return this._contentRange;
else if (undefined === this.parent && undefined !== this.tree.contentRange)
return this.tree.contentRange;
else
return this.range;
}
/** Tile contents are loaded asynchronously on demand. This member tracks the current loading status of this tile's contents. */
get loadStatus() {
switch (this._state) {
case 0 /* TileState.NotReady */: {
if (undefined === this.request)
return TileLoadStatus.NotLoaded;
else if (TileRequest.State.Loading === this.request.state)
return TileLoadStatus.Loading;
assert(TileRequest.State.Completed !== this.request.state && TileRequest.State.Failed !== this.request.state); // this.request should be undefined in these cases...
return TileLoadStatus.Queued;
}
case 3 /* TileState.Ready */: {
assert(undefined === this.request);
return TileLoadStatus.Ready;
}
case 4 /* TileState.NotFound */: {
assert(undefined === this.request);
return TileLoadStatus.NotFound;
}
default: {
assert(5 /* TileState.Abandoned */ === this._state);
return TileLoadStatus.Abandoned;
}
}
}
/** Produce the graphics that should be drawn. */
produceGraphics() {
return this._graphic;
}
setGraphic(graphic) {
dispose(this._graphic);
this._graphic = graphic;
this.setIsReady();
}
/** Set this tile's content to the result of [[readContent]] */
setContent(content) {
const { graphic, isLeaf, contentRange } = content;
this.setGraphic(graphic);
if (undefined !== isLeaf && isLeaf !== this._isLeaf) {
if (isLeaf)
this.setLeaf();
else
this._isLeaf = false;
}
if (undefined !== contentRange)
this._contentRange = contentRange;
this.setIsReady();
}
/** Disclose any resources owned by this tile, other than its [[RenderGraphic]].
* @internal
*/
_collectStatistics(_stats) { }
/** Disclose resources owned by this tile and (by default) all of its child tiles.
* @note Do not override this method! Override `_collectStatistics` instead.
* @internal
*/
collectStatistics(stats, includeChildren = true) {
if (undefined !== this._graphic)
this._graphic.collectStatistics(stats);
this._collectStatistics(stats);
if (!includeChildren)
return;
const children = this.children;
if (undefined !== children)
for (const child of children)
child.collectStatistics(stats);
}
/** If this tile's child tiles have not yet been requested, enqueue an asynchronous request to load them.
* @note This function itself is *not* asynchronous - it immediately returns the current loading status.
* @note Do not override this method - implement [[_loadChildren]].
*/
loadChildren() {
if (this._childrenLoadStatus !== TileTreeLoadStatus.NotLoaded)
return this._childrenLoadStatus;
this._childrenLoadStatus = TileTreeLoadStatus.Loading;
this._loadChildren((children) => {
this._children = children;
this._childrenLoadStatus = TileTreeLoadStatus.Loaded;
if (undefined === children || 0 === children.length)
this._isLeaf = true;
IModelApp.tileAdmin.onTileChildrenLoad.raiseEvent(this);
}, (_error) => {
this._isLeaf = true;
this._childrenLoadStatus = TileTreeLoadStatus.NotFound;
IModelApp.tileAdmin.onTileChildrenLoad.raiseEvent(this);
});
return this._childrenLoadStatus;
}
/** Dispose of this tile's child tiles and mark them as "not loaded". */
disposeChildren() {
const children = this.children;
if (undefined === children)
return;
for (const child of children)
child[Symbol.dispose]();
this._childrenLoadStatus = TileTreeLoadStatus.NotLoaded;
this._children = undefined;
}
/** Returns true if this tile's bounding volume is culled by the frustum or clip volumes specified by `args`. */
isRegionCulled(args) {
return this.isCulled(this.range, args, true, this.boundingSphere);
}
/** Returns true if this tile's content bounding volume is culled by the frustum or clip volumes specified by `args`. */
isContentCulled(args) {
return this.isCulled(this.contentRange, args, false);
}
isCulled(range, args, testClipIntersection, sphere) {
const box = Frustum.fromRange(range, scratchRootFrustum);
return this.isFrustumCulled(box, args, testClipIntersection, sphere);
}
isFrustumCulled(box, args, testClipIntersection, sphere) {
const worldBox = box.transformBy(args.location, scratchWorldFrustum);
const worldSphere = sphere?.transformBy(args.location, scratchWorldSphere);
// Test against frustum.
if (FrustumPlanes.Containment.Outside === args.frustumPlanes.computeFrustumContainment(worldBox, worldSphere))
return true;
// Test against TileTree's own clip volume, if any.
if (undefined !== args.clip && ClipPlaneContainment.StronglyOutside === args.clip.classifyPointContainment(worldBox.points))
return true;
// Test against view clip, if any (will be undefined if TileTree does not want view clip applied to it).
if (undefined !== args.viewClip && ClipPlaneContainment.StronglyOutside === args.viewClip.classifyPointContainment(worldBox.points))
return true;
// Test against intersection clip - reject if tile doesn't intersect (used for section-cut graphics).
if (testClipIntersection && undefined !== args.intersectionClip && ClipPlaneContainment.Ambiguous !== args.intersectionClip.classifyPointContainment(worldBox.points))
return true;
return false;
}
/** Determine the visibility of this tile according to the specified args. */
computeVisibility(args) {
if (this.isEmpty)
return TileVisibility.OutsideFrustum;
if (args.boundingRange && !args.boundingRange.intersectsRange(this.range))
return TileVisibility.OutsideFrustum;
// NB: We test for region culling before isDisplayable - otherwise we will never unload children of undisplayed tiles when
// they are outside frustum
if (this.isRegionCulled(args))
return TileVisibility.OutsideFrustum;
// some nodes are merely for structure and don't have any geometry
if (!this.isDisplayable)
return TileVisibility.TooCoarse;
if (this.isLeaf) {
if (this.hasContentRange && this.isContentCulled(args))
return TileVisibility.OutsideFrustum;
else
return TileVisibility.Visible;
}
return this.meetsScreenSpaceError(args) ? TileVisibility.Visible : TileVisibility.TooCoarse;
}
/** Returns true if this tile is of at least high enough resolution to be displayed, per the supplied [[TileDrawArgs]]; or false if
* a higher-resolution tile should be substituted for it.
* This method is called by [[computeVisibility]] if the tile has passed all culling checks.
*/
meetsScreenSpaceError(args) {
const pixelSize = args.getPixelSize(this) * args.pixelSizeScaleFactor;
const maxSize = this.maximumSize * args.tileSizeModifier;
return pixelSize <= maxSize;
}
/** @internal */
extendRangeForContent(range, matrix, treeTransform, frustumPlanes) {
if (this.isEmpty || this.contentRange.isNull)
return;
const box = Frustum.fromRange(this.contentRange, scratchFrustum);
box.transformBy(treeTransform, box);
if (frustumPlanes !== undefined && FrustumPlanes.Containment.Outside === frustumPlanes.computeFrustumContainment(box))
return;
if (this.children === undefined) {
for (const boxPoint of box.points) {
const pt = matrix.multiplyPoint3d(boxPoint, 1, scratchPoint4d);
if (pt.w > .0001)
range.extendXYZW(pt.x, pt.y, pt.z, pt.w);
else
range.high.z = Math.max(1.0, range.high.z); // behind eye plane...
}
}
else {
for (const child of this.children)
child.extendRangeForContent(range, matrix, treeTransform, frustumPlanes);
}
}
/** Primarily for debugging purposes, compute the number of tiles below this one in the [[TileTree]]. */
countDescendants() {
const children = this.children;
if (undefined === children || 0 === children.length)
return 0;
let count = 0;
for (const child of children)
count += child.countDescendants();
return count;
}
/** Output this tile's graphics. */
drawGraphics(args) {
const gfx = this.produceGraphics();
if (undefined === gfx)
return;
args.graphics.add(gfx);
const rangeGfx = this.getRangeGraphic(args.context);
if (undefined !== rangeGfx)
args.graphics.add(rangeGfx);
}
/** @internal */
get rangeGraphicColor() {
return this.isLeaf ? ColorDef.blue : ColorDef.green;
}
/** @internal */
getRangeGraphic(context) {
const type = context.viewport.debugBoundingBoxes;
if (type === this._rangeGraphicType)
return this._rangeGraphic;
this._rangeGraphic = dispose(this._rangeGraphic);
this._rangeGraphicType = type;
if (TileBoundingBoxes.None !== type) {
const builder = context.createSceneGraphicBuilder();
this.addRangeGraphic(builder, type);
this._rangeGraphic = builder.finish();
}
return this._rangeGraphic;
}
/** @internal */
addRangeGraphic(builder, type) {
if (TileBoundingBoxes.Both === type) {
builder.setSymbology(ColorDef.blue, ColorDef.blue, 1);
addRangeGraphic(builder, this.range, this.tree.is2d);
if (this.hasContentRange) {
builder.setSymbology(ColorDef.red, ColorDef.red, 1);
addRangeGraphic(builder, this.contentRange, this.tree.is2d);
}
}
else if (TileBoundingBoxes.Sphere === type) {
builder.setSymbology(ColorDef.green, ColorDef.green, 1);
const x = new Vector3d(this.radius, 0, 0);
const y = new Vector3d(0, this.radius, 0);
const z = new Vector3d(0, 0, this.radius);
builder.addArc(Arc3d.create(this.center, x, y), false, false);
builder.addArc(Arc3d.create(this.center, x, z), false, false);
builder.addArc(Arc3d.create(this.center, y, z), false, false);
}
else if (TileBoundingBoxes.SolidBox === type) {
const range = this.range;
let color = this.rangeGraphicColor;
builder.setSymbology(color, color, 1);
addRangeGraphic(builder, range, this.tree.is2d);
color = color.withTransparency(0xcf);
builder.setSymbology(color, color, 1);
builder.addRangeBox(range, true);
}
else {
const color = this.rangeGraphicColor;
builder.setSymbology(color, color, 1);
const range = TileBoundingBoxes.Content === type ? this.contentRange : this.range;
addRangeGraphic(builder, range, this.tree.is2d);
}
}
/** Optional corners used to compute the screen size of the tile. These are used, e.g., by reality tiles with oriented bounding boxes to
* produce more accurate size calculation.
*/
getSizeProjectionCorners() { return undefined; }
/** @internal */
clearLayers() {
this.disposeContents();
if (this.children)
for (const child of this.children)
(child).clearLayers();
}
}
/** Describes the current status of a [[Tile]]'s content. Tile content is loaded via an asynchronous [[TileRequest]].
* @see [[Tile.loadStatus]].
* @public
* @extensions
*/
export var TileLoadStatus;
(function (TileLoadStatus) {
/** No attempt to load the tile's content has been made, or the tile has since been unloaded. It currently has no graphics. */
TileLoadStatus[TileLoadStatus["NotLoaded"] = 0] = "NotLoaded";
/** A request has been dispatched to load the tile's contents, and a response is pending. */
TileLoadStatus[TileLoadStatus["Queued"] = 1] = "Queued";
/** A response has been received and the tile's graphics and other data are being loaded on the frontend. */
TileLoadStatus[TileLoadStatus["Loading"] = 2] = "Loading";
/** The tile has been loaded, and if the tile is displayable it has graphics. */
TileLoadStatus[TileLoadStatus["Ready"] = 3] = "Ready";
/** A request to load the tile's contents failed. */
TileLoadStatus[TileLoadStatus["NotFound"] = 4] = "NotFound";
/** The tile has been disposed. */
TileLoadStatus[TileLoadStatus["Abandoned"] = 5] = "Abandoned";
})(TileLoadStatus || (TileLoadStatus = {}));
/**
* Describes the visibility of a tile based on its size and a view frustum.
* @public
* @extensions
*/
export var TileVisibility;
(function (TileVisibility) {
/** The tile is entirely outside of the viewing frustum. */
TileVisibility[TileVisibility["OutsideFrustum"] = 0] = "OutsideFrustum";
/** The tile's graphics are of too low a resolution for the viewing frustum. */
TileVisibility[TileVisibility["TooCoarse"] = 1] = "TooCoarse";
/** The tile's graphics are of appropriate resolution for the viewing frustum. */
TileVisibility[TileVisibility["Visible"] = 2] = "Visible";
})(TileVisibility || (TileVisibility = {}));
/**
* Loosely describes the "importance" of a [[Tile]]. Requests for tiles of greater "importance" are prioritized for loading.
* @note A lower priority value indicates higher importance.
* @public
* @extensions
*/
export var TileLoadPriority;
(function (TileLoadPriority) {
/** Contents of geometric models that are being interactively edited. */
TileLoadPriority[TileLoadPriority["Dynamic"] = 5] = "Dynamic";
/** Background map tiles. */
TileLoadPriority[TileLoadPriority["Map"] = 15] = "Map";
/** Typically, tiles generated from the contents of geometric models. */
TileLoadPriority[TileLoadPriority["Primary"] = 20] = "Primary";
/** 3d terrain tiles onto which background map imagery is draped. */
TileLoadPriority[TileLoadPriority["Terrain"] = 10] = "Terrain";
/** Typically, reality models. */
TileLoadPriority[TileLoadPriority["Context"] = 40] = "Context";
/** Supplementary tiles used to classify the contents of geometric or reality models. */
TileLoadPriority[TileLoadPriority["Classifier"] = 50] = "Classifier";
})(TileLoadPriority || (TileLoadPriority = {}));
/**
* Options for displaying tile bounding boxes for debugging purposes.
*
* Bounding boxes are color-coded based on refinement strategy:
* - Blue: A leaf tile (has no child tiles).
* - Green: An ordinary tile (sub-divides into 4 or 8 child tiles).
* - Red: A tile which refines to a single higher-resolution child occupying the same volume.
* @see [[Viewport.debugBoundingBoxes]]
* @public
* @extensions
*/
export var TileBoundingBoxes;
(function (TileBoundingBoxes) {
/** Display no bounding boxes */
TileBoundingBoxes[TileBoundingBoxes["None"] = 0] = "None";
/** Display boxes representing the tile's full volume. */
TileBoundingBoxes[TileBoundingBoxes["Volume"] = 1] = "Volume";
/** Display boxes representing the range of the tile's contents, which may be tighter than (but never larger than) the tile's full volume. */
TileBoundingBoxes[TileBoundingBoxes["Content"] = 2] = "Content";
/** Display both volume and content boxes. */
TileBoundingBoxes[TileBoundingBoxes["Both"] = 3] = "Both";
/** Display boxes for direct children, where blue boxes indicate empty volumes. */
TileBoundingBoxes[TileBoundingBoxes["ChildVolumes"] = 4] = "ChildVolumes";
/** Display bounding sphere. */
TileBoundingBoxes[TileBoundingBoxes["Sphere"] = 5] = "Sphere";
/** Display a transparent solid box representing the tile's full volume.
* @alpha To be replaced with a separate option that applies to any of the other TileBoundingBoxes modes.
*/
TileBoundingBoxes[TileBoundingBoxes["SolidBox"] = 6] = "SolidBox";
})(TileBoundingBoxes || (TileBoundingBoxes = {}));
//# sourceMappingURL=Tile.js.map