Reference Source

src/XKTModel/XKTModel.js

import {math} from "../lib/math.js";
import {geometryCompression} from "./lib/geometryCompression.js";
import {buildEdgeIndices} from "./lib/buildEdgeIndices.js";
import {isTriangleMeshSolid} from "./lib/isTriangleMeshSolid.js";

import {XKTMesh} from './XKTMesh.js';
import {XKTGeometry} from './XKTGeometry.js';
import {XKTEntity} from './XKTEntity.js';
import {XKTTile} from './XKTTile.js';
import {KDNode} from "./KDNode.js";
import {XKTMetaObject} from "./XKTMetaObject.js";
import {XKTPropertySet} from "./XKTPropertySet.js";
import {mergeVertices} from "../lib/mergeVertices.js";

const tempVec4a = math.vec4([0, 0, 0, 1]);
const tempVec4b = math.vec4([0, 0, 0, 1]);

const tempMat4 = math.mat4();
const tempMat4b = math.mat4();

const MIN_TILE_DIAG = 10000;

const kdTreeDimLength = new Float64Array(3);

/**
 * A document model that represents the contents of an .XKT file.
 *
 * * An XKTModel contains {@link XKTTile}s, which spatially subdivide the model into axis-aligned, box-shaped regions.
 * * Each {@link XKTTile} contains {@link XKTEntity}s, which represent the objects within its region.
 * * Each {@link XKTEntity} has {@link XKTMesh}s, which each have a {@link XKTGeometry}. Each {@link XKTGeometry} can be shared by multiple {@link XKTMesh}s.
 * * Import models into an XKTModel using {@link parseGLTFIntoXKTModel}, {@link parseIFCIntoXKTModel}, {@link parse3DXMLIntoXKTModel}, {@link parseCityJSONIntoXKTModel} etc.
 * * Build an XKTModel programmatically using {@link XKTModel#createGeometry}, {@link XKTModel#createMesh} and {@link XKTModel#createEntity}.
 * * Serialize an XKTModel to an ArrayBuffer using {@link writeXKTModelToArrayBuffer}.
 *
 * ## Usage
 *
 * See [main docs page](/docs/#javascript-api) for usage examples.
 *
 * @class XKTModel
 */
class XKTModel {

