@itwin/core-frontend
Version:
iTwin.js frontend components
325 lines • 13 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, 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