UNPKG

@itwin/core-frontend

Version:
325 lines • 13 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, ByteStream, compareStrings, DbOpcode, Id64, partitionArray, SortedArray, } from "@itwin/core-bentley"; import { Range3d } from "@itwin/core-geometry"; import { TileFormat, } from "@itwin/core-common"; import { IModelApp } from "../../IModelApp"; import { ImdlReader, IModelTileTree, Tile, } from "../../tile/internal"; /** The root tile for the branch of an [[IModelTileTree]] containing graphics for elements that have been modified during the current * Not intended for direct consumption - exported for use by [[IModelTileTree]]. * [[GraphicalEditingScope]]. */ export class DynamicIModelTile extends Tile { constructor(params, tree) { super(params, tree); } static create(root, elements) { return new RootTile(root, elements); } } /** The child tiles of the root tile, representing inserted or modified elements and sorted by element Id. */ class ElementTiles extends SortedArray { constructor() { super((lhs, rhs) => compareStrings(lhs.contentId, rhs.contentId)); } get array() { return this._array; } } /** The root tile. Each of its children represent a newly-inserted or modified element. */ class RootTile extends DynamicIModelTile { _hiddenElements; transformToTree; _elements; get _imodelRoot() { return this.parent; } get _elementChildren() { assert(undefined !== this.children); assert(this.children === this._elements.array); return this._elements.array; } constructor(parent, elements) { const params = { parent, isLeaf: false, contentId: "dynamic", range: Range3d.createNull(), maximumSize: parent.tileScreenSize, }; super(params, parent.tree); this._hiddenElements = new Id64.Uint32Set(); const inverseTransform = parent.tree.iModelTransform.inverse(); assert(undefined !== inverseTransform); this.transformToTree = inverseTransform; this._elements = new ElementTiles(); this.loadChildren(); // initially empty. assert(undefined !== this.children); this.setIsReady(); this.handleGeometryChanges(elements); } get hiddenElements() { return this._hiddenElements.toId64Array(); } get dynamicElements() { return this._elements.array.map((tile) => tile.contentId); } get appearanceProvider() { return this; } getFeatureAppearance(source, elemLo, elemHi, subcatLo, subcatHi, geomClass, modelLo, modelHi, type, animationNodeId) { if (this._hiddenElements.has(elemLo, elemHi)) return undefined; return source.getAppearance(elemLo, elemHi, subcatLo, subcatHi, geomClass, modelLo, modelHi, type, animationNodeId); } handleGeometryChanges(changes) { assert(undefined !== this.children); for (const change of changes) { if (change.type !== DbOpcode.Insert) this._hiddenElements.addId(change.id); let tile = this._elements.findEquivalent((t) => compareStrings(t.contentId, change.id)); if (change.type === DbOpcode.Delete) { if (tile) { tile[Symbol.dispose](); this._elements.remove(tile); } } else { const range = change.range.isNull ? change.range.clone() : this.transformToTree.multiplyRange(change.range); if (tile) tile.update(range); else this._elements.insert(tile = new ElementTile(this, change.id, range)); } } // Recompute range. this.range.setNull(); for (const element of this._elements) this.range.extendRange(element.range); this._imodelRoot.updateDynamicRange(this); } _loadChildren(resolve, _reject) { // This is invoked from constructor. We will add a child per element later - for now just mark the children as having been loaded. resolve(this._elements.array); } get channel() { throw new Error("Root dynamic tile has no content"); } async requestContent() { assert(false, "Root dynamic tile has no content"); return undefined; } async readContent(_data, _system, _isCanceled) { throw new Error("Root dynamic tile has no content"); } selectTiles(selected, args) { for (const child of this._elementChildren) child.selectTiles(selected, args); } pruneChildren(olderThan) { // Never discard ElementTiles - do discard not-recently-used graphics. for (const child of this._elementChildren) child.pruneChildren(olderThan); } } /** Represents a single element that has been inserted or had its geometric properties modified during the current [[GraphicalEditingScope]]. * It has no graphics of its own; it has any number of child tiles, each of which have graphics of a different level of detail. * Its contentId is the element's Id. */ class ElementTile extends Tile { constructor(parent, elementId, range) { super({ parent, isLeaf: false, contentId: elementId, range, maximumSize: parent.maximumSize, }, parent.tree); this.loadChildren(); this.setIsReady(); } _loadChildren(resolve, _reject) { // Invoked from constructor. We'll add child tiles later as needed. resolve([]); } get channel() { throw new Error("ElementTile has no content"); } async requestContent(_isCanceled) { assert(false, "ElementTile has no content"); return undefined; } async readContent(_data, _system, _isCanceled) { throw new Error("ElementTile has no content"); } pruneChildren(olderThan) { const children = this.children; assert(undefined !== children); const partitionIndex = partitionArray(children, (child) => !child.usageMarker.isExpired(olderThan)); // Remove expired children. if (partitionIndex < children.length) { const expired = children.splice(partitionIndex); for (const child of expired) child[Symbol.dispose](); } // Restore ordering. children.sort((x, y) => y.toleranceLog10 - x.toleranceLog10); } selectTiles(selected, args) { assert(undefined !== this.children); if (this.isRegionCulled(args)) return; args.markUsed(this); // ###TODO: Test content range culled. // Compute the ideal chord tolerance. assert(this.maximumSize > 0); const pixelSize = args.getPixelSizeInMetersAtClosestPoint(this); assert(pixelSize > 0); // Round down to the nearest power of ten. const toleranceLog10 = Math.floor(Math.log10(pixelSize)); // Find (or create) a child tile of desired tolerance. Also find a child tile that can be substituted for the desired tile if that tile's content is not yet loaded. // NB: Children are sorted in descending order by log10(tolerance) const children = this.children; let closestMatch; let exactMatch; for (let i = 0; i < children.length; i++) { const child = children[i]; const tol = child.toleranceLog10; if (tol > toleranceLog10) { assert(undefined === exactMatch); if (child.hasGraphics) closestMatch = child; } else if (tol === toleranceLog10) { exactMatch = child; } else if (tol < toleranceLog10) { if (!exactMatch) children.splice(i++, 0, exactMatch = new GraphicsTile(this, toleranceLog10)); if (child.hasGraphics && (!closestMatch || closestMatch.toleranceLog10 > toleranceLog10)) closestMatch = child; } } if (!exactMatch) { assert(children.length === 0 || children[children.length - 1].toleranceLog10 > toleranceLog10); children.push(exactMatch = new GraphicsTile(this, toleranceLog10)); } if (!exactMatch.isReady) { args.insertMissing(exactMatch); if (closestMatch) { selected.push(closestMatch); args.markUsed(closestMatch); } } else if (exactMatch.hasGraphics) { selected.push(exactMatch); args.markUsed(exactMatch); } } update(range) { range.clone(this.range); const center = this.range.low.interpolate(0.5, this.range.high); const radius = 0.5 * this.range.low.distance(this.range.high); this.boundingSphere.init(center, radius); // Discard out-dated graphics. assert(undefined !== this.children); for (const child of this.children) child[Symbol.dispose](); this.children.length = 0; } } function* makeIdSequence() { let current = 0; while (true) { if (current >= Number.MAX_SAFE_INTEGER) current = 0; yield ++current; } } const requestIdSequence = makeIdSequence(); /** Supplies graphics of a specific LOD for a single element. */ class GraphicsTile extends Tile { toleranceLog10; tolerance; constructor(parent, toleranceLog10) { assert(Math.round(toleranceLog10) === toleranceLog10); super({ parent, isLeaf: true, contentId: `${parent.contentId}_${toleranceLog10}`, range: parent.range, maximumSize: parent.maximumSize, }, parent.tree); this.toleranceLog10 = toleranceLog10; this.tolerance = 10 ** toleranceLog10; } computeLoadPriority() { // We want the element's graphics to be updated as soon as possible return 0; } _loadChildren(resolve, _reject) { resolve(undefined); } get channel() { return IModelApp.tileAdmin.channels.elementGraphicsRpc; } async requestContent(_isCanceled) { // ###TODO tree flags (enforce display priority) // ###TODO classifiers, animation assert(undefined !== this.parent); const requestId = requestIdSequence.next(); assert(!requestId.done); assert(this.tree instanceof IModelTileTree); const idProvider = this.tree.contentIdProvider; const props = { id: requestId.value.toString(16), elementId: this.parent.contentId, toleranceLog10: this.toleranceLog10, formatVersion: idProvider.majorFormatVersion, location: this.tree.iModelTransform.toJSON(), contentFlags: idProvider.contentFlags, omitEdges: !this.tree.edgeOptions, edgeType: this.tree.edgeOptions && "non-indexed" !== this.tree.edgeOptions.type ? 2 : 1, smoothPolyfaceEdges: this.tree.edgeOptions && this.tree.edgeOptions.smooth, clipToProjectExtents: this.tree.is3d, sectionCut: this.tree.stringifiedSectionClip, useAbsolutePositions: true, }; return IModelApp.tileAdmin.requestElementGraphics(this.tree.iModel, props); } async readContent(data, system, isCanceled) { if (undefined === isCanceled) isCanceled = () => !this.isLoading; assert(data instanceof Uint8Array); const stream = ByteStream.fromUint8Array(data); const position = stream.curPos; const format = stream.readUint32(); stream.curPos = position; // ###TODO: IModelGraphics format wraps IModel format. assert(TileFormat.IModel === format); const tree = this.tree; assert(tree instanceof IModelTileTree); const { iModel, modelId, is3d, containsTransformNodes } = tree; const reader = ImdlReader.create({ stream, iModel, modelId, is3d, system, isCanceled, containsTransformNodes, type: tree.batchType, loadEdges: false !== tree.edgeOptions, options: { tileId: this.contentId }, timeline: tree.timeline, }); let content = { isLeaf: true }; if (reader) { try { content = await reader.read(); } catch { // } } return content; } } //# sourceMappingURL=DynamicIModelTile.js.map