    /**
     * Constructs a new XKTModel.
     *
     * @param {*} [cfg] Configuration
     * @param {Number} [cfg.edgeThreshold=10]
     */
    constructor(cfg = {}) {

        /**
         * The model's ID, if available.
         *
         * Will be "default" by default.
         *
         * @type {String}
         */
        this.modelId = cfg.modelId || "default";

        /**
         * The project ID, if available.
         *
         * Will be an empty string by default.
         *
         * @type {String}
         */
        this.projectId = cfg.projectId || "";

        /**
         * The revision ID, if available.
         *
         * Will be an empty string by default.
         *
         * @type {String}
         */
        this.revisionId = cfg.revisionId || "";

        /**
         * The model author, if available.
         *
         * Will be an empty string by default.
         *
         * @property author
         * @type {String}
         */
        this.author = cfg.author || "";

        /**
         * The date the model was created, if available.
         *
         * Will be an empty string by default.
         *
         * @property createdAt
         * @type {String}
         */
        this.createdAt = cfg.createdAt || "";

        /**
         * The application that created the model, if available.
         *
         * Will be an empty string by default.
         *
         * @property creatingApplication
         * @type {String}
         */
        this.creatingApplication = cfg.creatingApplication || "";

        /**
         * The model schema version, if available.
         *
         * In the case of IFC, this could be "IFC2x3" or "IFC4", for example.
         *
         * Will be an empty string by default.
         *
         * @property schema
         * @type {String}
         */
        this.schema = cfg.schema || "";

        /**
         *
         * @type {Number|number}
         */
        this.edgeThreshold = cfg.edgeThreshold || 10;

        /**
         * Map of {@link XKTPropertySet}s within this XKTModel, each mapped to {@link XKTPropertySet#propertySetId}.
         *
         * Created by {@link XKTModel#createPropertySet}.
         *
         * @type {{String:XKTPropertySet}}
         */
        this.propertySets = {};

        /**
         * {@link XKTPropertySet}s within this XKTModel.
         *
         * Each XKTPropertySet holds its position in this list in {@link XKTPropertySet#propertySetIndex}.
         *
         * Created by {@link XKTModel#finalize}.
         *
         * @type {XKTPropertySet[]}
         */
        this.propertySetsList = [];

        /**
         * Map of {@link XKTMetaObject}s within this XKTModel, each mapped to {@link XKTMetaObject#metaObjectId}.
         *
         * Created by {@link XKTModel#createMetaObject}.
         *
         * @type {{String:XKTMetaObject}}
         */
        this.metaObjects = {};

        /**
         * {@link XKTMetaObject}s within this XKTModel.
         *
         * Each XKTMetaObject holds its position in this list in {@link XKTMetaObject#metaObjectIndex}.
         *
         * Created by {@link XKTModel#finalize}.
         *
         * @type {XKTMetaObject[]}
         */
        this.metaObjectsList = [];

        /**
         * The positions of all shared {@link XKTGeometry}s are de-quantized using this singular
         * de-quantization matrix.
         *
         * This de-quantization matrix is generated from the collective Local-space boundary of the
         * positions of all shared {@link XKTGeometry}s.
         *
         * @type {Float32Array}
         */
        this.reusedGeometriesDecodeMatrix = new Float32Array(16);

        /**
         * Map of {@link XKTGeometry}s within this XKTModel, each mapped to {@link XKTGeometry#geometryId}.
         *
         * Created by {@link XKTModel#createGeometry}.
         *
         * @type {{Number:XKTGeometry}}
         */
        this.geometries = {};

        /**
         * List of {@link XKTGeometry}s within this XKTModel, in the order they were created.
         *
         * Each XKTGeometry holds its position in this list in {@link XKTGeometry#geometryIndex}.
         *
         * Created by {@link XKTModel#finalize}.
         *
         * @type {XKTGeometry[]}
         */
        this.geometriesList = [];

        /**
         * Map of {@link XKTMesh}s within this XKTModel, each mapped to {@link XKTMesh#meshId}.
         *
         * Created by {@link XKTModel#createMesh}.
         *
         * @type {{Number:XKTMesh}}
         */
        this.meshes = {};

        /**
         * List of {@link XKTMesh}s within this XKTModel, in the order they were created.
         *
         * Each XKTMesh holds its position in this list in {@link XKTMesh#meshIndex}.
         *
         * Created by {@link XKTModel#finalize}.
         *
         * @type {XKTMesh[]}
         */
        this.meshesList = [];

        /**
         * Map of {@link XKTEntity}s within this XKTModel, each mapped to {@link XKTEntity#entityId}.
         *
         * Created by {@link XKTModel#createEntity}.
         *
         * @type {{String:XKTEntity}}
         */
        this.entities = {};

        /**
         * {@link XKTEntity}s within this XKTModel.
         *
         * Each XKTEntity holds its position in this list in {@link XKTEntity#entityIndex}.
         *
         * Created by {@link XKTModel#finalize}.
         *
         * @type {XKTEntity[]}
         */
        this.entitiesList = [];

        /**
         * {@link XKTTile}s within this XKTModel.
         *
         * Created by {@link XKTModel#finalize}.
         *
         * @type {XKTTile[]}
         */
        this.tilesList = [];

        /**
         * The axis-aligned 3D World-space boundary of this XKTModel.
         *
         * Created by {@link XKTModel#finalize}.
         *
         * @type {Float64Array}
         */
        this.aabb = math.AABB3();

        /**
         * Indicates if this XKTModel has been finalized.
         *
         * Set ````true```` by {@link XKTModel#finalize}.
         *
         * @type {boolean}
         */
        this.finalized = false;
    }

    /**
     * Creates an {@link XKTPropertySet} within this XKTModel.
     *
     * Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
     *
     * @param {*} params Method parameters.
     * @param {String} params.propertySetId Unique ID for the {@link XKTPropertySet}.
     * @param {String} [params.propertySetType="default"] A meta type for the {@link XKTPropertySet}.
     * @param {String} [params.propertySetName] Human-readable name for the {@link XKTPropertySet}. Defaults to the ````propertySetId```` parameter.
     * @param {String[]} params.properties Properties for the {@link XKTPropertySet}.
     * @returns {XKTPropertySet} The new {@link XKTPropertySet}.
     */
    createPropertySet(params) {

        if (!params) {
            throw "Parameters expected: params";
        }

        if (params.propertySetId === null || params.propertySetId === undefined) {
            throw "Parameter expected: params.propertySetId";
        }

        if (params.properties === null || params.properties === undefined) {
            throw "Parameter expected: params.properties";
        }

        if (this.finalized) {
            console.error("XKTModel has been finalized, can't add more property sets");
            return;
        }

        if (this.propertySets[params.propertySetId]) {
            //          console.error("XKTPropertySet already exists with this ID: " + params.propertySetId);
            return;
        }

        const propertySetId = params.propertySetId;
        const propertySetType = params.propertySetType || "Default";
        const propertySetName = params.propertySetName || params.propertySetId;
        const properties = params.properties || [];

        const propertySet = new XKTPropertySet(propertySetId, propertySetType, propertySetName, properties);

        this.propertySets[propertySetId] = propertySet;
        this.propertySetsList.push(propertySet);

        return propertySet;
    }

