@itwin/core-common
Version:
iTwin.js components common to frontend and backend
352 lines • 15.5 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 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