UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

352 lines • 15.5 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 Rendering */ import { assert, expectDefined, Id64, } from "@itwin/core-bentley"; import { BatchType, Feature, FeatureTable, ModelFeature, PackedFeature } from "../FeatureTable"; import { GeometryClass } from "../GeometryParams"; /** * An immutable, packed representation of a [[FeatureTable]]. The features are packed into a single array of 32-bit integer values, * wherein each feature occupies 3 32-bit integers. * @internal */ export class PackedFeatureTable { data; batchModelId; batchModelIdPair; numFeatures; anyDefined; type; animationNodeIds; get byteLength() { return this.data.byteLength; } /** Construct a PackedFeatureTable from the packed binary data. * This is used internally when deserializing Tiles in iMdl format. * @internal */ constructor(data, modelId, numFeatures, type, animationNodeIds) { this.data = data; this.batchModelId = modelId; this.batchModelIdPair = Id64.getUint32Pair(modelId); this.numFeatures = numFeatures; this.type = type; this.animationNodeIds = animationNodeIds; switch (this.numFeatures) { case 0: this.anyDefined = false; break; case 1: this.anyDefined = ModelFeature.isDefined(this.getFeature(0, ModelFeature.create())); break; default: this.anyDefined = true; break; } assert(this.data.length >= this._subCategoriesOffset); assert(undefined === this.animationNodeIds || this.animationNodeIds.length === this.numFeatures); } /** Create a packed feature table from a [[FeatureTable]]. */ static pack(featureTable) { // We must determine how many subcategories we have ahead of time to compute the size of the Uint32Array, as // the array cannot be resized after it is created. // We are not too worried about this as FeatureTables created on the front-end will contain few if any features; those obtained from the // back-end arrive within tiles already in the packed format. const subcategories = new Map(); for (const iv of featureTable.getArray()) { const found = subcategories.get(iv.value.subCategoryId.toString()); if (undefined === found) subcategories.set(iv.value.subCategoryId, subcategories.size); } // We need 3 32-bit integers per feature, plus 2 32-bit integers per subcategory. const subCategoriesOffset = 3 * featureTable.length; const nUint32s = subCategoriesOffset + 2 * subcategories.size; const uint32s = new Uint32Array(nUint32s); for (const iv of featureTable.getArray()) { const feature = iv.value; const index = iv.index * 3; let subCategoryIndex = expectDefined(subcategories.get(feature.subCategoryId)); assert(undefined !== subCategoryIndex); // we inserted it above... subCategoryIndex |= (feature.geometryClass << 24); uint32s[index + 0] = Id64.getLowerUint32(feature.elementId); uint32s[index + 1] = Id64.getUpperUint32(feature.elementId); uint32s[index + 2] = subCategoryIndex; } subcategories.forEach((index, id, _map) => { const index32 = subCategoriesOffset + 2 * index; uint32s[index32 + 0] = Id64.getLowerUint32(id); uint32s[index32 + 1] = Id64.getUpperUint32(id); }); return new PackedFeatureTable(uint32s, featureTable.modelId, featureTable.length, featureTable.type); } /** Retrieve the Feature associated with the specified index. */ getFeature(featureIndex, result) { const packed = this.getPackedFeature(featureIndex, scratchPackedFeature); return ModelFeature.unpack(packed, result, this.batchModelId); } /** Returns the Feature associated with the specified index, or undefined if the index is out of range. */ findFeature(featureIndex, result) { return featureIndex < this.numFeatures ? this.getFeature(featureIndex, result) : undefined; } /** @internal */ getElementIdPair(featureIndex, out) { out = out ?? { lower: 0, upper: 0 }; assert(featureIndex < this.numFeatures); const offset = 3 * featureIndex; out.lower = this.data[offset]; out.upper = this.data[offset + 1]; return out; } /** @internal */ getSubCategoryIdPair(featureIndex) { const index = 3 * featureIndex; let subCatIndex = this.data[index + 2]; subCatIndex = (subCatIndex & 0x00ffffff) >>> 0; subCatIndex = subCatIndex * 2 + this._subCategoriesOffset; return { lower: this.data[subCatIndex], upper: this.data[subCatIndex + 1] }; } /** @internal */ getAnimationNodeId(featureIndex) { return undefined !== this.animationNodeIds && featureIndex < this.numFeatures ? this.animationNodeIds[featureIndex] : 0; } /** @internal */ getPackedFeature(featureIndex, result) { assert(featureIndex < this.numFeatures); const index32 = 3 * featureIndex; result.elementId.lower = this.data[index32]; result.elementId.upper = this.data[index32 + 1]; const subCatIndexAndClass = this.data[index32 + 2]; result.geometryClass = (subCatIndexAndClass >>> 24) & 0xff; let subCatIndex = (subCatIndexAndClass & 0x00ffffff) >>> 0; subCatIndex = subCatIndex * 2 + this._subCategoriesOffset; result.subCategoryId.lower = this.data[subCatIndex]; result.subCategoryId.upper = this.data[subCatIndex + 1]; result.animationNodeId = this.getAnimationNodeId(featureIndex); result.modelId.lower = this.batchModelIdPair.lower; result.modelId.upper = this.batchModelIdPair.upper; return result; } getModelIdPair(_featureIndex, out) { out.lower = this.batchModelIdPair.lower; out.upper = this.batchModelIdPair.upper; return out; } /** Returns the element ID of the Feature associated with the specified index, or undefined if the index is out of range. */ findElementId(featureIndex) { if (featureIndex >= this.numFeatures) return undefined; else return this.readId(3 * featureIndex); } /** Return true if this table contains exactly 1 feature. */ get isUniform() { return 1 === this.numFeatures; } /** If this table contains exactly 1 feature, return it. */ getUniform(result) { return this.isUniform ? this.getFeature(0, result) : undefined; } get isVolumeClassifier() { return BatchType.VolumeClassifier === this.type; } get isPlanarClassifier() { return BatchType.VolumeClassifier === this.type; } get isClassifier() { return this.isVolumeClassifier || this.isPlanarClassifier; } /** Unpack the features into a [[FeatureTable]]. */ unpack() { const table = new FeatureTable(this.numFeatures, this.batchModelId); const feature = ModelFeature.create(); for (let i = 0; i < this.numFeatures; i++) { this.getFeature(i, feature); table.insertWithIndex(new Feature(feature.elementId, feature.subCategoryId, feature.geometryClass), i); } return table; } populateAnimationNodeIds(computeNodeId, maxNodeId) { assert(undefined === this.animationNodeIds); this.animationNodeIds = populateAnimationNodeIds(this, computeNodeId, maxNodeId); } *iterator(output) { for (let i = 0; i < this.numFeatures; i++) { this.getPackedFeature(i, output); output.index = i; yield output; } } iterable(output) { return { [Symbol.iterator]: () => this.iterator(output), }; } get _subCategoriesOffset() { return this.numFeatures * 3; } readId(offset32) { return Id64.fromUint32Pair(this.data[offset32], this.data[offset32 + 1]); } } const scratchPackedFeatureModelEntry = { lastFeatureIndex: -1, idLower: -1, idUpper: -1 }; /** A table of model Ids associated with a [[MultiModelPackedFeatureTable]]. * The feature indices in the packed feature table are grouped together by model, such that the first N features belong to model 1, the next M features to model 2, and so on. * The model table itself consists of one entry per model, where each entry looks like: * indexOfLastFeatureInModel: u32 * modelId: u64 * The modelId associated with a feature can therefore be derived by finding the entry in the model table with the highest indexOfLastFeatureInModel no greater than the feature index. * This lookup can be optimized using binary search. * Moreover, while iterating the feature table in sequence, the model table can be iterated in parallel so that no per-feature lookup of model Id is required. * @internal */ export class PackedFeatureModelTable { _data; constructor(data) { this._data = data; assert(this._data.length % 3 === 0); } /** The number of models in the table. */ get length() { return this._data.length / 3; } get byteLength() { return this._data.byteLength; } getLastFeatureIndex(modelIndex) { return this._data[modelIndex * 3]; } getEntry(modelIndex, result) { if (modelIndex >= this.length) { result.idLower = result.idUpper = 0; result.lastFeatureIndex = Number.MAX_SAFE_INTEGER; return result; } const index = modelIndex * 3; result.lastFeatureIndex = this._data[index + 0]; result.idLower = this._data[index + 1]; result.idUpper = this._data[index + 2]; return result; } /** Get the Id of the model associated with the specified feature, or an invalid Id if the feature is not associated with any model. */ getModelIdPair(featureIndex, result) { if (!result) result = { lower: 0, upper: 0 }; else result.lower = result.upper = 0; let first = 0; const last = this.length; let count = last; while (count > 0) { const step = Math.floor(count / 2); const mid = first + step; const lastFeatureIndex = this.getLastFeatureIndex(mid); if (featureIndex > lastFeatureIndex) { first = mid + 1; count -= step + 1; } else { count = step; } } if (first < last) { result.lower = this._data[first * 3 + 1]; result.upper = this._data[first * 3 + 2]; } return result; } } /** A PackedFeatureTable with a PackedFeatureModelTable appended to it, capable of storing features belonging to more than one model. * @internal */ export class MultiModelPackedFeatureTable { _features; _models; constructor(features, models) { this._features = features; this._models = models; } static create(data, batchModelId, numFeatures, type, numSubCategories) { const modelTableOffset = 3 * numFeatures + 2 * numSubCategories; const featureData = data.subarray(0, modelTableOffset); const features = new PackedFeatureTable(featureData, batchModelId, numFeatures, type); const modelData = data.subarray(modelTableOffset); const models = new PackedFeatureModelTable(modelData); return new MultiModelPackedFeatureTable(features, models); } get batchModelId() { return this._features.batchModelId; } get batchModelIdPair() { return this._features.batchModelIdPair; } get numFeatures() { return this._features.numFeatures; } get type() { return this._features.type; } get animationNodeIds() { return this._features.animationNodeIds; } set animationNodeIds(ids) { this._features.animationNodeIds = ids; } get byteLength() { return this._features.byteLength + this._models.byteLength; } getPackedFeature(featureIndex, result) { this._features.getPackedFeature(featureIndex, result); this._models.getModelIdPair(featureIndex, result.modelId); return result; } getFeature(featureIndex, result) { const packed = this.getPackedFeature(featureIndex, scratchPackedFeature); return ModelFeature.unpack(packed, result); } findFeature(featureIndex, result) { return featureIndex < this.numFeatures ? this.getFeature(featureIndex, result) : undefined; } getElementIdPair(featureIndex, out) { return this._features.getElementIdPair(featureIndex, out); } getModelIdPair(featureIndex, out) { this._models.getModelIdPair(featureIndex, out); return out; } findElementId(featureIndex) { return this._features.findElementId(featureIndex); } *iterator(output) { // Rather than perform a binary search on the model table to find each feature's model Id, traverse the model table in parallel with the feature table. let modelIndex = 0; const modelEntry = this._models.getEntry(modelIndex, scratchPackedFeatureModelEntry); for (let featureIndex = 0; featureIndex < this.numFeatures; featureIndex++) { if (featureIndex > modelEntry.lastFeatureIndex) this._models.getEntry(++modelIndex, modelEntry); this._features.getPackedFeature(featureIndex, output); output.modelId.lower = modelEntry.idLower; output.modelId.upper = modelEntry.idUpper; output.index = featureIndex; yield output; } } iterable(output) { return { [Symbol.iterator]: () => this.iterator(output), }; } getAnimationNodeId(featureIndex) { return this._features.getAnimationNodeId(featureIndex); } populateAnimationNodeIds(computeNodeId, maxNodeId) { this._features.animationNodeIds = populateAnimationNodeIds(this, computeNodeId, maxNodeId); } } export function createPackedFeature() { const pair = { upper: 0, lower: 0 }; return { modelId: { ...pair }, elementId: { ...pair }, subCategoryId: { ...pair }, geometryClass: GeometryClass.Primary, animationNodeId: 0, }; } const scratchPackedFeature = createPackedFeature(); function populateAnimationNodeIds(table, computeNodeId, maxNodeId) { assert(maxNodeId > 0); let nodeIds; const outputFeature = PackedFeature.createWithIndex(); for (const feature of table.iterable(outputFeature)) { const nodeId = computeNodeId(feature); assert(nodeId <= maxNodeId); if (0 !== nodeId) { if (!nodeIds) { const size = table.numFeatures; nodeIds = maxNodeId < 0x100 ? new Uint8Array(size) : (maxNodeId < 0x10000 ? new Uint16Array(size) : new Uint32Array(size)); } nodeIds[feature.index] = nodeId; } } return nodeIds; } //# sourceMappingURL=PackedFeatureTable.js.map