    /**
     * Creates an {@link XKTMetaObject} within this XKTModel.
     *
     * Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
     *
     * @param {*} params Method parameters.
     * @param {String} params.metaObjectId Unique ID for the {@link XKTMetaObject}.
     * @param {String} params.propertySetIds ID of one or more property sets that contains additional metadata about
     * this {@link XKTMetaObject}. The property sets could be stored externally (ie not managed at all by the XKT file),
     * or could be {@link XKTPropertySet}s within {@link XKTModel#propertySets}.
     * @param {String} [params.metaObjectType="default"] A meta type for the {@link XKTMetaObject}. Can be anything,
     * but is usually an IFC type, such as "IfcSite" or "IfcWall".
     * @param {String} [params.metaObjectName] Human-readable name for the {@link XKTMetaObject}. Defaults to the ````metaObjectId```` parameter.
     * @param {String} [params.parentMetaObjectId] ID of the parent {@link XKTMetaObject}, if any. Defaults to the ````metaObjectId```` parameter.
     * @returns {XKTMetaObject} The new {@link XKTMetaObject}.
     */
    createMetaObject(params) {

        if (!params) {
            throw "Parameters expected: params";
        }

        if (params.metaObjectId === null || params.metaObjectId === undefined) {
            throw "Parameter expected: params.metaObjectId";
        }

        if (this.finalized) {
            console.error("XKTModel has been finalized, can't add more meta objects");
            return;
        }

        if (this.metaObjects[params.metaObjectId]) {
            //          console.error("XKTMetaObject already exists with this ID: " + params.metaObjectId);
            return;
        }

        const metaObjectId = params.metaObjectId;
        const propertySetIds = params.propertySetIds;
        const metaObjectType = params.metaObjectType || "Default";
        const metaObjectName = params.metaObjectName || params.metaObjectId;
        const parentMetaObjectId = params.parentMetaObjectId;

        const metaObject = new XKTMetaObject(metaObjectId, propertySetIds, metaObjectType, metaObjectName, parentMetaObjectId);

        this.metaObjects[metaObjectId] = metaObject;
        this.metaObjectsList.push(metaObject);

        if (!parentMetaObjectId) {
            if (!this._rootMetaObject) {
                this._rootMetaObject = metaObject;
            }
        }

        return metaObject;
    }

