import {
    line3_closest_points_segment_segment
} from "../../../core/geom/3d/line/line3_closest_points_segment_segment.js";
import { quat3_to_matrix3 } from "../../../core/geom/3d/quaternion/quat3_to_matrix3.js";
import { clip_against_axis_uv } from "./clip_against_axis_uv.js";
import { reduce_manifold_contacts } from "./reduce_manifold_contacts.js";

/**
 * Multi-point manifold construction for an oriented box vs. a triangle.
 *
 * Algorithm (Parry's `contact_manifolds_cuboid_triangle.rs` as the
 * blueprint):
 *
 *  1. SAT over 13 candidate axes — 3 box face normals + 1 triangle face
 *     normal + 9 edge-edge cross products. The triangle's projection
 *     onto an axis is NOT symmetric around its centroid (unlike the
 *     box's), so the per-axis MTV is computed via the asymmetric
 *     `min(push_pos, push_neg)` form rather than the box-box-style
 *     `(rA + rB) - dist`.
 *
 *  2. Branch by winning-axis source:
 *
 *     a. Box face axis (sources 0..2) — reference face = box face along
 *        winning axis, incident polygon = triangle. Project the triangle
 *        into the reference face's (u, v) basis, clip against the
 *        rectangle |u| ≤ half_u, |v| ≤ half_v via Sutherland-Hodgman
 *        with 4 axis-aligned passes. For each surviving (u, v) point,
 *        recover its world position on the triangle plane.
 *
 *     b. Triangle face axis (source 3) — reference = triangle, incident
 *        = box face most antiparallel to the contact normal. Project
 *        the box face's 4 corners into a 2D basis on the triangle
 *        plane, clip against the triangle's 3 edges (3 general
 *        half-plane passes). Recover world positions on the box face
 *        plane.
 *
 *     c. Edge-cross axis (sources 4..12) — single contact at the
 *        closest pair of points on the relevant box edge and triangle
 *        edge (via {@link line3_closest_points_segment_segment}).
 *
 *  3. Reduce surviving contacts to at most {@link MAX_CONTACTS} by
 *     deepest-first then perimeter expansion.
 *
 * Contact convention: `out[0..2]` is the world normal pointing from the
 * triangle's surface toward the box's centre (the direction the box
 * should be pushed to separate from the triangle). The caller in
 * `narrowphase_step.js` swaps to the "B → A" convention as needed.
 *
 * Output layout (mirrors {@link box_box_manifold}):
 *   out[0..2]               : world normal (triangle → box)
 *   out[3]                  : contact count
 *   out[4 + k*7 + 0..2]     : world contact on triangle surface
 *   out[4 + k*7 + 3..5]     : world contact on box surface
 *   out[4 + k*7 + 6]        : penetration depth (positive)
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */

const MAX_CONTACTS = 4;
const CONTACT_STRIDE = 7;
const PARALLEL_EPS_SQR = 1e-8;

/**
 * Length of `out` required by {@link box_triangle_contact}.
 * @type {number}
 */
export const BOX_TRIANGLE_OUT_LENGTH = 4 + MAX_CONTACTS * CONTACT_STRIDE;

// --- scratch storage (allocation-free across calls) ----------------------------

const scratch_axes = new Float64Array(9);

// Triangle projected to the box face's uv basis (start with 3 points;
// after axis-aligned clipping can grow to at most 7 points).
const tri_uv_in  = new Float64Array(8 * 2);
const tri_uv_out = new Float64Array(8 * 2);

// Box face corners (for the triangle-axis-winner branch).
const box_face_corners_uv_in  = new Float64Array(8 * 2);
const box_face_corners_uv_out = new Float64Array(8 * 2);

// Surviving contact candidates: stride 7 (3 world-tri + 3 world-box + 1 depth).
const candidates = new Float64Array(8 * 7);

// Closest-points scratch for the edge-cross-winner branch.
const closest_pair_st = new Float64Array(2);

// --- helpers -----------------------------------------------------------------

/**
 * Half-extent of the box projected onto a unit world-axis `(ux, uy, uz)`,
 * given the box's world-space axes and half-extents.
 */
function projected_box_half_extent(axes, hx, hy, hz, ux, uy, uz) {
    const px = ux * axes[0] + uy * axes[1] + uz * axes[2];
    const py = ux * axes[3] + uy * axes[4] + uz * axes[5];
    const pz = ux * axes[6] + uy * axes[7] + uz * axes[8];
    return (px < 0 ? -px : px) * hx
         + (py < 0 ? -py : py) * hy
         + (pz < 0 ? -pz : pz) * hz;
}

/**
 * Sutherland-Hodgman clip of `points_in` against the general half-plane
 * `(p - (line_ox, line_oy)) · (line_nx, line_ny) ≤ 0`. 2D stride 2;
 * result to `points_out`. Returns surviving vertex count.
 */
