var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Collider, CollisionComponent, Component, Emitter, Mesh, MeshAttribute, MeshComponent, MeshIndexType, } from '@wonderlandengine/api';
import { property } from '@wonderlandengine/api/decorators.js';
import { setXRRigidTransformLocal } from './utils/webxr.js';
// FIXME: earcut overrides default export, breaking our tests
import earcut from 'earcut';
const tempVec3 = new Float32Array(3);
/** Compute minimum and maxium extents of given list of contour points */
function extentsFromContour(out, points) {
    if (points.length == 0)
        return out;
    let absMaxX = Math.abs(points[0].x);
    let absMaxZ = Math.abs(points[0].z);
    for (let i = 1; i < points.length; ++i) {
        absMaxX = Math.max(absMaxX, Math.abs(points[i].x));
        absMaxZ = Math.max(absMaxZ, Math.abs(points[i].z));
    }
    out[0] = absMaxX;
    out[1] = 0;
    out[2] = absMaxZ;
}
/** Check whether x lies between a and b */
function within(x, a, b) {
    if (a > b)
        return x < a && x > b;
    return x > a && x < b;
}
/**
 * Check whether given point on plane's bounding box is inside plane's polygon
 *
 * @param p 3D point in plane's local space, Y value is ignored, since it is assumed
 *     that the point was checked against the plane's bounding box.
 * @param plane XRPlane that has `XRPlane.polygon`
 * @returns `true` if the point lies on the plane
 */
export function isPointLocalOnXRPlanePolygon(p, plane) {
    const points = plane.polygon;
    if (points.length < 3)
        return false;
    /* Count ray intersections: even == inside, odd == outside */
    const pX = p[0];
    const pZ = p[2];
    let intersections = 0;
    for (let n = 0, l = points.length - 1; n < points.length; ++n) {
        const aX = points[l].x;
        const aZ = points[l].z;
        const s = (points[n].z - aZ) / (points[n].x - aX);
        const x = Math.abs((pZ - aZ) / s);
        if (x >= 0.0 && x <= 1.0 && within(x + pX, aX, points[n].x))
            ++intersections;
        l = n;
    }
    return (intersections & 1) == 0;
}
/**
 * Check whether given point on plane's bounding box is inside plane's polygon
 *
 * @param p 3D point to test. It is assumed that the point was checked against
 *     the plane's bounding box beforehand.
 * @param plane XRPlane that has `XRPlane.polygon`
 * @returns `true` if the point lies on the plane
 */
export function isPointWorldOnXRPlanePolygon(object, p, plane) {
    if (plane.polygon.length < 3)
        return false;
    isPointLocalOnXRPlanePolygon(object.transformPointInverseWorld(tempVec3, p), plane);
}
/**
 * Create a plane mesh from a list of contour points
 *
 * @param engine Engine to create the mesh with
 * @param points Contour points
 * @param meshToUpdate Optional mesh to update instead of creating a new one.
 */
function planeMeshFromContour(engine, points, meshToUpdate = null) {
    const vertexCount = points.length;
    const vertices = new Float32Array(vertexCount * 2);
    for (let i = 0, d = 0; i < vertexCount; ++i, d += 2) {
        vertices[d] = points[i].x;
        vertices[d + 1] = points[i].z;
    }
    const triangles = earcut(vertices);
    const mesh = meshToUpdate ||
        new Mesh(engine, {
            vertexCount,
            /* Assumption here that we will never have more than 256 points
             * in the detected plane meshes! */
            indexType: MeshIndexType.UnsignedByte,
            indexData: triangles,
        });
    if (mesh.vertexCount !== vertexCount) {
        console.warn('vertexCount of meshToUpdate did not match required vertexCount');
        return mesh;
    }
    const positions = mesh.attribute(MeshAttribute.Position);
    const textureCoords = mesh.attribute(MeshAttribute.TextureCoordinate);
    const normals = mesh.attribute(MeshAttribute.Normal);
    tempVec3[1] = 0;
    for (let i = 0, s = 0; i < vertexCount; ++i, s += 2) {
        tempVec3[0] = vertices[s];
        tempVec3[2] = vertices[s + 1];
        positions.set(i, tempVec3);
    }
    textureCoords?.set(0, vertices);
    if (normals) {
        tempVec3[0] = 0;
        tempVec3[1] = 1;
        tempVec3[2] = 0;
        for (let i = 0; i < vertexCount; ++i) {
            normals.set(i, tempVec3);
        }
    }
    if (meshToUpdate)
        mesh.update();
    return mesh;
}
/**
 * Generate meshes and collisions for XRPlanes using [WebXR Device API - Plane Detection](https://immersive-web.github.io/real-world-geometry/plane-detection.html).
 */
