/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */ /** */
import { setMax, setMin } from '../math';
import { FastTransform } from '../math/FastTransform';
import { Vector3, Geometry, VertexBuffer } from './babylonjs-import';
import { telemetry } from '../Telemetry';
import { decodePackedNormal } from './PackedNormal';
/**
 * Represents a handle for a 3D geometry.
 * Can be used to unload the geometry from the GPU.
 */
export class Geometry3dHandle {
    id;
    aabb;
    isTransparent;
    productsAndMeshes;
    cullingDistance;
    center;
    /**
     * Creates a new instance of the {@link Geometry3dHandle} class.
     * @param id - The unique identifier for the geometry.
     * @param aabb - The axis-aligned bounding box of the geometry.
     * @param isTransparent - Indicates whether the geometry is transparent.
     * @param productsAndMeshes - The products and meshes associated with the geometry.
     * @param cullingDistance - The distance at which the geometry should be culled.
     * @param center - The center of the geometry's axis-aligned bounding box in IFC space.
     */
    constructor(id, aabb, isTransparent, productsAndMeshes, cullingDistance, center) {
        this.id = id;
        this.aabb = aabb;
        this.isTransparent = isTransparent;
        this.productsAndMeshes = productsAndMeshes;
        this.cullingDistance = cullingDistance;
        this.center = center;
    }
    /**
     * Flags all the {@link IfcProductMesh} instances this instance
     * refers to as being not on the GPU. Allows for them to be remeshed
     * in a call tocalling {@link BimIfcLoaderElement.geometryBuilder}.
     * Not intended to be called by the user.
     * @hidden
     */
    _resetBuildId() {
        for (const [, meshes] of this.productsAndMeshes) {
            for (const mesh of meshes) {
                mesh._clearMergeId();
            }
        }
    }
}
/**
 * Returns the number of primitive (vertex) slots that must be reserved in a {@link GeometryBuffer}
 * for `mesh` before a merge runs. For `t8gd` meshes with `includeNormals` true this is the worst-case
 * `descriptor.i` (per-face-normal expansion before WASM dedup); otherwise `descriptor.p`.
 *
 * Single source of truth shared between {@link VertexDataMerge}'s constructor and external callers
 * that pre-size the buffer (e.g. `BimIfcObject.initializeCreateGeometryFrom`).
 */