    /**
     * Creates an {@link XKTGeometry} within this XKTModel.
     *
     * Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
     *
     * @param {*} params Method parameters.
     * @param {Number} params.geometryId Unique ID for the {@link XKTGeometry}.
     * @param {String} params.primitiveType The type of {@link XKTGeometry}: "triangles", "lines" or "points".
     * @param {Float64Array} params.positions Floating-point Local-space vertex positions for the {@link XKTGeometry}. Required for all primitive types.
     * @param {Number[]} [params.normals] Floating-point vertex normals for the {@link XKTGeometry}. Only used with triangles primitives. Ignored for points and lines.
     * @param {Number[]} [params.colors] Floating-point RGBA vertex colors for the {@link XKTGeometry}. Required for points primitives. Ignored for lines and triangles.
     * @param {Number[]} [params.colorsCompressed] Integer RGBA vertex colors for the {@link XKTGeometry}. Required for points primitives. Ignored for lines and triangles.
     * @param {Uint32Array} [params.indices] Indices for the {@link XKTGeometry}. Required for triangles and lines primitives. Ignored for points.
     * @param {Number} [params.edgeThreshold=10]
     * @returns {XKTGeometry} The new {@link XKTGeometry}.
     */
    createGeometry(params) {

        if (!params) {
            throw "Parameters expected: params";
        }

        if (params.geometryId === null || params.geometryId === undefined) {
            throw "Parameter expected: params.geometryId";
        }

        if (!params.primitiveType) {
            throw "Parameter expected: params.primitiveType";
        }

        if (!params.positions) {
            throw "Parameter expected: params.positions";
        }

        const triangles = params.primitiveType === "triangles";
        const points = params.primitiveType === "points";
        const lines = params.primitiveType === "lines";

        if (!triangles && !points && !lines) {
            throw "Unsupported value for params.primitiveType: " + params.primitiveType + "' - supported values are 'triangles', 'points' and 'lines'";
        }

        if (triangles) {
            if (!params.indices) {
                throw "Parameter expected for 'triangles' primitive: params.indices";
            }
        }

        if (points) {
            if (!params.colors && !params.colorsCompressed) {
                throw "Parameter expected for 'points' primitive: params.colors or params.colorsCompressed";
            }
        }

        if (lines) {
            if (!params.indices) {
                throw "Parameter expected for 'lines' primitive: params.indices";
            }
        }

        if (this.finalized) {
            console.error("XKTModel has been finalized, can't add more geometries");
            return;
        }

        if (this.geometries[params.geometryId]) {
            console.error("XKTGeometry already exists with this ID: " + params.geometryId);
            return;
        }

        const geometryId = params.geometryId;
        const primitiveType = params.primitiveType;
        const positions = new Float64Array(params.positions); // May modify in #finalize

        const xktGeometryCfg = {
            geometryId: geometryId,
            geometryIndex: this.geometriesList.length,
            primitiveType: primitiveType,
            positions: positions
        }

        if (triangles) {
            if (params.normals) {
                xktGeometryCfg.normals = new Float32Array(params.normals);
            }
            xktGeometryCfg.indices = params.indices;
        }

        if (points) {
            if (params.colorsCompressed) {
                xktGeometryCfg.colorsCompressed = new Uint8Array(params.colorsCompressed);

            } else {
                const colors = params.colors;
                const colorsCompressed = new Uint8Array(colors.length);
                for (let i = 0, len = colors.length; i < len; i++) {
                    colorsCompressed[i] = Math.floor(colors[i] * 255);
                }
                xktGeometryCfg.colorsCompressed = colorsCompressed;
            }
        }

        if (lines) {
            xktGeometryCfg.indices = params.indices;
        }

        if (triangles) {

            if (!params.normals) {

                // Building models often duplicate positions to allow face-aligned vertex normals; when we're not
                // providing normals for a geometry, it becomes possible to merge duplicate vertex positions within it.

                // TODO: Make vertex merging also merge normals?

                const mergedPositions = [];
                const mergedIndices = [];
                mergeVertices(xktGeometryCfg.positions, xktGeometryCfg.indices, mergedPositions, mergedIndices);
                xktGeometryCfg.positions = new Float64Array(mergedPositions);
                xktGeometryCfg.indices = mergedIndices;
            }

            xktGeometryCfg.edgeIndices = buildEdgeIndices(xktGeometryCfg.positions, xktGeometryCfg.indices, null, params.edgeThreshold || this.edgeThreshold || 10);
        }

        const geometry = new XKTGeometry(xktGeometryCfg);

        this.geometries[geometryId] = geometry;
        this.geometriesList.push(geometry);

        return geometry;
    }

