import { getDieKey } from './dies.js';
// ── Geometry ──────────────────────────────────────────────────────────────────
/** Rotate (x, y) clockwise by angleDegrees around (cx, cy). */
export function rotatePoint(x, y, angleDegrees, cx = 0, cy = 0) {
    const rad = (angleDegrees * Math.PI) / 180;
    const cos = Math.cos(rad), sin = Math.sin(rad);
    const dx = x - cx, dy = y - cy;
    return { x: cx + dx * cos + dy * sin, y: cy - dx * sin + dy * cos };
}
/** The identity transform. */
export function affineIdentity() {
    return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
}
/**
 * Clockwise rotation by `angleDegrees` about (cx, cy) — the same convention as
 * {@link rotatePoint}, which this reproduces exactly.
 */
export function affineRotation(angleDegrees, cx = 0, cy = 0) {
    const rad = (angleDegrees * Math.PI) / 180;
    const cos = Math.cos(rad), sin = Math.sin(rad);
    // Linear part [a c; b d] = [cos sin; -sin cos]; translation keeps (cx,cy) fixed.
    return {
        a: cos, c: sin,
        b: -sin, d: cos,
        e: cx - (cos * cx + sin * cy),
        f: cy - (-sin * cx + cos * cy),
    };
}
/** Mirror through (cx, cy) on each requested axis. */
export function affineMirror(flipX, flipY, cx = 0, cy = 0) {
    return {
        a: flipX ? -1 : 1, c: 0,
        b: 0, d: flipY ? -1 : 1,
        e: flipX ? 2 * cx : 0,
        f: flipY ? 2 * cy : 0,
    };
}
/**
 * Compose two transforms: `inner` runs first, then `outer`. The frame brands must
 * meet (`inner` ends where `outer` begins), so an out-of-order composition — the
 * exact mistake that mis-drew reticle fields — will not type-check.
 */
export function affineCompose(outer, inner) {
    return {
        a: outer.a * inner.a + outer.c * inner.b,
        b: outer.b * inner.a + outer.d * inner.b,
        c: outer.a * inner.c + outer.c * inner.d,
        d: outer.b * inner.c + outer.d * inner.d,
        e: outer.a * inner.e + outer.c * inner.f + outer.e,
        f: outer.b * inner.e + outer.d * inner.f + outer.f,
    };
}
/** Invert a transform. Every transform built here is a rotation/mirror, so it is always invertible. */
export function affineInvert(m) {
    const det = m.a * m.d - m.b * m.c;
    const ia = m.d / det, ib = -m.b / det, ic = -m.c / det, id = m.a / det;
    return { a: ia, b: ib, c: ic, d: id, e: -(ia * m.e + ic * m.f), f: -(ib * m.e + id * m.f) };
}
/** Transform a point (translation applies). */
export function affinePoint(m, x, y) {
    return { x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f };
}
/**
 * Transform a direction/offset (translation does NOT apply). Use for arrows and
 * any other quantity that is a vector rather than a position.
 */
export function affineVector(m, dx, dy) {
    return { x: m.a * dx + m.c * dy, y: m.b * dx + m.d * dy };
}
/**
 * True when the transform maps the x-axis onto the y-axis (the 90°/270° class,
 * with or without mirroring) — i.e. when an axis-aligned rectangle's width and
 * height must be exchanged to describe its image.
 *
 * Rotations that are not a multiple of 90° do not map an axis-aligned rectangle
 * to an axis-aligned rectangle at all; those return `false`, matching the
 * library's existing behaviour of drawing dies as unrotated rectangles.
 */
export function affineSwapsAxes(m) {
    return Math.abs(m.a) < 1e-9 && Math.abs(m.d) < 1e-9 && Math.abs(m.b) > 1e-9;
}
/** Perpendicular distance from wafer centre to notch/flat chord: sqrt(r² − (L/2)²). */
function alignmentChordDistance(radius, chordLength) {
    return Math.sqrt(radius * radius - (chordLength / 2) ** 2);
}
/**
 * Check whether (x, y) — in wafer-local (pre-rotation) coordinates —
 * lies inside the wafer boundary, including the notch/flat exclusion zone.
 */