export function reservedPrimitiveCount(mesh, includeNormals) {
    const isPerFace = includeNormals && mesh.loaderElement.geometryFormat === 't8gd';
    return isPerFace ? mesh.descriptor.i : mesh.descriptor.p;
}
export class VertexDataMerge {
    id;
    _meshes;
    static _emptyFloat32Array = new Float32Array(0);
    static _emptyUint16Array = new Uint16Array(0);
    _cullingDistance = Number.MAX_VALUE;
    primitiveCount = 0; // Number of vertieces and normals (1 unit is one triplets x, y, z)
    indiceCount = 0; // Number of indices. Number of triangles is / 3.
    get positions() {
        return this._geometry.positions;
    }
    get uvs() {
        return this._geometry.uvs;
    }
    get indices() {
        return this._geometry.indices;
    }
    get cullingDistance() {
        return this._cullingDistance;
    }
    _geometry = {
        positions: VertexDataMerge._emptyFloat32Array,
        uvs: VertexDataMerge._emptyFloat32Array,
        indices: VertexDataMerge._emptyUint16Array
    };
    _currentOffset = {
        indices: 0,
        positions: 0
    };
    isTransparent;
    _options;
    _aabb = {
        min: Vector3.Zero(),
        max: Vector3.Zero()
    };
    constructor(id, geometryBuffer, _meshes, options) {
        this.id = id;
        this._meshes = _meshes;
        this._options = {
            includeNormals: true,
            disableTransparencyAndMergeIdChecks: false,
            cullOnCameraMove: false,
            noTransformCache: true,
            compactAfterMerge: false,
            ...options
        };
        if (this._meshes.length > 0) {
            const first = this._meshes[0];
            this.isTransparent = first.style.a < 255;
            this._cullingDistance = first.cullingDistance;
            const createMeshSet = (product) => new Set();
            for (const mesh of this._meshes) {
                this.indiceCount += mesh.descriptor.i;
                this.primitiveCount += reservedPrimitiveCount(mesh, this._options.includeNormals);
                this.productsAndMeshes.getOrAdd(mesh.ifcProduct, createMeshSet).add(mesh);
                if (!this._options.disableTransparencyAndMergeIdChecks) {
                    if (first.mergeId !== mesh.mergeId)
                        throw new Error(`Attempt to merge meshes with differing mergeIds: ${first.mergeId} != ${mesh.mergeId}`);
                    if (this.isTransparent && mesh.style.a === 255)
                        throw new Error(`Attempt to merge meshes opaque and transparent meshes`);
                    if (this._cullingDistance !== mesh.cullingDistance)
                        throw new Error(`Attempt to merge meshes with different culling distance: ${this.cullingDistance} != ${mesh.cullingDistance}`);
                }
                else {
                    this._cullingDistance = Math.min(this._cullingDistance, mesh.cullingDistance);
                }
            }
        }
        this._geometry = geometryBuffer.malloc(this.primitiveCount, this.indiceCount);
    }
    /**
     * Not intended to be called by the user. Use `this.toHandle()._resetBuildId()` instead.
     * @hidden
     */
    _resetBuildId() { }
    productsAndMeshes = new Map();
    get cullOnCameraMove() {
        return this._options.cullOnCameraMove;
    }
    get aabb() {
        return this._aabb;
    }
    get center() {
        return this._meshes[0].centerOfParentIfcCube;
    }
    /**
     * Applies the geometry data this instances represents ({@link positions}, {@link uvs}, {@link indices}, {@link aabb} and optionally `normals`.)
     * to the specified {@link Mesh}.
     * @param mesh {@link Mesh} to receive geometry data.
     */
    applyToMesh(mesh, engine) {
        if (this._meshes.length === 0) {
            return;
        }
        const g = Geometry.CreateGeometryForMesh(mesh);
        this.applyToGeometry(g, engine);
        mesh.position.copyFrom(this.center);
    }
    /**
     * Applies the geometry data this instances represents ({@link positions}, {@link uvs}, {@link indices}, {@link aabb} and optionally `normals`.)
     * to the specified {@link Geometry}.
     * Notice that the geometry is in IFC space. If geometry is assing to a {@link Mesh} one can use {@link coordinateSystems.toBabylonTransform}
     * as the mesh pretransform matrix. This will ensure that mesh behaves as if it is in BabylonJS worldspace.
     * @param babylonGeometry {@link Geometry} to receive geometry data.
     */
    applyToGeometry(babylonGeometry, engine) {
        if (this._meshes.length === 0) {
            return;
        }
        const babylonGeometryAny = babylonGeometry;
        const oldUpdateExtend = babylonGeometryAny._updateExtend;
        // Optimization. Normally babylonjs will calculate extends (when .setAllVerticesData is called)
        // by iterating all positions however since we already know max and min we can simply
        // skip that and set it directly
        babylonGeometryAny._updateExtend = (data = null) => {
            babylonGeometryAny._extend = {
                minimum: this.aabb.min.clone(),
                maximum: this.aabb.max.clone()
            };
        };
        const actualPositionCount = this._currentOffset.positions;
        const actualIndexCount = this._currentOffset.indices;
        const positionsToUpload = this.positions.subarray(0, actualPositionCount * 3);
        const uvsToUpload = this.uvs.subarray(0, actualPositionCount * 2);
        const indicesToUpload = this.indices.subarray(0, actualIndexCount);
        if (this._options.includeNormals) {
            // TODO Could probably be optimized to use a single cached (large enough) float array instead of
            // recreating it
            babylonGeometry.setVerticesData(VertexBuffer.NormalKind, this.copyNormalsTo(new Float32Array(actualPositionCount * 3)));
        }
        babylonGeometry.setVerticesData(VertexBuffer.UVKind, uvsToUpload);
        babylonGeometry.setIndices(indicesToUpload);
        // Note: Vertex data needs to be set after indices are set. Indices are used by BabylonJS to
        // determine if the created submesh is the only mesh in the geometry.
        // If there are no indices it will determine that it is not and recalculate bounding info
        babylonGeometry.setVerticesData(VertexBuffer.PositionKind, positionsToUpload);
        babylonGeometryAny._updateExtend = oldUpdateExtend;
    }
    copyNormalsTo(dst) {
        const actualPositionCount = this._currentOffset.positions;
        if (dst.length < actualPositionCount * 3) {
            throw new Error('Normals array must be at least actualPositionCount * 3 elements');
        }
        const uvLen = actualPositionCount * 2;
        for (let packedNormalOffset = 1, normalOffset = 0; packedNormalOffset < uvLen; packedNormalOffset += 2) {
            // The normals received from the backend is encoded into a uint16, which in turn are stored in the first two bytes of the uv.y component.
            const packedNormal = this.uvs[packedNormalOffset];
            const normal = decodePackedNormal[packedNormal];
            dst[normalOffset++] = normal.x;
            dst[normalOffset++] = normal.y;
            dst[normalOffset++] = normal.z;
        }
        return dst;
    }
    merge() {
        const itemsToMergeLen = this._meshes.length;
        if (itemsToMergeLen === 0) {
            telemetry.trackTrace({ message: 'nothing to merge' });
            return this;
        }
        setMax(this._aabb.min);
        setMin(this._aabb.max);
        const firstMesh = this._meshes[0];
        let mergedViaBatch = false;
        if (firstMesh.loaderElement.hasBatchSupport) {
            const batchResult = firstMesh.loaderElement.writeBatch(this._meshes, this._geometry, this._currentOffset);
            if (batchResult) {
                this._aabb.min.set(batchResult.aabb.minX, batchResult.aabb.minY, batchResult.aabb.minZ);
                this._aabb.max.set(batchResult.aabb.maxX, batchResult.aabb.maxY, batchResult.aabb.maxZ);
                this._currentOffset.positions += batchResult.vertexCount;
                this._currentOffset.indices += batchResult.indexCount;
                mergedViaBatch = true;
            }
        }
        if (!mergedViaBatch) {
            const center = this.center;
            const emptyTransform = FastTransform.identity();
            const { noTransformCache } = this._options;
            for (let mergeItemIdx = 0; mergeItemIdx < itemsToMergeLen; ++mergeItemIdx) {
                const mesh = this._meshes[mergeItemIdx];
                const transform = mesh.fastTransform(noTransformCache).translateToRef(center, emptyTransform);
                const { vertexCount, indexCount } = mesh.loaderElement.writeVertexData(mesh, transform, this._geometry, this._currentOffset, this.aabb, this._options);
                this._currentOffset.indices += indexCount;
                this._currentOffset.positions += vertexCount;
            }
        }
        // TODO Should we attempt to remove duplicate vertices???? After all we have now merged multiple geometries into one
        // perhaps there are a number of vertices that are now duplicates and could be removed
        if (this._options.compactAfterMerge) {
            this.compact();
        }
        return this;
    }
    /**
     * Replaces the internal geometry arrays with tightly-sized copies of the actually-written prefix.
     * After this call, the views into the {@link GeometryBuffer} that backed the merge are dropped, so the
     * underlying ArrayBuffers become GC-eligible once the {@link GeometryBuffer} itself goes out of scope.
     *
     * Safe to call only after {@link merge}. Result-equivalent on repeated calls but each call performs
     * a fresh allocation; do not call more than once per merge. {@link _currentOffset} is preserved so
     * that {@link applyToGeometry} and {@link copyNormalsTo} continue to compute identical bounds.
     *
     * The indices array constructor (Uint16Array vs Uint32Array) is preserved; we deliberately do NOT
     * downgrade Uint32 to Uint16 even when the actual position count fits, because consumers may hold
     * references typed against the original choice.
     */
    compact() {
        const actualPositionCount = this._currentOffset.positions;
        const actualIndexCount = this._currentOffset.indices;
        if (actualPositionCount === 0 && actualIndexCount === 0) {
            return;
        }
        const tightPositions = new Float32Array(actualPositionCount * 3);
        tightPositions.set(this._geometry.positions.subarray(0, actualPositionCount * 3));
        const tightUvs = new Float32Array(actualPositionCount * 2);
        tightUvs.set(this._geometry.uvs.subarray(0, actualPositionCount * 2));
        const tightIndices = this._geometry.indices instanceof Uint16Array
            ? new Uint16Array(actualIndexCount)
            : new Uint32Array(actualIndexCount);
        tightIndices.set(this._geometry.indices.subarray(0, actualIndexCount));
        this._geometry = {
            positions: tightPositions,
            uvs: tightUvs,
            indices: tightIndices
        };
    }
    toHandle() {
        return new Geometry3dHandle(this.id, {
            min: this.aabb.min.clone(),
            max: this.aabb.max.clone()
        }, this.isTransparent, new Map(this.productsAndMeshes), this.cullingDistance, this.center.clone());
    }
}
//# sourceMappingURL=vertex-data-merge.js.map