function clip_against_half_plane_uv(points_in, point_count, points_out, line_ox, line_oy, line_nx, line_ny) {
    let out_count = 0;
    for (let i = 0; i < point_count; i++) {
        const j = (i + 1) % point_count;
        const ax = points_in[i * 2],     ay = points_in[i * 2 + 1];
        const bx = points_in[j * 2],     by = points_in[j * 2 + 1];
        const a_val = (ax - line_ox) * line_nx + (ay - line_oy) * line_ny;
        const b_val = (bx - line_ox) * line_nx + (by - line_oy) * line_ny;
        const a_inside = a_val <= 0;
        const b_inside = b_val <= 0;

        if (a_inside) {
            points_out[out_count * 2] = ax;
            points_out[out_count * 2 + 1] = ay;
            out_count++;
        }
        if (a_inside !== b_inside) {
            const denom = a_val - b_val;
            const t = denom !== 0 ? a_val / denom : 0;
            points_out[out_count * 2]     = ax + (bx - ax) * t;
            points_out[out_count * 2 + 1] = ay + (by - ay) * t;
            out_count++;
        }
    }
    return out_count;
}

/**
 * In-place reduction of `candidates` (stride 7: x, y, z on triangle,
 * x, y, z on box, depth) to at most {@link MAX_CONTACTS} entries.
 * Strategy: keep the deepest, then iteratively pick the candidate
 * whose minimum distance to the already-kept set (in 3D space) is
 * largest — approximates max-perimeter / max-area selection.
 */
// Contact reduction (deepest + farthest-point spread) is shared via
// reduce_manifold_contacts (stride 7 here).
function reduce_contacts(n) {
    return reduce_manifold_contacts(candidates, n, CONTACT_STRIDE, MAX_CONTACTS);
}

// --- main --------------------------------------------------------------------

/**
 * @param {number[]|Float64Array} out length >= {@link BOX_TRIANGLE_OUT_LENGTH}
 * @param {number} bcx box centre x (world)
 * @param {number} bcy
 * @param {number} bcz
 * @param {number} bqx box quaternion x
 * @param {number} bqy
 * @param {number} bqz
 * @param {number} bqw
 * @param {number} bhx box half-extent x (body frame)
 * @param {number} bhy
 * @param {number} bhz
 * @param {number} ax triangle vertex A x (world)
 * @param {number} ay
 * @param {number} az
 * @param {number} bx triangle vertex B x (world)
 * @param {number} by
 * @param {number} bz
 * @param {number} cx triangle vertex C x (world)
 * @param {number} cy
 * @param {number} cz
 * @returns {boolean} true if box and triangle overlap
 */
