import { ControlVector3, traverseControlObject } from "./NativeControlTypes.js";
export class Picking {
    pick(root, origin = new ControlVector3(), direction = new ControlVector3(0, 0, -1), options = {}) {
        return this.report(root, origin, direction, options).hit;
    }
    pickAll(root, origin = new ControlVector3(), direction = new ControlVector3(0, 0, -1), options = {}) {
        return this.report(root, origin, direction, options).hits;
    }
    report(root, origin = new ControlVector3(), direction = new ControlVector3(0, 0, -1), options = {}) {
        const normalizedDirection = normalizedRayDirection(direction);
        const near = finiteRange(options.near ?? 0, "near", 0);
        const far = finiteRange(options.far ?? Number.POSITIVE_INFINITY, "far", near);
        const tolerance = finiteRange(options.tolerance ?? 0, "tolerance", 0);
        const hits = [];
        const counters = {
            testedObjects: 0,
            candidateObjects: 0,
            skippedInvisible: 0,
            skippedNonPickable: 0,
            skippedDisabled: 0,
            skippedOutOfRange: 0,
            skippedMissedRadius: 0
        };
        let nearestMissPerpendicularDistance = Number.POSITIVE_INFINITY;
        traverseControlObject(root, (object) => {
            counters.testedObjects += 1;
            if (object.visible === false && options.includeInvisible !== true) {
                counters.skippedInvisible += 1;
                return;
            }
            const metadata = resolvePickMetadata(object, options);
            if (metadata?.selectable === false) {
                counters.skippedDisabled += 1;
                return;
            }
            if (!isPickableObject(object, metadata, options)) {
                counters.skippedNonPickable += 1;
                return;
            }
            counters.candidateObjects += 1;
            const toObject = new ControlVector3(object.position.x - origin.x, object.position.y - origin.y, object.position.z - origin.z);
            const distanceAlongRay = dot(toObject, normalizedDirection);
            if (distanceAlongRay < near || distanceAlongRay > far) {
                counters.skippedOutOfRange += 1;
                return;
            }
            const closestPoint = pointOnRay(origin, normalizedDirection, distanceAlongRay);
            const perpendicularDistance = new ControlVector3(object.position.x - closestPoint.x, object.position.y - closestPoint.y, object.position.z - closestPoint.z).length();
            const radius = resolvePickRadius(object, metadata) + tolerance;
            if (perpendicularDistance > radius) {
                counters.skippedMissedRadius += 1;
                nearestMissPerpendicularDistance = Math.min(nearestMissPerpendicularDistance, perpendicularDistance);
                return;
            }
            const rayEntryDistance = Math.max(near, distanceAlongRay - Math.sqrt(Math.max(0, radius * radius - perpendicularDistance * perpendicularDistance)));
            hits.push({
                object,
                distance: rayEntryDistance,
                distanceAlongRay,
                perpendicularDistance,
                point: pointOnRay(origin, normalizedDirection, rayEntryDistance),
                radius,
                metadata
            });
        });
        hits.sort(compareHits);
        const hit = hits[0] ?? null;
        return {
            hit,
            hits,
            diagnostics: {
                ...counters,
                hitCount: hits.length,
                nearestMissPerpendicularDistance: Number.isFinite(nearestMissPerpendicularDistance) ? nearestMissPerpendicularDistance : null,
                nearestHitLabel: hit ? pickLabel(hit.object, hit.metadata) : null
            }
        };
    }
}
function normalizedRayDirection(direction) {
    const normalized = new ControlVector3(direction.x, direction.y, direction.z);
    const length = normalized.length();
    if (!Number.isFinite(length) || length <= 1e-8) {
        throw new RangeError("Picking ray direction must be finite and non-zero.");
    }
    return normalized.set(normalized.x / length, normalized.y / length, normalized.z / length);
}
function finiteRange(value, label, minimum) {
    if (value === Number.POSITIVE_INFINITY && label === "far")
        return value;
    if (!Number.isFinite(value) || value < minimum) {
        throw new RangeError(`Picking ${label} must be finite and >= ${minimum}.`);
    }
    return value;
}
function resolvePickMetadata(object, options) {
    return options.metadata?.(object)
        ?? object.picking
        ?? readUserDataMetadata(object.userData);
}
function readUserDataMetadata(userData) {
    const value = userData?.a3dPicking ?? userData?.picking;
    if (!value || typeof value !== "object")
        return undefined;
    return value;
}
function isPickableObject(object, metadata, options) {
    if (metadata && options.includeNonRenderableMetadata !== false)
        return true;
    return object.type === "Mesh" || object.type === "Sprite" || object.type === "Points" || object.type === "LineSegments";
}
function resolvePickRadius(object, metadata) {
    const explicit = metadata?.pickRadius ?? object.pickRadius ?? readUserDataRadius(object.userData);
    if (explicit !== undefined)
        return positiveFinite(explicit, "pickRadius");
    const scale = object.scale;
    if (!scale)
        return 0.5;
    const radius = Math.max(Math.abs(scale.x), Math.abs(scale.y), Math.abs(scale.z)) * 0.5;
    return Number.isFinite(radius) && radius > 0 ? radius : 0.5;
}
function readUserDataRadius(userData) {
    const value = userData?.a3dPickRadius;
    return typeof value === "number" ? value : undefined;
}
function positiveFinite(value, label) {
    if (!Number.isFinite(value) || value <= 0)
        throw new RangeError(`Picking ${label} must be finite and positive.`);
    return value;
}
function dot(left, right) {
    return left.x * right.x + left.y * right.y + left.z * right.z;
}
function pointOnRay(origin, direction, distance) {
    return new ControlVector3(origin.x + direction.x * distance, origin.y + direction.y * distance, origin.z + direction.z * distance);
}
function compareHits(left, right) {
    const distanceDelta = left.distance - right.distance;
    if (Math.abs(distanceDelta) > 1e-8)
        return distanceDelta;
    return pickPriority(right) - pickPriority(left);
}
function pickPriority(hit) {
    return hit.metadata?.priority ?? hit.object.pickPriority ?? 0;
}
function pickLabel(object, metadata) {
    return metadata?.label ?? metadata?.id ?? object.name ?? null;
}
//# sourceMappingURL=Picking.js.map