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.
// 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;
}
