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
Vec2local type alias ([number, number]).- Module-private
_hullOutshared output buffer (Vec2[]) — pre-allocated once, grown on demand, reused every frame bytransformHullPoly. - Exported functions:
transformHullPoly(baseVerts, tx, ty, angle, scale)— applies scale → rotate → translate to a unit-space convex polygon and returns the shared_hullOutbuffer (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, nosqrton 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.hypotfrom the standard library.- The vertex arrays passed in by the caller — no imports, no module-level configuration, no global state.
PUSHES TO
engine/bridgeconsumestransformHullPolyonce per frame to recomputeship.hullPolyfrom the per-class base verts returned bygetHullHitbox, the ship’s(x, y, angle), andhullScale * 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-resolverconsumescircleIntersectsConvexPolygonas the narrow phase for bothresolveEnemyBulletPlayerCollisions(enemy bullet againstship.hullPolyafter a broad-circle gate) andresolveShipEnemyBodyCollisions(enemy body againstship.hullPoly).nearestEdgeNormalis 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_HITBOXESmap +getHullHitboxaccessor 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
scaleand 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 fromengine/bridgeto refreshship.hullPolyeach frame, and from the bridge’s debug-overlay draw path for the hitbox outline.pointInConvexPolygon(px, py, verts)— invoked internally bycircleIntersectsConvexPolygon; also available as a public export.circleIntersectsConvexPolygon(cx, cy, cr, verts)— called fromcollision-resolverfor 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.
pointInConvexPolygonuses 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. transformHullPolyreturns a shared_hullOutbuffer 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 trimslengthdown when a smaller hull is loaded, but never shrinks the allocation.circleIntersectsConvexPolygondecomposes 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 projectiontto[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 avoidsqrt. OnlynearestEdgeNormalcallsMath.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.80multiplier to the scale before callingtransformHullPoly, 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
pointInConvexPolygonand ~30 multiplies plus a comparison forcircleIntersectsConvexPolygon. - 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.