UNPKG

@itwin/core-frontend

Version:
565 lines • 27.6 kB
/*--------------------------------------------------------------------------------------------- * 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