export function isInsideWafer(x, y, wafer) {
    const dx = x - wafer.center.x, dy = y - wafer.center.y;
    if (dx * dx + dy * dy > wafer.radius * wafer.radius)
        return false;
    if (wafer.notch) {
        const d = alignmentChordDistance(wafer.radius, wafer.notch.length);
        if (wafer.notch.type === 'bottom' && dy < -d)
            return false;
        if (wafer.notch.type === 'top' && dy > d)
            return false;
        if (wafer.notch.type === 'left' && dx < -d)
            return false;
        if (wafer.notch.type === 'right' && dx > d)
            return false;
    }
    return true;
}
// ── Pipeline transforms ───────────────────────────────────────────────────────
/**
 * Clip dies to the wafer boundary (circle + optional notch/flat).
 * When dieConfig is supplied, all four corners are checked to detect partial dies.
 * Operates on wafer-local coordinates (before applyOrientation).
 */
export function clipDiesToWafer(dies, wafer, dieConfig) {
    const result = [];
    for (const die of dies) {
        // Only ever called on synthetically generated dies (generateDies), which
        // always carry physX/physY by construction — not part of the
        // coordinate-less data path (buildWaferMap never clips real prober
        // results this way).
        const physX = die.physX, physY = die.physY;
        const centerIn = isInsideWafer(physX, physY, wafer);
        if (!dieConfig) {
            if (centerIn)
                result.push({ ...die, insideWafer: true, partial: false });
            continue;
        }
        const hw = dieConfig.width / 2, hh = dieConfig.height / 2;
        const corners = [
            [physX - hw, physY - hh], [physX + hw, physY - hh],
            [physX + hw, physY + hh], [physX - hw, physY + hh],
        ];
        const cornersIn = corners.filter(([cx, cy]) => isInsideWafer(cx, cy, wafer)).length;
        if (!centerIn && cornersIn === 0)
            continue;
        result.push({ ...die, insideWafer: true, partial: cornersIn < 4 });
    }
    return result;
}
/**
 * Rotate all die display coordinates by wafer.orientation around wafer.center.
 * Call this after clipping and data mapping so that i/j indices remain intact.
 */
export function applyOrientation(dies, wafer) {
    if (wafer.orientation === 0)
        return dies;
    const m = affineRotation(wafer.orientation, wafer.center.x, wafer.center.y);
    return dies.map((die) => {
        const p = affinePoint(m, die.physX, die.physY);
        return { ...die, physX: p.x, physY: p.y };
    });
}
/**
 * Apply interactive transforms (rotation + flip) to die display coordinates.
 * Rotation is around wafer.center; flip mirrors through wafer.center.
 * Call this at render time — baseDies already have applyOrientation baked in.
 */
export function transformDies(dies, options, center = { x: 0, y: 0 }) {
    const { rotation = 0, flipX = false, flipY = false } = options;
    if (rotation === 0 && !flipX && !flipY)
        return dies;
    // Rotate first, then mirror — composed into one matrix so the whole pipeline is
    // a single pass over the dies. The previous implementation ran up to three
    // separate `.map()` stages, allocating three full copies of the die array.
    const m = affineCompose(affineMirror(flipX, flipY, center.x, center.y), affineRotation(rotation, center.x, center.y));
    return dies.map((d) => {
        const p = affinePoint(m, d.physX, d.physY);
        return { ...d, physX: p.x, physY: p.y };
    });
}
/**
 * Attach data values to dies.
 * matchBy='xy'  — matches by wafer coordinates (v0.1 behaviour)
 * matchBy='ij'  — matches by grid indices
 *
 * Note: duplicate rows with the same coordinate produce last-wins behaviour.
 * Deduplicate `data` upstream if needed.
 */
export function mapDataToDies(dies, data, options) {
    const { valueField, matchBy = 'xy' } = options;
    const lookup = new Map();
    // Each call appends one more mapped value, keyed by how many are already
    // present — so the first call lands at testValues[0], which is the key
    // `plotMode: 'value'` selects by default when no testDefs are supplied.
    const attach = (d) => {
        const v = lookup.get(getDieKey(d));
        if (v === undefined)
            return { ...d };
        const existing = d.testValues ?? {};
        return { ...d, testValues: { ...existing, [Object.keys(existing).length]: v } };
    };
    if (matchBy === 'ij') {
        const iField = options.iField ?? 'x', jField = options.jField ?? 'y';
        for (const row of data)
            lookup.set(`${+row[iField]},${+row[jField]}`, +row[valueField]);
        return dies.map(attach);
    }
    const xField = options.xField ?? 'x', yField = options.yField ?? 'y';
    for (const row of data)
        lookup.set(`${+row[xField]},${+row[yField]}`, +row[valueField]);
    return dies.map(attach);
}
//# sourceMappingURL=transforms.js.map