Back to Napplet Machines V8
SOURCE / PINNED RELEASE

Made of little things.

Napplet Machines V8

Release
b3f614e739f0…
Author-recorded commit
534f19acb4ba…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b5572362d4a

Archive hash verified: 0f7feb97e2c3d336…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/contacts.ts
// Small planar rigid bodies: metres, seconds, and relative mass units.
// The same yaw inertia is used by tires, wall hits and car-to-car contacts.
export const yawInertia = (mass: number) => mass * 1.35;
type XY = { x: number; y: number };
type Chassis = XY & { angle: number; vx: number; vy: number; spin: number };
const cross = (a: XY, b: XY) => a.x * b.y - a.y * b.x;
const dot = (a: XY, b: XY) => a.x * b.x + a.y * b.y;
function corners(b: Chassis): XY[] {
  const c = Math.cos(b.angle),
    s = Math.sin(b.angle);
  return [
    [-1.275, -0.725],
    [1.275, -0.725],
    [1.275, 0.725],
    [-1.275, 0.725],
  ].map(([x, y]) => ({ x: b.x + x * c - y * s, y: b.y + x * s + y * c }));
}
export function chassisContact(a: Chassis, b: Chassis) {
  if (Math.hypot(b.x - a.x, b.y - a.y) > 3) return null;
  const aa = corners(a),
    bb = corners(b);
  let depth = Infinity,
    normal = { x: 1, y: 0 };
  for (const angle of [a.angle, b.angle])
    for (const offset of [0, Math.PI / 2]) {
      const n = { x: Math.cos(angle + offset), y: Math.sin(angle + offset) };
      const ap = aa.map((p) => dot(p, n)),
        bp = bb.map((p) => dot(p, n));
      const overlap =
        Math.min(Math.max(...ap), Math.max(...bp)) -
        Math.max(Math.min(...ap), Math.min(...bp));
      if (overlap <= 0) return null;
      if (overlap < depth) {
        depth = overlap;
        const sign = (b.x - a.x) * n.x + (b.y - a.y) * n.y < 0 ? -1 : 1;
        normal = { x: n.x * sign, y: n.y * sign };
      }
    }
  // Clip the intersection polygon. Its centroid approximates the contact patch,
  // making a square rear impact symmetric instead of picking one bumper corner.
  let polygon = aa;
  for (let i = 0; i < 4; i++) {
    const p = bb[i],
      q = bb[(i + 1) % 4],
      edge = { x: q.x - p.x, y: q.y - p.y };
    const side = (v: XY) => cross(edge, { x: v.x - p.x, y: v.y - p.y });
    const clipped: XY[] = [];
    for (let j = 0; j < polygon.length; j++) {
      const u = polygon[j],
        v = polygon[(j + 1) % polygon.length],
        du = side(u),
        dv = side(v);
      if (du >= 0) clipped.push(u);
      if (du >= 0 !== dv >= 0) {
        const t = du / (du - dv);
        clipped.push({ x: u.x + (v.x - u.x) * t, y: u.y + (v.y - u.y) * t });
      }
    }
    polygon = clipped;
  }
  const point = polygon.length
    ? {
        x: polygon.reduce((sum, p) => sum + p.x, 0) / polygon.length,
        y: polygon.reduce((sum, p) => sum + p.y, 0) / polygon.length,
      }
    : { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
  return { nx: normal.x, ny: normal.y, depth, ...point };
}
export function resolveCars(a: Chassis, b: Chassis, ma: number, mb: number) {
  const hit = chassisContact(a, b);
  if (!hit) return;
  const n = { x: hit.nx, y: hit.ny },
    t = { x: -n.y, y: n.x };
  const ra = { x: hit.x - a.x, y: hit.y - a.y },
    rb = { x: hit.x - b.x, y: hit.y - b.y };
  const ia = 1 / yawInertia(ma),
    ib = 1 / yawInertia(mb);
  const relative = (axis: XY) =>
    (b.vx - b.spin * rb.y - a.vx + a.spin * ra.y) * axis.x +
    (b.vy + b.spin * rb.x - a.vy - a.spin * ra.x) * axis.y;
  const effectiveMass = (axis: XY) =>
    1 / ma + 1 / mb + cross(ra, axis) ** 2 * ia + cross(rb, axis) ** 2 * ib;
  const apply = (axis: XY, impulse: number) => {
    a.vx -= (axis.x * impulse) / ma;
    a.vy -= (axis.y * impulse) / ma;
    b.vx += (axis.x * impulse) / mb;
    b.vy += (axis.y * impulse) / mb;
    a.spin -= cross(ra, axis) * impulse * ia;
    b.spin += cross(rb, axis) * impulse * ib;
  };
  const closing = relative(n);
  if (closing < 0) {
    const impulse = (-(1 + 0.38) * closing) / effectiveMass(n);
    const sliding = relative(t); // Pre-impact slip: a bumper push doesn't invent friction.
    apply(n, impulse);
    const friction = Math.max(
      -impulse * 0.28,
      Math.min(impulse * 0.28, -sliding / effectiveMass(t)),
    );
    apply(t, friction);
  }
  const correction =
    (Math.max(0, hit.depth - 0.015) * 0.85) / (1 / ma + 1 / mb);
  a.x -= (n.x * correction) / ma;
  a.y -= (n.y * correction) / ma;
  b.x += (n.x * correction) / mb;
  b.y += (n.y * correction) / mb;
}