    /**
     * Creates an {@link XKTMesh} within this XKTModel.
     *
     * An {@link XKTMesh} can be owned by one {@link XKTEntity}, which can own multiple {@link XKTMesh}es.
     *
     * @param {*} params Method parameters.
     * @param {Number} params.meshId Unique ID for the {@link XKTMesh}.
     * @param {Number} params.geometryId ID of an existing {@link XKTGeometry} in {@link XKTModel#geometries}.
     * @param {Uint8Array} params.color RGB color for the {@link XKTMesh}, with each color component in range [0..1].
     * @param {Number} [params.metallic=0] How metallic the {@link XKTMesh} is, in range [0..1]. A value of ````0```` indicates fully dielectric material, while ````1```` indicates fully metallic.
     * @param {Number} [params.roughness=1] How rough the {@link XKTMesh} is, in range [0..1]. A value of ````0```` indicates fully smooth, while ````1```` indicates fully rough.
     * @param {Number} params.opacity Opacity factor for the {@link XKTMesh}, in range [0..1].
     * @param {Float64Array} [params.matrix] Modeling matrix for the {@link XKTMesh}. Overrides ````position````, ````scale```` and ````rotation```` parameters.
     * @param {Number[]} [params.position=[0,0,0]] Position of the {@link XKTMesh}. Overridden by the ````matrix```` parameter.
     * @param {Number[]} [params.scale=[1,1,1]] Scale of the {@link XKTMesh}. Overridden by the ````matrix```` parameter.
     * @param {Number[]} [params.rotation=[0,0,0]] Rotation of the {@link XKTMesh} as Euler angles given in degrees, for each of the X, Y and Z axis. Overridden by the ````matrix```` parameter.
     * @returns {XKTMesh} The new {@link XKTMesh}.
     */
    createMesh(params) {

        if (params.meshId === null || params.meshId === undefined) {
            throw "Parameter expected: params.meshId";
        }

        if (params.geometryId === null || params.geometryId === undefined) {
            throw "Parameter expected: params.geometryId";
        }

        if (this.finalized) {
            throw "XKTModel has been finalized, can't add more meshes";
        }

        if (this.meshes[params.meshId]) {
            console.error("XKTMesh already exists with this ID: " + params.meshId);
            return;
        }

        const geometry = this.geometries[params.geometryId];

        if (!geometry) {
            console.error("XKTGeometry not found: " + params.geometryId);
            return;
        }

        geometry.numInstances++;

        let matrix = params.matrix;

        if (!matrix) {

            const position = params.position;
            const scale = params.scale;
            const rotation = params.rotation;

            if (position || scale || rotation) {
                matrix = math.identityMat4();
                const quaternion = math.eulerToQuaternion(rotation || [0, 0, 0], "XYZ", math.identityQuaternion());
                math.composeMat4(position || [0, 0, 0], quaternion, scale || [1, 1, 1], matrix)

            } else {
                matrix = math.identityMat4();
            }
        }

        const meshIndex = this.meshesList.length;

        const mesh = new XKTMesh({
            meshId: params.meshId,
            meshIndex: meshIndex,
            matrix: matrix,
            geometry: geometry,
            color: params.color,
            metallic: params.metallic,
            roughness: params.roughness,
            opacity: params.opacity
        });

        this.meshes[mesh.meshId] = mesh;
        this.meshesList.push(mesh);

        return mesh;
    }

    /**
     * Creates an {@link XKTEntity} within this XKTModel.
     *
     * Logs error and does nothing if this XKTModel has been finalized (see {@link XKTModel#finalized}).
     *
     * @param {*} params Method parameters.
     * @param {String} params.entityId Unique ID for the {@link XKTEntity}.
     * @param {String[]} params.meshIds IDs of {@link XKTMesh}es used by the {@link XKTEntity}. Note that each {@link XKTMesh} can only be used by one {@link XKTEntity}.
     * @returns {XKTEntity} The new {@link XKTEntity}.
     */
    createEntity(params) {

        if (!params) {
            throw "Parameters expected: params";
        }

        if (params.entityId === null || params.entityId === undefined) {
            throw "Parameter expected: params.entityId";
        }

        if (!params.meshIds) {
            throw "Parameter expected: params.meshIds";
        }

        if (this.finalized) {
            console.error("XKTModel has been finalized, can't add more entities");
            return;
        }

        if (params.meshIds.length === 0) {
            console.warn("XKTEntity has no meshes - won't create: " + params.entityId);
            return;
        }

        if (this.entities[params.entityId]) {
            console.error("XKTEntity already exists with this ID: " + params.entityId);
            return;
        }

        const entityId = params.entityId;
        const meshIds = params.meshIds;
        const meshes = [];

        for (let meshIdIdx = 0, meshIdLen = meshIds.length; meshIdIdx < meshIdLen; meshIdIdx++) {

            const meshId = meshIds[meshIdIdx];
            const mesh = this.meshes[meshId];

            if (!mesh) {
                console.error("XKTMesh found: " + meshId);
                continue;
            }

            if (mesh.entity) {
                console.error("XKTMesh " + meshId + " already used by XKTEntity " + mesh.entity.entityId);
                continue;
            }

            meshes.push(mesh);
        }

        const entity = new XKTEntity(entityId, meshes);

        for (let i = 0, len = meshes.length; i < len; i++) {
            const mesh = meshes[i];
            mesh.entity = entity;
        }

        this.entities[entityId] = entity;
        this.entitiesList.push(entity);

        return entity;
    }