class PlaneDetection extends Component {
    static TypeName = 'plane-detection';
    /**
     * Material to assign to created plane meshes or `null` if meshes should not be created.
     */
    planeMaterial = null;
    /**
     * Collision mask to assign to newly created collision components or a negative value if
     * collision components should not be created.
     */
    collisionMask = -1;
    /** Map of all planes and their last updated timestamps */
    planes = new Map();
    /** Objects generated for each XRPlane */
    planeObjects = new Map();
    /** Called when a plane starts tracking */
    onPlaneFound = new Emitter();
    /** Called when a plane stops tracking */
    onPlaneLost = new Emitter();
    update() {
        if (!this.engine.xr?.frame)
            return;
        // @ts-ignore
        if (this.engine.xr.frame.detectedPlanes === undefined) {
            console.error('plane-detection: WebXR feature not available.');
            this.active = false;
            return;
        }
        // @ts-ignore
        const detectedPlanes = this.engine.xr.frame.detectedPlanes;
        for (const [plane, _] of this.planes) {
            if (!detectedPlanes.has(plane)) {
                this.#planeLost(plane);
            }
        }
        detectedPlanes.forEach((plane) => {
            if (this.planes.has(plane)) {
                if (plane.lastChangedTime > this.planes.get(plane)) {
                    this.#planeUpdate(plane);
                }
            }
            else {
                this.#planeFound(plane);
            }
            this.#planeUpdatePose(plane);
        });
    }
    #planeLost(plane) {
        this.planes.delete(plane);
        const o = this.planeObjects.get(plane);
        this.onPlaneLost.notify(plane, o);
        /* User might destroy the object */
        if (o.objectId > 0)
            o.destroy();
    }
    #planeFound(plane) {
        this.planes.set(plane, plane.lastChangedTime);
        const o = this.engine.scene.addObject(this.object);
        this.planeObjects.set(plane, o);
        if (this.planeMaterial) {
            o.addComponent(MeshComponent, {
                mesh: planeMeshFromContour(this.engine, plane.polygon),
                material: this.planeMaterial,
            });
        }
        if (this.collisionMask >= 0) {
            extentsFromContour(tempVec3, plane.polygon);
            tempVec3[1] = 0.025;
            o.addComponent(CollisionComponent, {
                group: this.collisionMask,
                collider: Collider.Box,
                extents: tempVec3,
            });
        }
        this.onPlaneFound.notify(plane, o);
    }
    #planeUpdate(plane) {
        this.planes.set(plane, plane.lastChangedTime);
        const planeMesh = this.planeObjects.get(plane).getComponent(MeshComponent);
        if (!planeMesh)
            return;
        planeMeshFromContour(this.engine, plane.polygon, planeMesh.mesh);
    }
    #planeUpdatePose(plane) {
        const o = this.planeObjects.get(plane);
        const pose = this.engine.xr.frame.getPose(plane.planeSpace, this.engine.xr.currentReferenceSpace);
        if (!pose) {
            o.active = false;
            return;
        }
        setXRRigidTransformLocal(o, pose.transform);
    }
}
__decorate([
    property.material()
], PlaneDetection.prototype, "planeMaterial", void 0);
__decorate([
    property.int()
], PlaneDetection.prototype, "collisionMask", void 0);
export { PlaneDetection };
//# sourceMappingURL=plane-detection.js.map