export function box_triangle_contact(
    out,
    bcx, bcy, bcz, bqx, bqy, bqz, bqw, bhx, bhy, bhz,
    ax, ay, az, bx, by, bz, cx, cy, cz
) {
    quat3_to_matrix3(scratch_axes, 0, bqx, bqy, bqz, bqw);
    const axes = scratch_axes;

    // Triangle edges in world.
    const ab_x = bx - ax, ab_y = by - ay, ab_z = bz - az;
    const bc_x = cx - bx, bc_y = cy - by, bc_z = cz - bz;
    const ca_x = ax - cx, ca_y = ay - cy, ca_z = az - cz;

    // Triangle face normal in world via (B-A) × (C-A).
    const e2x = cx - ax, e2y = cy - ay, e2z = cz - az;
    const tnx_raw = ab_y * e2z - ab_z * e2y;
    const tny_raw = ab_z * e2x - ab_x * e2z;
    const tnz_raw = ab_x * e2y - ab_y * e2x;
    const tn_mag_sqr = tnx_raw * tnx_raw + tny_raw * tny_raw + tnz_raw * tnz_raw;
    if (tn_mag_sqr <= PARALLEL_EPS_SQR) {
        // Degenerate triangle (zero area). No sensible contact.
        out[3] = 0;
        return false;
    }
    const tn_inv = 1 / Math.sqrt(tn_mag_sqr);
    const tnx = tnx_raw * tn_inv;
    const tny = tny_raw * tn_inv;
    const tnz = tnz_raw * tn_inv;

    // Triangle centroid for sign-canonicalisation of the contact normal.
    const tcx = (ax + bx + cx) / 3;
    const tcy = (ay + by + cy) / 3;
    const tcz = (az + bz + cz) / 3;
    // Vector from triangle centroid toward box centre — used to flip the
    // SAT axis so the stored normal consistently points triangle → box.
    const d_t2b_x = bcx - tcx;
    const d_t2b_y = bcy - tcy;
    const d_t2b_z = bcz - tcz;

    // SAT state.
    let best_overlap = Infinity;
    let best_nx = 0, best_ny = 0, best_nz = 0; // points triangle → box
    let best_source = -1;
    // Box-edge index (0..2) and triangle-edge index (0..2) for edge-cross sources.
    let best_box_edge_idx = -1;
    let best_tri_edge_idx = -1;

    /**
     * Test a candidate separating axis `(lx, ly, lz)` (need not be unit-length).
     * Returns true if a separating axis is found (overlap < 0); otherwise
     * updates `best_*` if this axis has the smallest overlap so far.
     *
     * Source:
     *   0..2  : box face normal (axis index 0..2)
     *   3     : triangle face normal
     *   4..12 : edge-cross (box_edge_idx * 3 + tri_edge_idx, offset +4)
     */
    function test_axis(lx, ly, lz, source, box_e_idx, tri_e_idx) {
        const len_sqr = lx * lx + ly * ly + lz * lz;
        if (len_sqr <= PARALLEL_EPS_SQR) return false;
        const inv_len = 1 / Math.sqrt(len_sqr);
        const ux = lx * inv_len, uy = ly * inv_len, uz = lz * inv_len;

        // Box projection.
        const box_centre_proj = ux * bcx + uy * bcy + uz * bcz;
        const r_box = projected_box_half_extent(axes, bhx, bhy, bhz, ux, uy, uz);
        const bmin = box_centre_proj - r_box;
        const bmax = box_centre_proj + r_box;

        // Triangle projection — NOT symmetric around centroid, so we take
        // the per-vertex extreme values.
        const da = ux * ax + uy * ay + uz * az;
        const db = ux * bx + uy * by + uz * bz;
        const dc = ux * cx + uy * cy + uz * cz;
        const tmin = da < db ? (da < dc ? da : dc) : (db < dc ? db : dc);
        const tmax = da > db ? (da > dc ? da : dc) : (db > dc ? db : dc);

        // Separation test.
        if (bmax < tmin || tmax < bmin) return true;

        // Compute MTV magnitude as the smaller of the two "push directions".
        // push_pos: how far we'd push the triangle in +u to escape.
        // push_neg: how far in -u.
        // For asymmetric intervals these are not equal in general.
        const push_pos = bmax - tmin;
        const push_neg = tmax - bmin;
        const overlap = push_pos < push_neg ? push_pos : push_neg;

        if (overlap < best_overlap) {
            best_overlap = overlap;
            // Canonical normal direction: triangle → box. Use the
            // box-centre-vs-triangle-centroid sign as the tiebreaker so
            // the SAT axis is canonically oriented before we negate.
            const center_dot = d_t2b_x * ux + d_t2b_y * uy + d_t2b_z * uz;
            const sign = center_dot >= 0 ? 1 : -1;
            best_nx = ux * sign;
            best_ny = uy * sign;
            best_nz = uz * sign;
            best_source = source;
            best_box_edge_idx = box_e_idx;
            best_tri_edge_idx = tri_e_idx;
        }
        return false;
    }

    // 1. Box face normals (3 axes).
    if (test_axis(axes[0], axes[1], axes[2], 0, -1, -1)) { out[3] = 0; return false; }
    if (test_axis(axes[3], axes[4], axes[5], 1, -1, -1)) { out[3] = 0; return false; }
    if (test_axis(axes[6], axes[7], axes[8], 2, -1, -1)) { out[3] = 0; return false; }

    // 2. Triangle face normal (1 axis).
    if (test_axis(tnx, tny, tnz, 3, -1, -1)) { out[3] = 0; return false; }

    // 3. Edge-edge cross products (9 axes).
    for (let i = 0; i < 3; i++) {
        const aix = axes[i * 3], aiy = axes[i * 3 + 1], aiz = axes[i * 3 + 2];
        for (let j = 0; j < 3; j++) {
            let ex, ey, ez;
            if (j === 0)      { ex = ab_x; ey = ab_y; ez = ab_z; }
            else if (j === 1) { ex = bc_x; ey = bc_y; ez = bc_z; }
            else              { ex = ca_x; ey = ca_y; ez = ca_z; }
            const cx_ax = aiy * ez - aiz * ey;
            const cy_ax = aiz * ex - aix * ez;
            const cz_ax = aix * ey - aiy * ex;
            if (test_axis(cx_ax, cy_ax, cz_ax, 4 + i * 3 + j, i, j)) { out[3] = 0; return false; }
        }
    }

    // We have overlap. Output the contact normal (triangle → box).
    const nx = best_nx, ny = best_ny, nz = best_nz;
    out[0] = nx; out[1] = ny; out[2] = nz;

    // --- Contact manifold construction ---

    if (best_source < 3) {
        // Box face axis winner: reference = box face, incident = triangle.
        return emit_box_face_manifold(out, best_source, axes, bcx, bcy, bcz, bhx, bhy, bhz,
            ax, ay, az, bx, by, bz, cx, cy, cz, tnx, tny, tnz, nx, ny, nz);
    } else if (best_source === 3) {
        // Triangle face axis winner: reference = triangle, incident = box face.
        return emit_triangle_face_manifold(out, axes, bcx, bcy, bcz, bhx, bhy, bhz,
            ax, ay, az, bx, by, bz, cx, cy, cz, tnx, tny, tnz, nx, ny, nz);
    } else {
        // Edge-cross winner: single contact at closest pair on the two edges.
        return emit_edge_cross_contact(out, axes, bcx, bcy, bcz, bhx, bhy, bhz,
            ax, ay, az, bx, by, bz, cx, cy, cz,
            best_box_edge_idx, best_tri_edge_idx,
            nx, ny, nz, best_overlap);
    }
}