    /**
     * Creates a default {@link XKTMetaObject} for each {@link XKTEntity} that does not already have one.
     */
    createDefaultMetaObjects() {

        for (let i = 0, len = this.entitiesList.length; i < len; i++) {

            const entity = this.entitiesList[i];
            const metaObjectId = entity.entityId;
            const metaObject = this.metaObjects[metaObjectId];

            if (!metaObject) {

                if (!this._rootMetaObject) {
                    this._rootMetaObject = this.createMetaObject({
                        metaObjectId: this.modelId,
                        metaObjectType: "Default",
                        metaObjectName: this.modelId
                    });
                }

                this.createMetaObject({
                    metaObjectId: metaObjectId,
                    metaObjectType: "Default",
                    metaObjectName: "" + metaObjectId,
                    parentMetaObjectId: this._rootMetaObject.metaObjectId
                });
            }
        }
    }

    /**
     * Finalizes this XKTModel.
     *
     * After finalizing, we may then serialize the model to an array buffer using {@link writeXKTModelToArrayBuffer}.
     *
     * Logs error and does nothing if this XKTModel has already been finalized.
     *
     * Internally, this method:
     *
     * * for each {@link XKTEntity} that doesn't already have a {@link XKTMetaObject}, creates one with {@link XKTMetaObject#metaObjectType} set to "default"
     * * sets each {@link XKTEntity}'s {@link XKTEntity#hasReusedGeometries} true if it shares its {@link XKTGeometry}s with other {@link XKTEntity}s,
     * * creates each {@link XKTEntity}'s {@link XKTEntity#aabb},
     * * creates {@link XKTTile}s in {@link XKTModel#tilesList}, and
     * * sets {@link XKTModel#finalized} ````true````.
     */
    finalize() {

        if (this.finalized) {
            console.log("XKTModel already finalized");
            return;
        }

        this._bakeSingleUseGeometryPositions();

        this._bakeAndOctEncodeNormals();

        this._createEntityAABBs();

        const rootKDNode = this._createKDTree();

        this.entitiesList = [];

        this._createTilesFromKDTree(rootKDNode);

        this._createReusedGeometriesDecodeMatrix();

        this._flagSolidGeometries();

        this.aabb.set(rootKDNode.aabb);

        this.finalized = true;
    }

    _bakeSingleUseGeometryPositions() {

        for (let j = 0, lenj = this.meshesList.length; j < lenj; j++) {

            const mesh = this.meshesList[j];

            const geometry = mesh.geometry;

            if (geometry.numInstances === 1) {

                const matrix = mesh.matrix;

                if (matrix && (!math.isIdentityMat4(matrix))) {

                    const positions = geometry.positions;

                    for (let i = 0, len = positions.length; i < len; i += 3) {

                        tempVec4a[0] = positions[i + 0];
                        tempVec4a[1] = positions[i + 1];
                        tempVec4a[2] = positions[i + 2];
                        tempVec4a[3] = 1;

                        math.transformPoint4(matrix, tempVec4a, tempVec4b);

                        positions[i + 0] = tempVec4b[0];
                        positions[i + 1] = tempVec4b[1];
                        positions[i + 2] = tempVec4b[2];
                    }
                }
            }
        }
    }

    _bakeAndOctEncodeNormals() {

        for (let i = 0, len = this.meshesList.length; i < len; i++) {

            const mesh = this.meshesList[i];
            const geometry = mesh.geometry;

            if (geometry.normals && !geometry.normalsOctEncoded) {

                geometry.normalsOctEncoded = new Int8Array(geometry.normals.length);

                if (geometry.numInstances > 1) {
                    geometryCompression.octEncodeNormals(geometry.normals, geometry.normals.length, geometry.normalsOctEncoded, 0);

                } else {
                    const modelNormalMatrix = math.inverseMat4(math.transposeMat4(mesh.matrix, tempMat4), tempMat4b);
                    geometryCompression.transformAndOctEncodeNormals(modelNormalMatrix, geometry.normals, geometry.normals.length, geometry.normalsOctEncoded, 0);
                }
            }
        }
    }

