PURPOSE

Pure geometry helpers for hull-accurate player hitboxes. Provides a per-frame transform from a unit-space hull polygon to world space (transformHullPoly) and three narrow-phase predicates against a convex polygon (pointInConvexPolygon, circleIntersectsConvexPolygon, nearestEdgeNormal) — the building blocks the collision resolver uses for bullet-vs-ship and enemy-body-vs-ship checks against the ship’s hull instead of a single bounding radius.

OWNS

  • Vec2 local type alias ([number, number]).
  • Module-private _hullOut shared output buffer (Vec2[]) — pre-allocated once, grown on demand, reused every frame by transformHullPoly.
  • Exported functions:
    • transformHullPoly(baseVerts, tx, ty, angle, scale) — applies scale → rotate → translate to a unit-space convex polygon and returns the shared _hullOut buffer (sin/cos computed once, length trimmed if the hull’s vertex count shrank).
    • pointInConvexPolygon(px, py, verts) — boolean CCW cross-product winding test.
    • circleIntersectsConvexPolygon(cx, cy, cr, verts) — boolean center-in-polygon plus per-edge nearest-point-on-segment distance test against the radius (squared-distance comparison, no sqrt on the hot path).
    • nearestEdgeNormal(px, py, verts) — returns { nx, ny, dist } where the unit normal points from the nearest edge toward the external point, with a CCW edge-perpendicular fallback when the point is closer than 0.001 to the surface.

READS FROM

  • Math.cos, Math.sin, Math.hypot from the standard library.
  • The vertex arrays passed in by the caller — no imports, no module-level configuration, no global state.

PUSHES TO

  • engine/bridge consumes transformHullPoly once per frame to recompute ship.hullPoly from the per-class base verts returned by getHullHitbox, the ship’s (x, y, angle), and hullScale * HULL_COLLISION_SHRINK (0.80 shrink so the hitbox sits inside the visible sprite). The debug overlay path calls it a second time with the un-shrunk scale for the yellow hull-outline draw.
  • engine/combat/collision-resolver consumes circleIntersectsConvexPolygon as the narrow phase for both resolveEnemyBulletPlayerCollisions (enemy bullet against ship.hullPoly after a broad-circle gate) and resolveShipEnemyBodyCollisions (enemy body against ship.hullPoly).
  • nearestEdgeNormal is exported for bounce/separation physics use; no in-tree call site consumes it in the current build.

DOES NOT

  • Does not look up, store, or own the unit-space hull polygons themselves — those live in data/hull-hitboxes (HULL_HITBOXES map + getHullHitbox accessor with the octagon fallback).
  • Does not handle non-convex polygons. All functions assume convex CCW geometry; concave hulls would silently fail the winding test or miss segments.
  • Does not validate winding order, vertex count, or NaN inputs — caller is responsible for the contract (convex, CCW, ≥3 verts).
  • Does not allocate per-call output for transformHullPoly — returns a shared module-level buffer that must be consumed before the next call (single-threaded contract).
  • Does not perform polygon-vs-polygon, swept-circle, ray-vs-polygon, or continuous collision — only point-in-polygon, circle-vs-polygon, and nearest-edge queries.
  • Does not run a broad phase, spatial-grid lookup, or AABB pre-filter — the collision resolver applies a bounding-circle gate before calling in.
  • Does not apply hit-shrink, padding, or per-rarity scaling — receives a pre-computed scale and the caller is responsible for any shrink multiplier.
  • Does not consume or write any signal-bus events, telemetry, or audio cues.
  • Does not depend on Rapier; this is plain JS geometry that runs independently of the physics simulation.

Signals

None fired, none watched. Pure synchronous geometry functions.

Entry points

  • transformHullPoly(baseVerts, tx, ty, angle, scale) — called from engine/bridge to refresh ship.hullPoly each frame, and from the bridge’s debug-overlay draw path for the hitbox outline.
  • pointInConvexPolygon(px, py, verts) — invoked internally by circleIntersectsConvexPolygon; also available as a public export.
  • circleIntersectsConvexPolygon(cx, cy, cr, verts) — called from collision-resolver for bullet-vs-ship and body-vs-ship narrow phase.
  • nearestEdgeNormal(px, py, verts) — exported for push-out / bounce use; no in-tree consumer at the moment.

Pattern notes

  • The polygons must be convex and wound counter-clockwise. pointInConvexPolygon uses the cross-product sign: if any edge yields a negative cross with the test-point vector, the point is to the right of that edge and therefore outside.
  • transformHullPoly returns a shared _hullOut buffer that is reused on the next call. Callers must consume or copy the result before invoking the function again. Buffer growth is one-shot — it expands when the hull’s vertex count exceeds the current capacity and trims length down when a smaller hull is loaded, but never shrinks the allocation.
  • circleIntersectsConvexPolygon decomposes into three cases in order: (1) center inside the polygon, (2) the squared distance from the circle center to any edge segment is less than the squared radius, (3) otherwise no hit. Edge case 2 clamps the parametric edge projection t to [0, 1] so corners are handled correctly.
  • All distance comparisons in the circle-vs-polygon hot path are squared (cr2, dx*dx + dy*dy) to avoid sqrt. Only nearestEdgeNormal calls Math.hypot, since it must return an actual distance and a unit-length normal.
  • nearestEdgeNormal’s degenerate-case branch (d > 0.001) handles the point-exactly-on-edge case by emitting the CCW edge-perpendicular normal (-aby/edgeLen, abx/edgeLen), which points outward for a CCW polygon.
  • The bridge applies a HULL_COLLISION_SHRINK = 0.80 multiplier to the scale before calling transformHullPoly, so the collision polygon sits ~20% inside the rendered sprite. This is owned by the bridge, not by this module.
  • These helpers are sized for ~12-vertex auto-generated hulls. Each test is O(n) where n is the vertex count; on the hot path that is ~12 multiplies for pointInConvexPolygon and ~30 multiplies plus a comparison for circleIntersectsConvexPolygon.
  • Ray-vs-polygon is not implemented. Where the broader combat system needs hit-scan or beam intersection, it uses point-to-line-distance helpers in engine/combat (pointToLineDistance) rather than a true polygon ray test.