// --- Manifold builders ------------------------------------------------------

/**
 * Box-face SAT winner: clip the triangle against the box's reference face
 * rectangle, then for each surviving (u, v) recover the world contact on
 * the triangle plane.
 */
function emit_box_face_manifold(
    out, ref_axis_idx, axes,
    bcx, bcy, bcz, bhx, bhy, bhz,
    ax, ay, az, bx, by, bz, cx, cy, cz,
    tnx, tny, tnz,
    nx, ny, nz
) {
    // The contact normal `n` points triangle → box. The box's outward
    // face normal at the reference face points box → triangle = -n.
    const face_out_nx = -nx;
    const face_out_ny = -ny;
    const face_out_nz = -nz;

    // Sign of the reference axis aligned with the outward normal.
    const ref_axis_x = axes[ref_axis_idx * 3];
    const ref_axis_y = axes[ref_axis_idx * 3 + 1];
    const ref_axis_z = axes[ref_axis_idx * 3 + 2];
    const ref_axis_dot = ref_axis_x * face_out_nx + ref_axis_y * face_out_ny + ref_axis_z * face_out_nz;
    const ref_axis_sign = ref_axis_dot >= 0 ? 1 : -1;
    const ref_h_along = ref_axis_idx === 0 ? bhx : (ref_axis_idx === 1 ? bhy : bhz);

    // Reference face origin (centre of the face on the +/- side of the box).
    const ref_face_ox = bcx + ref_axis_x * ref_axis_sign * ref_h_along;
    const ref_face_oy = bcy + ref_axis_y * ref_axis_sign * ref_h_along;
    const ref_face_oz = bcz + ref_axis_z * ref_axis_sign * ref_h_along;

    // Reference face's two tangent axes (u, v) and their half-extents.
    let u_axis_idx, v_axis_idx;
    if (ref_axis_idx === 0)      { u_axis_idx = 1; v_axis_idx = 2; }
    else if (ref_axis_idx === 1) { u_axis_idx = 2; v_axis_idx = 0; }
    else                         { u_axis_idx = 0; v_axis_idx = 1; }
    const ux = axes[u_axis_idx * 3],     uy = axes[u_axis_idx * 3 + 1], uz = axes[u_axis_idx * 3 + 2];
    const vx = axes[v_axis_idx * 3],     vy = axes[v_axis_idx * 3 + 1], vz = axes[v_axis_idx * 3 + 2];
    const half_u = u_axis_idx === 0 ? bhx : (u_axis_idx === 1 ? bhy : bhz);
    const half_v = v_axis_idx === 0 ? bhx : (v_axis_idx === 1 ? bhy : bhz);

    // Project the triangle's 3 vertices into the (u, v) basis on the
    // reference plane.
    const da_x = ax - ref_face_ox, da_y = ay - ref_face_oy, da_z = az - ref_face_oz;
    const db_x = bx - ref_face_ox, db_y = by - ref_face_oy, db_z = bz - ref_face_oz;
    const dc_x = cx - ref_face_ox, dc_y = cy - ref_face_oy, dc_z = cz - ref_face_oz;
    tri_uv_in[0] = da_x * ux + da_y * uy + da_z * uz;
    tri_uv_in[1] = da_x * vx + da_y * vy + da_z * vz;
    tri_uv_in[2] = db_x * ux + db_y * uy + db_z * uz;
    tri_uv_in[3] = db_x * vx + db_y * vy + db_z * vz;
    tri_uv_in[4] = dc_x * ux + dc_y * uy + dc_z * uz;
    tri_uv_in[5] = dc_x * vx + dc_y * vy + dc_z * vz;

    // Clip against rectangle |u| ≤ half_u, |v| ≤ half_v via 4 passes.
    let n = 3;
    n = clip_against_axis_uv(tri_uv_in, n, tri_uv_out, 0, half_u, true);
    if (n === 0) { out[3] = 0; return false; }
    n = clip_against_axis_uv(tri_uv_out, n, tri_uv_in, 0, -half_u, false);
    if (n === 0) { out[3] = 0; return false; }
    n = clip_against_axis_uv(tri_uv_in, n, tri_uv_out, 1, half_v, true);
    if (n === 0) { out[3] = 0; return false; }
    n = clip_against_axis_uv(tri_uv_out, n, tri_uv_in, 1, -half_v, false);
    if (n === 0) { out[3] = 0; return false; }

    // The clipped polygon is in tri_uv_in. For each surviving (u, v):
    //   X = ref_face_origin + u * u_axis + v * v_axis  (point on ref plane)
    //   Find t such that (X + t * face_out_n) is on the triangle plane:
    //     (X + t * face_out_n - A) · tn = 0
    //     t = ((A - X) · tn) / (face_out_n · tn)
    //   World point on triangle plane = X + t * face_out_n.
    //   Depth = -t (positive when triangle penetrates the box).
    const denom = face_out_nx * tnx + face_out_ny * tny + face_out_nz * tnz;
    if (Math.abs(denom) < PARALLEL_EPS_SQR) {
        // Triangle plane parallel to ref outward — degenerate. No contacts.
        out[3] = 0;
        return false;
    }
    const clipped_uv = tri_uv_in;

    let cand_count = 0;
    for (let i = 0; i < n; i++) {
        const u = clipped_uv[i * 2];
        const v = clipped_uv[i * 2 + 1];
        const xpx = ref_face_ox + u * ux + v * vx;
        const xpy = ref_face_oy + u * uy + v * vy;
        const xpz = ref_face_oz + u * uz + v * vz;

        const t = ((ax - xpx) * tnx + (ay - xpy) * tny + (az - xpz) * tnz) / denom;
        const world_tri_x = xpx + face_out_nx * t;
        const world_tri_y = xpy + face_out_ny * t;
        const world_tri_z = xpz + face_out_nz * t;
        const depth = -t;
        if (depth <= 0) continue;

        const off = cand_count * 7;
        candidates[off]     = world_tri_x;
        candidates[off + 1] = world_tri_y;
        candidates[off + 2] = world_tri_z;
        // Box-side contact = same (u, v) on ref plane.
        candidates[off + 3] = xpx;
        candidates[off + 4] = xpy;
        candidates[off + 5] = xpz;
        candidates[off + 6] = depth;
        cand_count++;
    }

    if (cand_count === 0) { out[3] = 0; return false; }
    const kept = reduce_contacts(cand_count);
    out[3] = kept;
    for (let k = 0; k < kept; k++) {
        const src = k * 7;
        const base = 4 + k * CONTACT_STRIDE;
        out[base]     = candidates[src];
        out[base + 1] = candidates[src + 1];
        out[base + 2] = candidates[src + 2];
        out[base + 3] = candidates[src + 3];
        out[base + 4] = candidates[src + 4];
        out[base + 5] = candidates[src + 5];
        out[base + 6] = candidates[src + 6];
    }
    return true;
}