    _createEntityAABBs() {

        for (let i = 0, len = this.entitiesList.length; i < len; i++) {

            const entity = this.entitiesList[i];
            const entityAABB = entity.aabb;
            const meshes = entity.meshes;

            math.collapseAABB3(entityAABB);

            for (let j = 0, lenj = meshes.length; j < lenj; j++) {

                const mesh = meshes[j];
                const geometry = mesh.geometry;
                const matrix = mesh.matrix;

                if (geometry.numInstances > 1) {

                    const positions = geometry.positions;
                    for (let i = 0, len = positions.length; i < len; i += 3) {
                        tempVec4a[0] = positions[i + 0];
                        tempVec4a[1] = positions[i + 1];
                        tempVec4a[2] = positions[i + 2];
                        tempVec4a[3] = 1;
                        math.transformPoint4(matrix, tempVec4a, tempVec4b);
                        math.expandAABB3Point3(entityAABB, tempVec4b);
                    }

                } else {

                    const positions = geometry.positions;
                    for (let i = 0, len = positions.length; i < len; i += 3) {
                        tempVec4a[0] = positions[i + 0];
                        tempVec4a[1] = positions[i + 1];
                        tempVec4a[2] = positions[i + 2];
                        math.expandAABB3Point3(entityAABB, tempVec4a);
                    }
                }
            }
        }
    }

    _createKDTree() {

        const aabb = math.collapseAABB3();

        for (let i = 0, len = this.entitiesList.length; i < len; i++) {
            const entity = this.entitiesList[i];
            math.expandAABB3(aabb, entity.aabb);
        }

        const rootKDNode = new KDNode(aabb);

        for (let i = 0, len = this.entitiesList.length; i < len; i++) {
            const entity = this.entitiesList[i];
            this._insertEntityIntoKDTree(rootKDNode, entity);
        }

        return rootKDNode;
    }

    _insertEntityIntoKDTree(kdNode, entity) {

        const nodeAABB = kdNode.aabb;
        const entityAABB = entity.aabb;

        const nodeAABBDiag = math.getAABB3Diag(nodeAABB);

        if (nodeAABBDiag < MIN_TILE_DIAG) {
            kdNode.entities = kdNode.entities || [];
            kdNode.entities.push(entity);
            math.expandAABB3(nodeAABB, entityAABB);
            return;
        }

        if (kdNode.left) {
            if (math.containsAABB3(kdNode.left.aabb, entityAABB)) {
                this._insertEntityIntoKDTree(kdNode.left, entity);
                return;
            }
        }

        if (kdNode.right) {
            if (math.containsAABB3(kdNode.right.aabb, entityAABB)) {
                this._insertEntityIntoKDTree(kdNode.right, entity);
                return;
            }
        }

        kdTreeDimLength[0] = nodeAABB[3] - nodeAABB[0];
        kdTreeDimLength[1] = nodeAABB[4] - nodeAABB[1];
        kdTreeDimLength[2] = nodeAABB[5] - nodeAABB[2];

        let dim = 0;

        if (kdTreeDimLength[1] > kdTreeDimLength[dim]) {
            dim = 1;
        }

        if (kdTreeDimLength[2] > kdTreeDimLength[dim]) {
            dim = 2;
        }

        if (!kdNode.left) {
            const aabbLeft = nodeAABB.slice();
            aabbLeft[dim + 3] = ((nodeAABB[dim] + nodeAABB[dim + 3]) / 2.0);
            kdNode.left = new KDNode(aabbLeft);
            if (math.containsAABB3(aabbLeft, entityAABB)) {
                this._insertEntityIntoKDTree(kdNode.left, entity);
                return;
            }
        }

        if (!kdNode.right) {
            const aabbRight = nodeAABB.slice();
            aabbRight[dim] = ((nodeAABB[dim] + nodeAABB[dim + 3]) / 2.0);
            kdNode.right = new KDNode(aabbRight);
            if (math.containsAABB3(aabbRight, entityAABB)) {
                this._insertEntityIntoKDTree(kdNode.right, entity);
                return;
            }
        }

        kdNode.entities = kdNode.entities || [];
        kdNode.entities.push(entity);

        math.expandAABB3(nodeAABB, entityAABB);
    }

    _createTilesFromKDTree(rootKDNode) {
        this._createTilesFromKDNode(rootKDNode);
    }

    _createTilesFromKDNode(kdNode) {
        if (kdNode.entities && kdNode.entities.length > 0) {
            this._createTileFromEntities(kdNode.entities);
        }
        if (kdNode.left) {
            this._createTilesFromKDNode(kdNode.left);
        }
        if (kdNode.right) {
            this._createTilesFromKDNode(kdNode.right);
        }
    }

