Source: overlays/fieldofview/fieldofview-geometry.mjs

/**
 * Field of view: mpp validation, physical diameter → base pixels, project ↔ image for preview.
 */

import { OpenSeadragon } from '../../osd-loader.mjs';
import { paper } from '../../paperjs.mjs';
import { mppFromTiledImage } from '../../utils/mpp.mjs';

export { mppFromTiledImage };

const FOV_DEBUG = true;
/** @param {...unknown} args */
function fovGeometryLog(...args) {
    if (FOV_DEBUG) console.log('[FOV geometry]', ...args);
}

/**
 * Isotropic diameter along image +X (mpp.x), v1 policy per overlay spec.
 * @param {{ x: number, y: number }} mpp
 * @param {number} diameterPhysical
 * @param {'mm' | 'um'} unit
 */
export function diameterPhysicalToBasePixels(mpp, diameterPhysical, unit) {
    const d = Number(diameterPhysical);
    if (!mpp || !Number.isFinite(d) || d <= 0) return null;
    if (unit === 'um') {
        return d / mpp.x;
    }
    return (d * 1000) / mpp.x;
}

/**
 * Circle area in mm² from diameter in physical units (mm or µm).
 * @param {number} diameterPhysical
 * @param {'mm' | 'um'} unit
 * @returns {number|null}
 */
export function areaMm2FromDiameterPhysical(diameterPhysical, unit) {
    const d = Number(diameterPhysical);
    if (!Number.isFinite(d) || d <= 0) return null;
    const dMm = unit === 'um' ? d / 1000 : d;
    return Math.PI * (dMm / 2) ** 2;
}

/**
 * Diameter in physical `unit` (mm or µm) for a circle of the given area in mm².
 * @param {number} areaMm2
 * @param {'mm' | 'um'} unit
 * @returns {number|null}
 */
export function diameterPhysicalFromAreaMm2(areaMm2, unit) {
    const A = Number(areaMm2);
    if (!Number.isFinite(A) || A <= 0) return null;
    const dMm = 2 * Math.sqrt(A / Math.PI);
    if (unit === 'um') {
        return dMm * 1000;
    }
    return dMm;
}

/**
 * @param {OpenSeadragon.Viewer} viewer
 * @param {paper.PaperScope} paperScope
 * @param {paper.Point} projectPoint
 * @returns {OpenSeadragon.Point}
 */
export function projectPointToImagePoint(viewer, paperScope, projectPoint) {
    const vp = viewer.viewport;
    const viewPt = paperScope.view.projectToView(projectPoint);
    const viewerPt = new OpenSeadragon.Point(viewPt.x, viewPt.y);
    const viewportPt = vp.viewerElementToViewportCoordinates(viewerPt);
    return vp.viewportToImageCoordinates(viewportPt);
}

/**
 * @param {OpenSeadragon.Viewer} viewer
 * @param {paper.PaperScope} paperScope
 * @param {OpenSeadragon.Point} imagePoint
 * @returns {paper.Point}
 */
export function imagePointToProjectPoint(viewer, paperScope, imagePoint) {
    const vp = viewer.viewport;
    const viewportPt = vp.imageToViewportCoordinates(imagePoint.x, imagePoint.y);
    const viewerPt = vp.viewportToViewerElementCoordinates(viewportPt);
    return paperScope.view.viewToProject(new paper.Point(viewerPt.x, viewerPt.y));
}

/**
 * Preview circle in project space; committed drop uses centerImg + diameterPx on the tile layer.
 * @returns {{ centerProj: paper.Point, radiusProj: number, centerImg: OpenSeadragon.Point } | null}
 */
export function circlePreviewGeometryFromProjectPoint(viewer, paperScope, projectPoint, diameterPx) {
    const d = Number(diameterPx);
    if (!projectPoint || !Number.isFinite(d) || d <= 0) {
        fovGeometryLog('circlePreviewGeometryFromProjectPoint null (inputs)', {
            hasProjectPoint: Boolean(projectPoint),
            diameterPx: d,
        });
        return null;
    }
    const rPx = d / 2;
    const centerImg = projectPointToImagePoint(viewer, paperScope, projectPoint);
    if (
        !Number.isFinite(centerImg.x) ||
        !Number.isFinite(centerImg.y) ||
        Number.isNaN(centerImg.x) ||
        Number.isNaN(centerImg.y)
    ) {
        fovGeometryLog('circlePreviewGeometryFromProjectPoint null (centerImg NaN)', {
            centerImg: { x: centerImg.x, y: centerImg.y },
            projectPoint: { x: projectPoint.x, y: projectPoint.y },
        });
        return null;
    }
    const rimImg = new OpenSeadragon.Point(centerImg.x + rPx, centerImg.y);
    const centerProj = imagePointToProjectPoint(viewer, paperScope, centerImg);
    const rimProj = imagePointToProjectPoint(viewer, paperScope, rimImg);
    const radiusProj = centerProj.getDistance(rimProj);
    if (!Number.isFinite(radiusProj) || radiusProj <= 0) {
        fovGeometryLog('circlePreviewGeometryFromProjectPoint null (radiusProj)', {
            radiusProj,
            centerProj: { x: centerProj.x, y: centerProj.y },
            rimProj: { x: rimProj.x, y: rimProj.y },
        });
        return null;
    }
    return { centerProj, radiusProj, centerImg };
}