/**
 * Triangle-face SAT winner: reference = triangle, incident = the box face
 * most antiparallel to the contact normal. Clip the box face quad against
 * the triangle's 3 edges, then recover world contacts on the box face plane.
 */
function emit_triangle_face_manifold(
    out, axes,
    bcx, bcy, bcz, bhx, bhy, bhz,
    ax, ay, az, bx, by, bz, cx, cy, cz,
    tnx, tny, tnz,
    nx, ny, nz
) {
    // Reference = triangle. Its outward face normal points from the triangle
    // surface toward the box — i.e. the contact normal `n` (triangle → box),
    // which the SAT already sign-canonicalised. The incident box face is then
    // the one whose outward normal is most antiparallel to this (the box face
    // actually facing the triangle), and the per-point depth below measures the
    // penetration of THAT face — not the box's far face.
    //
    // (The earlier `-n` here pointed the reference normal into the terrain, so
    // the incident picker selected the box's FAR face and the plane-to-plane
    // depth ballooned to ~the box's thickness for any tilted box — catapulting
    // rocking boxes off heightmaps. See box_triangle_contact.spec.js
    // "tilted box, shallow corner".)
    const ref_out_x = nx;
    const ref_out_y = ny;
    const ref_out_z = nz;

    // Pick the incident box face: the one whose outward axis is most
    // antiparallel to ref_out.
    let inc_axis_idx = 0;
    let inc_axis_sign = 1;
    let max_anti = -Infinity;
    for (let i = 0; i < 3; i++) {
        const iax = axes[i * 3], iay = axes[i * 3 + 1], iaz = axes[i * 3 + 2];
        const d_pos = iax * ref_out_x + iay * ref_out_y + iaz * ref_out_z;
        if (-d_pos > max_anti) { max_anti = -d_pos; inc_axis_idx = i; inc_axis_sign = -1; }
        if ( d_pos > max_anti) { max_anti =  d_pos; inc_axis_idx = i; inc_axis_sign =  1; }
    }
    const inc_outward_sign = -inc_axis_sign;
    const inc_h_along = inc_axis_idx === 0 ? bhx : (inc_axis_idx === 1 ? bhy : bhz);
    const inc_face_cx = bcx + axes[inc_axis_idx * 3]     * inc_outward_sign * inc_h_along;
    const inc_face_cy = bcy + axes[inc_axis_idx * 3 + 1] * inc_outward_sign * inc_h_along;
    const inc_face_cz = bcz + axes[inc_axis_idx * 3 + 2] * inc_outward_sign * inc_h_along;

    // Box face's two tangent axes in world.
    let i_u_idx, i_v_idx;
    if (inc_axis_idx === 0)      { i_u_idx = 1; i_v_idx = 2; }
    else if (inc_axis_idx === 1) { i_u_idx = 2; i_v_idx = 0; }
    else                         { i_u_idx = 0; i_v_idx = 1; }
    const iux = axes[i_u_idx * 3], iuy = axes[i_u_idx * 3 + 1], iuz = axes[i_u_idx * 3 + 2];
    const ivx = axes[i_v_idx * 3], ivy = axes[i_v_idx * 3 + 1], ivz = axes[i_v_idx * 3 + 2];
    const i_half_u = i_u_idx === 0 ? bhx : (i_u_idx === 1 ? bhy : bhz);
    const i_half_v = i_v_idx === 0 ? bhx : (i_v_idx === 1 ? bhy : bhz);

    // Build a 2D basis on the triangle plane. Use AB direction as `u`,
    // and `n_uv = tn × u` as `v` so the basis is orthonormal.
    const ab_x_w = bx - ax, ab_y_w = by - ay, ab_z_w = bz - az;
    const ab_len_sqr = ab_x_w * ab_x_w + ab_y_w * ab_y_w + ab_z_w * ab_z_w;
    if (ab_len_sqr <= PARALLEL_EPS_SQR) { out[3] = 0; return false; }
    const ab_inv = 1 / Math.sqrt(ab_len_sqr);
    const tux = ab_x_w * ab_inv, tuy = ab_y_w * ab_inv, tuz = ab_z_w * ab_inv;
    // v = tn × u (using the geometric face normal as the "up").
    const tvx = tny * tuz - tnz * tuy;
    const tvy = tnz * tux - tnx * tuz;
    const tvz = tnx * tuy - tny * tux;

    // Project triangle vertices to (u, v). A is the origin of the
    // 2D basis — A_uv = (0, 0); B is along the u axis by construction
    // so B_uv = (|AB|, 0); C is computed normally.
    const ac_x_w = cx - ax, ac_y_w = cy - ay, ac_z_w = cz - az;
    const b_u = ab_x_w * tux + ab_y_w * tuy + ab_z_w * tuz; // = |AB|
    const c_u = ac_x_w * tux + ac_y_w * tuy + ac_z_w * tuz;
    const c_v = ac_x_w * tvx + ac_y_w * tvy + ac_z_w * tvz;

    // Triangle's 3 edges as 2D half-planes in (u, v). For each edge, the
    // outward normal must point AWAY from the opposite vertex.
    // Edge AB: from A=(0,0) to B=(b_u, 0). Direction = (1, 0). Outward
    //   normal perpendicular = (0, 1) or (0, -1). Choose sign so it
    //   points away from C (= sign(-c_v) gives 1 if c_v < 0).
    const ab_n_v = c_v >= 0 ? -1 : 1;
    // Edge BC: from B=(b_u, 0) to C=(c_u, c_v). Direction = (c_u - b_u, c_v).
    //   Outward perpendicular: rotate by ±90° (depending on triangle winding).
    //   Outward direction must point away from A=(0,0).
    const bc_dir_u = c_u - b_u, bc_dir_v = c_v - 0;
    // Two perpendiculars: (-bc_dir_v, bc_dir_u) and (bc_dir_v, -bc_dir_u).
    // Pick the one with positive dot vs (A - midpoint of BC) negated.
    const bc_mid_u = (b_u + c_u) * 0.5;
    const bc_mid_v = c_v * 0.5;
    const to_a_u_from_bc = 0 - bc_mid_u;
    const to_a_v_from_bc = 0 - bc_mid_v;
    // We want outward = -direction_to_A.
    let bc_n_u = -bc_dir_v, bc_n_v = bc_dir_u;
    if (bc_n_u * to_a_u_from_bc + bc_n_v * to_a_v_from_bc > 0) {
        bc_n_u = -bc_n_u; bc_n_v = -bc_n_v;
    }
    // Normalise so the clip threshold is "distance ≤ 0" in 2D.
    const bc_n_len = Math.sqrt(bc_n_u * bc_n_u + bc_n_v * bc_n_v);
    if (bc_n_len > 0) { bc_n_u /= bc_n_len; bc_n_v /= bc_n_len; }
    // Edge CA: from C to A. Direction = (-c_u, -c_v). Outward must point
    //   away from B.
    const ca_dir_u = 0 - c_u, ca_dir_v = 0 - c_v;
    const ca_mid_u = c_u * 0.5;
    const ca_mid_v = c_v * 0.5;
    const to_b_u_from_ca = b_u - ca_mid_u;
    const to_b_v_from_ca = 0 - ca_mid_v;
    let ca_n_u = -ca_dir_v, ca_n_v = ca_dir_u;
    if (ca_n_u * to_b_u_from_ca + ca_n_v * to_b_v_from_ca > 0) {
        ca_n_u = -ca_n_u; ca_n_v = -ca_n_v;
    }
    const ca_n_len = Math.sqrt(ca_n_u * ca_n_u + ca_n_v * ca_n_v);
    if (ca_n_len > 0) { ca_n_u /= ca_n_len; ca_n_v /= ca_n_len; }

    // Build the box-face polygon (4 corners) and project to (u, v) on
    // the triangle plane.
    const signs_u = [ 1,  1, -1, -1];
    const signs_v = [-1,  1,  1, -1];
    for (let i = 0; i < 4; i++) {
        const su = signs_u[i] * i_half_u;
        const sv = signs_v[i] * i_half_v;
        const wx = inc_face_cx + iux * su + ivx * sv;
        const wy = inc_face_cy + iuy * su + ivy * sv;
        const wz = inc_face_cz + iuz * su + ivz * sv;
        const dx = wx - ax, dy = wy - ay, dz = wz - az;
        box_face_corners_uv_in[i * 2]     = dx * tux + dy * tuy + dz * tuz;
        box_face_corners_uv_in[i * 2 + 1] = dx * tvx + dy * tvy + dz * tvz;
    }

    // Clip the quad against the 3 triangle-edge half-planes.
    let np = 4;
    // Edge AB: half-plane (p.v - 0) * ab_n_v <= 0 → coord_idx = 1, bound = 0, keep_below if ab_n_v=+1.
    np = clip_against_half_plane_uv(box_face_corners_uv_in, np, box_face_corners_uv_out, 0, 0, 0, ab_n_v);
    if (np === 0) { out[3] = 0; return false; }
    np = clip_against_half_plane_uv(box_face_corners_uv_out, np, box_face_corners_uv_in, b_u, 0, bc_n_u, bc_n_v);
    if (np === 0) { out[3] = 0; return false; }
    np = clip_against_half_plane_uv(box_face_corners_uv_in, np, box_face_corners_uv_out, c_u, c_v, ca_n_u, ca_n_v);
    if (np === 0) { out[3] = 0; return false; }

    // Survivors are in box_face_corners_uv_out. For each:
    //   X = A + u * tu + v * tv (point on triangle plane)
    //   Find t such that X + t * ref_out is on box face plane:
    //     (X + t * ref_out - inc_face_c) · inc_out = 0
    //   Where inc_out = inc_axes[inc_axis_idx] * inc_outward_sign.
    const inc_out_x = axes[inc_axis_idx * 3]     * inc_outward_sign;
    const inc_out_y = axes[inc_axis_idx * 3 + 1] * inc_outward_sign;
    const inc_out_z = axes[inc_axis_idx * 3 + 2] * inc_outward_sign;
    const denom = ref_out_x * inc_out_x + ref_out_y * inc_out_y + ref_out_z * inc_out_z;
    if (Math.abs(denom) < PARALLEL_EPS_SQR) { out[3] = 0; return false; }
    const clipped_uv = box_face_corners_uv_out;

    let cand_count = 0;
    for (let i = 0; i < np; i++) {
        const u = clipped_uv[i * 2];
        const v = clipped_uv[i * 2 + 1];
        const xpx = ax + u * tux + v * tvx;
        const xpy = ay + u * tuy + v * tvy;
        const xpz = az + u * tuz + v * tvz;

        const num_x = inc_face_cx - xpx;
        const num_y = inc_face_cy - xpy;
        const num_z = inc_face_cz - xpz;
        const t = (num_x * inc_out_x + num_y * inc_out_y + num_z * inc_out_z) / denom;
        const world_box_x = xpx + ref_out_x * t;
        const world_box_y = xpy + ref_out_y * t;
        const world_box_z = xpz + ref_out_z * t;
        // Depth: how far the box face has penetrated past the triangle
        // plane, measured along ref_out. depth = -t.
        const depth = -t;
        if (depth <= 0) continue;

        const off = cand_count * 7;
        // Triangle-side contact: the clipped (u, v) point on the
        // triangle plane.
        candidates[off]     = xpx;
        candidates[off + 1] = xpy;
        candidates[off + 2] = xpz;
        candidates[off + 3] = world_box_x;
        candidates[off + 4] = world_box_y;
        candidates[off + 5] = world_box_z;
        candidates[off + 6] = depth;
        cand_count++;
    }

    if (cand_count === 0) { out[3] = 0; return false; }
    const kept = reduce_contacts(cand_count);
    out[3] = kept;
    for (let k = 0; k < kept; k++) {
        const src = k * 7;
        const base = 4 + k * CONTACT_STRIDE;
        out[base]     = candidates[src];
        out[base + 1] = candidates[src + 1];
        out[base + 2] = candidates[src + 2];
        out[base + 3] = candidates[src + 3];
        out[base + 4] = candidates[src + 4];
        out[base + 5] = candidates[src + 5];
        out[base + 6] = candidates[src + 6];
    }
    return true;
}