    /**
     * Creates a tile from the given entities.
     *
     * For each single-use {@link XKTGeometry}, this method centers {@link XKTGeometry#positions} to make them relative to the
     * tile's center, then quantizes the positions to unsigned 16-bit integers, relative to the tile's boundary.
     *
     * @param entities
     */
    _createTileFromEntities(entities) {

        const tileAABB = math.AABB3(); // A tighter World-space AABB around the entities
        math.collapseAABB3(tileAABB);

        for (let i = 0; i < entities.length; i++) {
            const entity = entities [i];
            math.expandAABB3(tileAABB, entity.aabb);
        }

        const tileCenter = math.getAABB3Center(tileAABB);
        const tileCenterNeg = math.mulVec3Scalar(tileCenter, -1, math.vec3());

        const rtcAABB = math.AABB3(); // AABB centered at the RTC origin

        rtcAABB[0] = tileAABB[0] - tileCenter[0];
        rtcAABB[1] = tileAABB[1] - tileCenter[1];
        rtcAABB[2] = tileAABB[2] - tileCenter[2];
        rtcAABB[3] = tileAABB[3] - tileCenter[0];
        rtcAABB[4] = tileAABB[4] - tileCenter[1];
        rtcAABB[5] = tileAABB[5] - tileCenter[2];

        for (let i = 0; i < entities.length; i++) {

            const entity = entities [i];

            const meshes = entity.meshes;

            for (let j = 0, lenj = meshes.length; j < lenj; j++) {

                const mesh = meshes[j];
                const geometry = mesh.geometry;

                if (!geometry.reused) {

                    const positions = geometry.positions;

                    // Center positions relative to their tile's World-space center

                    for (let k = 0, lenk = positions.length; k < lenk; k += 3) {

                        positions[k + 0] -= tileCenter[0];
                        positions[k + 1] -= tileCenter[1];
                        positions[k + 2] -= tileCenter[2];
                    }

                    // Quantize positions relative to tile's RTC-space boundary

                    geometryCompression.quantizePositions(positions, positions.length, rtcAABB, geometry.positionsQuantized);

                } else {

                    // Post-multiply a translation to the mesh's modeling matrix
                    // to center the entity's geometry instances to the tile RTC center

                    math.translateMat4v(tileCenterNeg, mesh.matrix);
                }
            }

            entity.entityIndex = this.entitiesList.length;

            this.entitiesList.push(entity);
        }

        const tile = new XKTTile(tileAABB, entities);

        this.tilesList.push(tile);
    }

    _createReusedGeometriesDecodeMatrix() {

        const tempVec3a = math.vec3();
        const reusedGeometriesAABB = math.collapseAABB3(math.AABB3());
        let countReusedGeometries = 0;

        for (let geometryIndex = 0, numGeometries = this.geometriesList.length; geometryIndex < numGeometries; geometryIndex++) {

            const geometry = this.geometriesList [geometryIndex];

            if (geometry.reused) {

                const positions = geometry.positions;

                for (let i = 0, len = positions.length; i < len; i += 3) {

                    tempVec3a[0] = positions[i];
                    tempVec3a[1] = positions[i + 1];
                    tempVec3a[2] = positions[i + 2];

                    math.expandAABB3Point3(reusedGeometriesAABB, tempVec3a);
                }

                countReusedGeometries++;
            }
        }

        if (countReusedGeometries > 0) {

            geometryCompression.createPositionsDecodeMatrix(reusedGeometriesAABB, this.reusedGeometriesDecodeMatrix);

            for (let geometryIndex = 0, numGeometries = this.geometriesList.length; geometryIndex < numGeometries; geometryIndex++) {

                const geometry = this.geometriesList [geometryIndex];

                if (geometry.reused) {
                    geometryCompression.quantizePositions(geometry.positions, geometry.positions.length, reusedGeometriesAABB, geometry.positionsQuantized);
                }
            }

        } else {
            math.identityMat4(this.reusedGeometriesDecodeMatrix); // No need for this matrix, but we'll be tidy and set it to identity
        }
    }

    _flagSolidGeometries() {
        for (let i = 0, len = this.geometriesList.length; i < len; i++) {
            const geometry = this.geometriesList[i];
            if (geometry.primitiveType === "triangles") {
                geometry.solid = isTriangleMeshSolid(geometry.indices, geometry.positionsQuantized); // Better memory/cpu performance with quantized values
            }
        }
    }
}

export {XKTModel};