src/XKTModel/writeXKTModelToArrayBuffer.js
import {XKT_INFO} from "../XKT_INFO.js";
import * as pako from 'pako';
import {TextEncoder} from "node:util"; // Use the V8's TextEncoder impl., otherwise the @loaders.gl/polyfill's one gets used, which is failing (at Array::push) for large metadata
const XKT_VERSION = XKT_INFO.xktVersion;
const NUM_TEXTURE_ATTRIBUTES = 9;
const NUM_MATERIAL_ATTRIBUTES = 6;
/**
* Writes an {@link XKTModel} to an {@link ArrayBuffer}.
*
* @param {XKTModel} xktModel The {@link XKTModel}.
* @param {String} metaModelJSON The metamodel JSON in a string.
* @param {Object} [stats] Collects statistics.
* @param {Object} options Options for how the XKT is written.
* @param {Boolean} [options.zip=true] ZIP the contents?
* @returns {ArrayBuffer} The {@link ArrayBuffer}.
*/
function writeXKTModelToArrayBuffer(xktModel, metaModelJSON, stats, options) {
if (! options.zip) {
return writeXKTModelToArrayBufferUncompressed(xktModel, metaModelJSON, stats);
}
const data = getModelData(xktModel, metaModelJSON, stats);
const deflatedData = deflateData(data, metaModelJSON, options);
stats.texturesSize += deflatedData.textureData.byteLength;
const arrayBuffer = createArrayBuffer(deflatedData);
return arrayBuffer;
}
// V11
function writeXKTModelToArrayBufferUncompressed(xktModel, metaModelJSON, stats) {
const data = getModelData(xktModel, metaModelJSON, stats);
stats.texturesSize += data.textureData.byteLength;
const object2Array = (function() {
const encoder = new TextEncoder();
return obj => encoder.encode(JSON.stringify(obj));
})();
const arrays = [
object2Array(metaModelJSON || data.metadata),
data.textureData,
data.eachTextureDataPortion,
data.eachTextureAttributes,
data.positions,
data.normals,
data.colors,
data.uvs,
data.indices,
data.edgeIndices,
data.eachTextureSetTextures,
data.matrices,
data.reusedGeometriesDecodeMatrix,
data.eachGeometryPrimitiveType,
data.eachGeometryPositionsPortion,
data.eachGeometryNormalsPortion,
data.eachGeometryColorsPortion,
data.eachGeometryUVsPortion,
data.eachGeometryIndicesPortion,
data.eachGeometryEdgeIndicesPortion,
data.eachMeshGeometriesPortion,
data.eachMeshMatricesPortion,
data.eachMeshTextureSet,
data.eachMeshMaterialAttributes,
object2Array(data.eachEntityId),
data.eachEntityMeshesPortion,
data.eachTileAABB,
data.eachTileEntitiesPortion
];
const arraysCnt = arrays.length;
const dataView = new DataView(new ArrayBuffer((1 + 2 * arraysCnt) * 4));
dataView.setUint32(0, XKT_VERSION, true);
let byteOffset = dataView.byteLength;
const offsets = [ ];
// Store arrays' offsets and lengths
for (let i = 0; i < arraysCnt; i++) {
const arr = arrays[i];
const BPE = arr.BYTES_PER_ELEMENT;
// align to BPE, so the arrayBuffer can be used for a typed array
byteOffset = Math.ceil(byteOffset / BPE) * BPE;
const byteLength = arr.byteLength;
const idx = 1 + 2 * i;
dataView.setUint32(idx * 4, byteOffset, true);
dataView.setUint32((idx + 1) * 4, byteLength, true);
offsets.push(byteOffset);
byteOffset += byteLength;
}
const dataArray = new Uint8Array(byteOffset);
dataArray.set(new Uint8Array(dataView.buffer), 0);
const requiresSwapToLittleEndian = (function() {
const buffer = new ArrayBuffer(2);
new Uint16Array(buffer)[0] = 1;
return new Uint8Array(buffer)[0] !== 1;
})();
// Store arrays themselves
for (let i = 0; i < arraysCnt; i++) {
const arr = arrays[i];
const subarray = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
const BPE = arr.BYTES_PER_ELEMENT;
if (requiresSwapToLittleEndian && (BPE > 1)) {
const swaps = BPE / 2;
const cnt = subarray.length / BPE;
for (let b = 0; b < cnt; b++) {
const offset = b * BPE;
for (let j = 0; j < swaps; j++) {
const i1 = offset + j;
const i2 = offset - j + BPE - 1;
const tmp = subarray[i1];
subarray[i1] = subarray[i2];
subarray[i2] = tmp;
}
}
}
dataArray.set(subarray, offsets[i]);
}
return dataArray.buffer;
}
function getModelData(xktModel, metaModelDataStr, stats) {
//------------------------------------------------------------------------------------------------------------------
// Allocate data
//------------------------------------------------------------------------------------------------------------------
const propertySetsList = xktModel.propertySetsList;
const metaObjectsList = xktModel.metaObjectsList;
const geometriesList = xktModel.geometriesList;
const texturesList = xktModel.texturesList;
const textureSetsList = xktModel.textureSetsList;
const meshesList = xktModel.meshesList;
const entitiesList = xktModel.entitiesList;
const tilesList = xktModel.tilesList;
const numPropertySets = propertySetsList.length;
const numMetaObjects = metaObjectsList.length;
const numGeometries = geometriesList.length;
const numTextures = texturesList.length;
const numTextureSets = textureSetsList.length;
const numMeshes = meshesList.length;
const numEntities = entitiesList.length;
const numTiles = tilesList.length;
let lenPositions = 0;
let lenNormals = 0;
let lenColors = 0;
let lenUVs = 0;
let lenIndices = 0;
let lenEdgeIndices = 0;
let lenMatrices = 0;
let lenTextures = 0;
for (let geometryIndex = 0; geometryIndex < numGeometries; geometryIndex++) {
const geometry = geometriesList [geometryIndex];
if (geometry.positionsQuantized) {
lenPositions += geometry.positionsQuantized.length;
}
if (geometry.normalsOctEncoded) {
lenNormals += geometry.normalsOctEncoded.length;
}
if (geometry.colorsCompressed) {
lenColors += geometry.colorsCompressed.length;
}
if (geometry.uvs) {
lenUVs += geometry.uvs.length;
}
if (geometry.indices) {
lenIndices += geometry.indices.length;
}
if (geometry.edgeIndices) {
lenEdgeIndices += geometry.edgeIndices.length;
}
}
for (let textureIndex = 0; textureIndex < numTextures; textureIndex++) {
const xktTexture = texturesList[textureIndex];
const imageData = xktTexture.imageData;
lenTextures += imageData.byteLength;
if (xktTexture.compressed) {
stats.numCompressedTextures++;
}
}
for (let meshIndex = 0; meshIndex < numMeshes; meshIndex++) {
const mesh = meshesList[meshIndex];
if (mesh.geometry.numInstances > 1) {
lenMatrices += 16;
}
}
const data = {
metadata: {},
textureData: new Uint8Array(lenTextures), // All textures
eachTextureDataPortion: new Uint32Array(numTextures), // For each texture, an index to its first element in textureData
eachTextureAttributes: new Uint16Array(numTextures * NUM_TEXTURE_ATTRIBUTES),
positions: new Uint16Array(lenPositions), // All geometry arrays
normals: new Int8Array(lenNormals),
colors: new Uint8Array(lenColors),
uvs: new Float32Array(lenUVs),
indices: new Uint32Array(lenIndices),
edgeIndices: new Uint32Array(lenEdgeIndices),
eachTextureSetTextures: new Int32Array(numTextureSets * 5), // For each texture set, a set of five Texture indices [color, metal/roughness,normals,emissive,occlusion]; each index has value -1 if no texture
matrices: new Float32Array(lenMatrices), // Modeling matrices for entities that share geometries. Each entity either shares all it's geometries, or owns all its geometries exclusively. Exclusively-owned geometries are pre-transformed into World-space, and so their entities don't have modeling matrices in this array.
reusedGeometriesDecodeMatrix: new Float32Array(xktModel.reusedGeometriesDecodeMatrix), // A single, global vertex position de-quantization matrix for all reused geometries. Reused geometries are quantized to their collective Local-space AABB, and this matrix is derived from that AABB.
eachGeometryPrimitiveType: new Uint8Array(numGeometries), // Primitive type for each geometry (0=solid triangles, 1=surface triangles, 2=lines, 3=points, 4=line-strip)
eachGeometryPositionsPortion: new Uint32Array(numGeometries), // For each geometry, an index to its first element in data.positions. Every primitive type has positions.
eachGeometryNormalsPortion: new Uint32Array(numGeometries), // For each geometry, an index to its first element in data.normals. If the next geometry has the same index, then this geometry has no normals.
eachGeometryColorsPortion: new Uint32Array(numGeometries), // For each geometry, an index to its first element in data.colors. If the next geometry has the same index, then this geometry has no colors.
eachGeometryUVsPortion: new Uint32Array(numGeometries), // For each geometry, an index to its first element in data.uvs. If the next geometry has the same index, then this geometry has no UVs.
eachGeometryIndicesPortion: new Uint32Array(numGeometries), // For each geometry, an index to its first element in data.indices. If the next geometry has the same index, then this geometry has no indices.
eachGeometryEdgeIndicesPortion: new Uint32Array(numGeometries), // For each geometry, an index to its first element in data.edgeIndices. If the next geometry has the same index, then this geometry has no edge indices.
eachMeshGeometriesPortion: new Uint32Array(numMeshes), // For each mesh, an index into the eachGeometry* arrays
eachMeshMatricesPortion: new Uint32Array(numMeshes), // For each mesh that shares its geometry, an index to its first element in data.matrices, to indicate the modeling matrix that transforms the shared geometry Local-space vertex positions. This is ignored for meshes that don't share geometries, because the vertex positions of non-shared geometries are pre-transformed into World-space.
eachMeshTextureSet: new Int32Array(numMeshes), // For each mesh, the index of its texture set in data.eachTextureSetTextures; this array contains signed integers so that we can use -1 to indicate when a mesh has no texture set
eachMeshMaterialAttributes: new Uint8Array(numMeshes * NUM_MATERIAL_ATTRIBUTES), // For each mesh, an RGBA integer color of format [0..255, 0..255, 0..255, 0..255], and PBR metallic and roughness factors, of format [0..255, 0..255]
eachEntityId: [], // For each entity, an ID string
eachEntityMeshesPortion: new Uint32Array(numEntities), // For each entity, the index of the first element of meshes used by the entity
eachTileAABB: new Float64Array(numTiles * 6), // For each tile, an axis-aligned bounding box
eachTileEntitiesPortion: new Uint32Array(numTiles) // For each tile, the index of the first element of eachEntityId, eachEntityMeshesPortion and eachEntityMatricesPortion used by the tile
};
let countPositions = 0;
let countNormals = 0;
let countColors = 0;
let countUVs = 0;
let countIndices = 0;
let countEdgeIndices = 0;
// Metadata
data.metadata = {
id: xktModel.modelId,
projectId: xktModel.projectId,
revisionId: xktModel.revisionId,
author: xktModel.author,
createdAt: xktModel.createdAt,
creatingApplication: xktModel.creatingApplication,
schema: xktModel.schema,
propertySets: [],
metaObjects: []
};
// Property sets
for (let propertySetsIndex = 0; propertySetsIndex < numPropertySets; propertySetsIndex++) {
const propertySet = propertySetsList[propertySetsIndex];
const propertySetJSON = {
id: "" + propertySet.propertySetId,
name: propertySet.propertySetName,
type: propertySet.propertySetType,
properties: propertySet.properties
};
data.metadata.propertySets.push(propertySetJSON);
}
// Metaobjects
if (!metaModelDataStr) {
for (let metaObjectsIndex = 0; metaObjectsIndex < numMetaObjects; metaObjectsIndex++) {
const metaObject = metaObjectsList[metaObjectsIndex];
const metaObjectJSON = {
name: metaObject.metaObjectName,
type: metaObject.metaObjectType,
id: "" + metaObject.metaObjectId
};
if (metaObject.parentMetaObjectId !== undefined && metaObject.parentMetaObjectId !== null) {
metaObjectJSON.parent = "" + metaObject.parentMetaObjectId;
}
if (metaObject.propertySetIds && metaObject.propertySetIds.length > 0) {
metaObjectJSON.propertySetIds = metaObject.propertySetIds;
}
if (metaObject.external) {
metaObjectJSON.external = metaObject.external;
}
data.metadata.metaObjects.push(metaObjectJSON);
}
}
// Geometries
for (let geometryIndex = 0; geometryIndex < numGeometries; geometryIndex++) {
const geometry = geometriesList [geometryIndex];
let primitiveType = 1;
switch (geometry.primitiveType) {
case "triangles":
primitiveType = geometry.solid ? 0 : 1;
break;
case "points":
primitiveType = 2;
break;
case "lines":
primitiveType = 3;
break;
case "line-strip":
case "line-loop":
primitiveType = 4;
break;
case "triangle-strip":
primitiveType = 5;
break;
case "triangle-fan":
primitiveType = 6;
break;
default:
primitiveType = 1
}
data.eachGeometryPrimitiveType [geometryIndex] = primitiveType;
data.eachGeometryPositionsPortion [geometryIndex] = countPositions;
data.eachGeometryNormalsPortion [geometryIndex] = countNormals;
data.eachGeometryColorsPortion [geometryIndex] = countColors;
data.eachGeometryUVsPortion [geometryIndex] = countUVs;
data.eachGeometryIndicesPortion [geometryIndex] = countIndices;
data.eachGeometryEdgeIndicesPortion [geometryIndex] = countEdgeIndices;
if (geometry.positionsQuantized) {
data.positions.set(geometry.positionsQuantized, countPositions);
countPositions += geometry.positionsQuantized.length;
}
if (geometry.normalsOctEncoded) {
data.normals.set(geometry.normalsOctEncoded, countNormals);
countNormals += geometry.normalsOctEncoded.length;
}
if (geometry.colorsCompressed) {
data.colors.set(geometry.colorsCompressed, countColors);
countColors += geometry.colorsCompressed.length;
}
if (geometry.uvs) {
data.uvs.set(geometry.uvs, countUVs);
countUVs += geometry.uvs.length;
}
if (geometry.indices) {
data.indices.set(geometry.indices, countIndices);
countIndices += geometry.indices.length;
}
if (geometry.edgeIndices) {
data.edgeIndices.set(geometry.edgeIndices, countEdgeIndices);
countEdgeIndices += geometry.edgeIndices.length;
}
}
// Textures
for (let textureIndex = 0, numTextures = xktModel.texturesList.length, portionIdx = 0; textureIndex < numTextures; textureIndex++) {
const xktTexture = xktModel.texturesList[textureIndex];
const imageData = xktTexture.imageData;
data.textureData.set(imageData, portionIdx);
data.eachTextureDataPortion[textureIndex] = portionIdx;
portionIdx += imageData.byteLength;
let textureAttrIdx = textureIndex * NUM_TEXTURE_ATTRIBUTES;
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.compressed ? 1 : 0;
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.mediaType; // GIFMediaType | PNGMediaType | JPEGMediaType
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.width;
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.height;
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.minFilter; // LinearMipmapLinearFilter | LinearMipMapNearestFilter | NearestMipMapNearestFilter | NearestMipMapLinearFilter | LinearMipMapLinearFilter
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.magFilter; // LinearFilter | NearestFilter
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.wrapS; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.wrapT; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
data.eachTextureAttributes[textureAttrIdx++] = xktTexture.wrapR; // ClampToEdgeWrapping | MirroredRepeatWrapping | RepeatWrapping
}
// Texture sets
for (let textureSetIndex = 0, numTextureSets = xktModel.textureSetsList.length, eachTextureSetTexturesIndex = 0; textureSetIndex < numTextureSets; textureSetIndex++) {
const textureSet = textureSetsList[textureSetIndex];
data.eachTextureSetTextures[eachTextureSetTexturesIndex++] = textureSet.colorTexture ? textureSet.colorTexture.textureIndex : -1; // Color map
data.eachTextureSetTextures[eachTextureSetTexturesIndex++] = textureSet.metallicRoughnessTexture ? textureSet.metallicRoughnessTexture.textureIndex : -1; // Metal/rough map
data.eachTextureSetTextures[eachTextureSetTexturesIndex++] = textureSet.normalsTexture ? textureSet.normalsTexture.textureIndex : -1; // Normal map
data.eachTextureSetTextures[eachTextureSetTexturesIndex++] = textureSet.emissiveTexture ? textureSet.emissiveTexture.textureIndex : -1; // Emissive map
data.eachTextureSetTextures[eachTextureSetTexturesIndex++] = textureSet.occlusionTexture ? textureSet.occlusionTexture.textureIndex : -1; // Occlusion map
}
// Tiles -> Entities -> Meshes
let entityIndex = 0;
let countEntityMeshesPortion = 0;
let eachMeshMaterialAttributesIndex = 0;
let matricesIndex = 0;
let meshIndex = 0;
for (let tileIndex = 0; tileIndex < numTiles; tileIndex++) {
const tile = tilesList [tileIndex];
const tileEntities = tile.entities;
const numTileEntities = tileEntities.length;
if (numTileEntities === 0) {
continue;
}
data.eachTileEntitiesPortion[tileIndex] = entityIndex;
const tileAABB = tile.aabb;
for (let j = 0; j < numTileEntities; j++) {
const entity = tileEntities[j];
const entityMeshes = entity.meshes;
const numEntityMeshes = entityMeshes.length;
for (let k = 0; k < numEntityMeshes; k++) {
const mesh = entityMeshes[k];
const geometry = mesh.geometry;
const geometryIndex = geometry.geometryIndex;
data.eachMeshGeometriesPortion [countEntityMeshesPortion + k] = geometryIndex;
if (mesh.geometry.numInstances > 1) {
data.matrices.set(mesh.matrix, matricesIndex);
data.eachMeshMatricesPortion [meshIndex] = matricesIndex;
matricesIndex += 16;
}
data.eachMeshTextureSet[meshIndex] = mesh.textureSet ? mesh.textureSet.textureSetIndex : -1;
data.eachMeshMaterialAttributes[eachMeshMaterialAttributesIndex++] = (mesh.color[0] * 255); // Color RGB
data.eachMeshMaterialAttributes[eachMeshMaterialAttributesIndex++] = (mesh.color[1] * 255);
data.eachMeshMaterialAttributes[eachMeshMaterialAttributesIndex++] = (mesh.color[2] * 255);
data.eachMeshMaterialAttributes[eachMeshMaterialAttributesIndex++] = (mesh.opacity * 255); // Opacity
data.eachMeshMaterialAttributes[eachMeshMaterialAttributesIndex++] = (mesh.metallic * 255); // Metallic
data.eachMeshMaterialAttributes[eachMeshMaterialAttributesIndex++] = (mesh.roughness * 255); // Roughness
meshIndex++;
}
data.eachEntityId [entityIndex] = entity.entityId;
data.eachEntityMeshesPortion[entityIndex] = countEntityMeshesPortion; // <<<<<<<<<<<<<<<<<<<< Error here? Order/value of countEntityMeshesPortion correct?
entityIndex++;
countEntityMeshesPortion += numEntityMeshes;
}
const tileAABBIndex = tileIndex * 6;
data.eachTileAABB.set(tileAABB, tileAABBIndex);
}
return data;
}
function deflateData(data, metaModelJSON, options) {
function deflate(buffer) {
return (options.zip !== false) ? pako.deflate(buffer) : buffer;
}
let metaModelBytes;
if (metaModelJSON) {
const deflatedJSON = deflateJSON(metaModelJSON);
metaModelBytes = deflate(deflatedJSON)
} else {
const deflatedJSON = deflateJSON(data.metadata);
metaModelBytes = deflate(deflatedJSON)
}
return {
metadata: metaModelBytes,
textureData: deflate(data.textureData.buffer),
eachTextureDataPortion: deflate(data.eachTextureDataPortion.buffer),
eachTextureAttributes: deflate(data.eachTextureAttributes.buffer),
positions: deflate(data.positions.buffer),
normals: deflate(data.normals.buffer),
colors: deflate(data.colors.buffer),
uvs: deflate(data.uvs.buffer),
indices: deflate(data.indices.buffer),
edgeIndices: deflate(data.edgeIndices.buffer),
eachTextureSetTextures: deflate(data.eachTextureSetTextures.buffer),
matrices: deflate(data.matrices.buffer),
reusedGeometriesDecodeMatrix: deflate(data.reusedGeometriesDecodeMatrix.buffer),
eachGeometryPrimitiveType: deflate(data.eachGeometryPrimitiveType.buffer),
eachGeometryPositionsPortion: deflate(data.eachGeometryPositionsPortion.buffer),
eachGeometryNormalsPortion: deflate(data.eachGeometryNormalsPortion.buffer),
eachGeometryColorsPortion: deflate(data.eachGeometryColorsPortion.buffer),
eachGeometryUVsPortion: deflate(data.eachGeometryUVsPortion.buffer),
eachGeometryIndicesPortion: deflate(data.eachGeometryIndicesPortion.buffer),
eachGeometryEdgeIndicesPortion: deflate(data.eachGeometryEdgeIndicesPortion.buffer),
eachMeshGeometriesPortion: deflate(data.eachMeshGeometriesPortion.buffer),
eachMeshMatricesPortion: deflate(data.eachMeshMatricesPortion.buffer),
eachMeshTextureSet: deflate(data.eachMeshTextureSet.buffer),
eachMeshMaterialAttributes: deflate(data.eachMeshMaterialAttributes.buffer),
eachEntityId: deflate(JSON.stringify(data.eachEntityId)
.replace(/[\u007F-\uFFFF]/g, function (chr) { // Produce only ASCII-chars, so that the data can be inflated later
return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4)
})),
eachEntityMeshesPortion: deflate(data.eachEntityMeshesPortion.buffer),
eachTileAABB: deflate(data.eachTileAABB.buffer),
eachTileEntitiesPortion: deflate(data.eachTileEntitiesPortion.buffer)
};
}
function deflateJSON(strings) {
return JSON.stringify(strings)
.replace(/[\u007F-\uFFFF]/g, function (chr) { // Produce only ASCII-chars, so that the data can be inflated later
return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4)
});
}
function createArrayBuffer(deflatedData) {
return toArrayBuffer([
deflatedData.metadata,
deflatedData.textureData,
deflatedData.eachTextureDataPortion,
deflatedData.eachTextureAttributes,
deflatedData.positions,
deflatedData.normals,
deflatedData.colors,
deflatedData.uvs,
deflatedData.indices,
deflatedData.edgeIndices,
deflatedData.eachTextureSetTextures,
deflatedData.matrices,
deflatedData.reusedGeometriesDecodeMatrix,
deflatedData.eachGeometryPrimitiveType,
deflatedData.eachGeometryPositionsPortion,
deflatedData.eachGeometryNormalsPortion,
deflatedData.eachGeometryColorsPortion,
deflatedData.eachGeometryUVsPortion,
deflatedData.eachGeometryIndicesPortion,
deflatedData.eachGeometryEdgeIndicesPortion,
deflatedData.eachMeshGeometriesPortion,
deflatedData.eachMeshMatricesPortion,
deflatedData.eachMeshTextureSet,
deflatedData.eachMeshMaterialAttributes,
deflatedData.eachEntityId,
deflatedData.eachEntityMeshesPortion,
deflatedData.eachTileAABB,
deflatedData.eachTileEntitiesPortion
]);
}
function toArrayBuffer(elements) {
const indexData = new Uint32Array(elements.length + 2);
indexData[0] = 10; // XKT_VERSION for legacy v10 mode
indexData [1] = elements.length; // Stored Data 1.1: number of stored elements
let dataLen = 0; // Stored Data 1.2: length of stored elements
for (let i = 0, len = elements.length; i < len; i++) {
const element = elements[i];
const elementsize = element.length;
indexData[i + 2] = elementsize;
dataLen += elementsize;
}
const indexBuf = new Uint8Array(indexData.buffer);
const dataArray = new Uint8Array(indexBuf.length + dataLen);
dataArray.set(indexBuf);
let offset = indexBuf.length;
for (let i = 0, len = elements.length; i < len; i++) { // Stored Data 2: the elements themselves
const element = elements[i];
dataArray.set(element, offset);
offset += element.length;
}
return dataArray.buffer;
}
export {writeXKTModelToArrayBuffer};