/**
 * Edge-cross SAT winner: emit a single contact at the closest pair of
 * points on (a parallel-translated) box edge × the triangle edge.
 *
 * The box has 12 edges, but along the winning box-axis direction `i`
 * there are 4 parallel edges (the four lines on the box surface
 * parallel to box-axis i). The "right" edge for the contact is the
 * one whose endpoints span the triangle edge — practically, pick the
 * box edge most aligned with the SAT separation: the one whose centre
 * (when translated perpendicular to the SAT axis) is closest to the
 * triangle edge.
 *
 * Procedure:
 *   1. Construct the box edge as the line through box centre +
 *      (the +/- of the other two box half-extents pushing to the
 *      "near" corner determined by the SAT-axis sign).
 *   2. Construct the triangle edge as the segment.
 *   3. Closest-pair via `line3_closest_points_segment_segment`.
 *   4. Contact = midpoint, depth = best_overlap.
 */
function emit_edge_cross_contact(
    out, axes,
    bcx, bcy, bcz, bhx, bhy, bhz,
    ax, ay, az, bx, by, bz, cx, cy, cz,
    box_edge_idx, tri_edge_idx,
    nx, ny, nz,
    overlap
) {
    // Box edge direction: axis index `box_edge_idx`.
    const edx = axes[box_edge_idx * 3];
    const edy = axes[box_edge_idx * 3 + 1];
    const edz = axes[box_edge_idx * 3 + 2];
    const edge_half = box_edge_idx === 0 ? bhx : (box_edge_idx === 1 ? bhy : bhz);

    // The OTHER two box axes determine which of the four parallel edges
    // we pick. Use the sign of the contact normal projected onto each
    // perpendicular axis to choose the edge closest to the contact.
    let u_idx, v_idx;
    if (box_edge_idx === 0)      { u_idx = 1; v_idx = 2; }
    else if (box_edge_idx === 1) { u_idx = 2; v_idx = 0; }
    else                         { u_idx = 0; v_idx = 1; }
    const u_ax_x = axes[u_idx * 3], u_ax_y = axes[u_idx * 3 + 1], u_ax_z = axes[u_idx * 3 + 2];
    const v_ax_x = axes[v_idx * 3], v_ax_y = axes[v_idx * 3 + 1], v_ax_z = axes[v_idx * 3 + 2];
    const half_u = u_idx === 0 ? bhx : (u_idx === 1 ? bhy : bhz);
    const half_v = v_idx === 0 ? bhx : (v_idx === 1 ? bhy : bhz);

    // The contact normal `n` points triangle → box. The box edge
    // closest to the triangle is the one whose corner (in u, v) is in
    // the -n direction. Decompose -n into the u and v components.
    const minus_n_dot_u = -(nx * u_ax_x + ny * u_ax_y + nz * u_ax_z);
    const minus_n_dot_v = -(nx * v_ax_x + ny * v_ax_y + nz * v_ax_z);
    const u_sign = minus_n_dot_u >= 0 ? 1 : -1;
    const v_sign = minus_n_dot_v >= 0 ? 1 : -1;

    // Box edge endpoints in world: from corner_lo to corner_hi along
    // the chosen box axis.
    const corner_cx = bcx + u_ax_x * u_sign * half_u + v_ax_x * v_sign * half_v;
    const corner_cy = bcy + u_ax_y * u_sign * half_u + v_ax_y * v_sign * half_v;
    const corner_cz = bcz + u_ax_z * u_sign * half_u + v_ax_z * v_sign * half_v;
    const box_p1x = corner_cx - edx * edge_half, box_p1y = corner_cy - edy * edge_half, box_p1z = corner_cz - edz * edge_half;
    const box_p2x = corner_cx + edx * edge_half, box_p2y = corner_cy + edy * edge_half, box_p2z = corner_cz + edz * edge_half;

    // Triangle edge endpoints.
    let tri_p1x, tri_p1y, tri_p1z, tri_p2x, tri_p2y, tri_p2z;
    if (tri_edge_idx === 0) {
        tri_p1x = ax; tri_p1y = ay; tri_p1z = az;
        tri_p2x = bx; tri_p2y = by; tri_p2z = bz;
    } else if (tri_edge_idx === 1) {
        tri_p1x = bx; tri_p1y = by; tri_p1z = bz;
        tri_p2x = cx; tri_p2y = cy; tri_p2z = cz;
    } else {
        tri_p1x = cx; tri_p1y = cy; tri_p1z = cz;
        tri_p2x = ax; tri_p2y = ay; tri_p2z = az;
    }

    // Closest points on the two segments.
    line3_closest_points_segment_segment(
        closest_pair_st,
        box_p1x, box_p1y, box_p1z, box_p2x, box_p2y, box_p2z,
        tri_p1x, tri_p1y, tri_p1z, tri_p2x, tri_p2y, tri_p2z
    );
    const s = closest_pair_st[0], t = closest_pair_st[1];
    const closest_box_x = box_p1x + s * (box_p2x - box_p1x);
    const closest_box_y = box_p1y + s * (box_p2y - box_p1y);
    const closest_box_z = box_p1z + s * (box_p2z - box_p1z);
    const closest_tri_x = tri_p1x + t * (tri_p2x - tri_p1x);
    const closest_tri_y = tri_p1y + t * (tri_p2y - tri_p1y);
    const closest_tri_z = tri_p1z + t * (tri_p2z - tri_p1z);

    out[3] = 1;
    const base = 4;
    out[base]     = closest_tri_x;
    out[base + 1] = closest_tri_y;
    out[base + 2] = closest_tri_z;
    out[base + 3] = closest_box_x;
    out[base + 4] = closest_box_y;
    out[base + 5] = closest_box_z;
    out[base + 6] = overlap;
    return true;
}
