Files
HexTrails/labs/car-loop/motion.js
T

116 lines
5.4 KiB
JavaScript

// Shared distance-based trajectories and local traffic clearance, in canvas pixels.
export function measure(points) {
const lengths = [0];
for (let i = 1; i < points.length; i++) lengths.push(lengths[i - 1] + Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y));
return { points, lengths, length: lengths.at(-1) };
}
export function at(path, distance) {
const d = Math.max(0, Math.min(path.length, distance));
let i = path.lengths.findIndex((v, index) => index && v >= d);
if (i < 1) i = path.points.length - 1;
const a = path.points[i - 1], b = path.points[i], len = path.lengths[i] - path.lengths[i - 1];
const t = len ? (d - path.lengths[i - 1]) / len : 0;
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, angle: Math.atan2(b.y - a.y, b.x - a.x) };
}
export function pointSegmentDistance(p, a, b) {
const dx = b.x - a.x, dy = b.y - a.y, length = dx * dx + dy * dy;
const t = length ? Math.max(0, Math.min(1, ((p.x-a.x)*dx+(p.y-a.y)*dy)/length)) : 0;
return Math.hypot(p.x-a.x-dx*t,p.y-a.y-dy*t);
}
export function hasPriority(a, b) {
if (a.yieldingTo === b) return false;
if (b.yieldingTo === a) return true;
return a.order < b.order;
}
// A car reserves road conflicts only after departure. Parked cars are physical
// obstacles, not reservations for every road they may eventually travel through.
export function activeCorridors(traffic, actor) {
return traffic.filter(other => other !== actor
&& ['board', 'waiting', 'joining'].includes(other.stage)
&& other.distance < other.boardLength && hasPriority(other, actor));
}
// Only the untraveled part of a priority vehicle's route reserves a conflict area.
// Other cars travel up to that area, wait there, and resume as it clears.
export function advance(path, current, target, obstacles, corridors, clearance) {
let distance = current;
while (distance < target - 1e-6) {
const next = Math.min(target, distance + 1.5), p = at(path, next);
if (obstacles.some(o => Math.hypot(p.x-o.x,p.y-o.y) < clearance)) break;
let conflict = false;
for (const corridor of corridors) {
let a = at(corridor.path, corridor.distance);
for (let i = 1; i < corridor.path.points.length; i++) {
if (corridor.path.lengths[i] <= corridor.distance) continue;
if (corridor.path.lengths[i-1] > corridor.boardLength + clearance) break;
const b = corridor.path.points[i];
if (pointSegmentDistance(p,a,b) < clearance) { conflict = true; break; }
a = b;
}
if (conflict) break;
}
if (conflict) break;
distance = next;
}
return distance;
}
// Stable passage priority respects the parked-car dependency graph, avoiding
// circular reservations when several intersecting cars are clicked together.
export function passageOrder(board, cars, trace) {
let remaining = [...cars], rank = 0;
const order = new Map();
while (remaining.length) {
const free = remaining.filter(car => trace(board, car, remaining).status === 'exit');
if (!free.length) { remaining.forEach(car => order.set(car.id, rank++)); break; }
const ids = new Set(free.map(car => car.id));
free.forEach(car => order.set(car.id, rank++));
remaining = remaining.filter(car => !ids.has(car.id));
}
return order;
}
// Back out of a mutual standstill, using the same swept body clearance as forward travel.
// The yielding car keeps its priority override until the other has left the board.
export function resolveGridlock(traffic, fixedObstacles, positionOf, clearance, step) {
const reversed = new Set();
const internal = traffic.filter(t => t.stage === 'board');
for (const actor of internal) {
if (actor.yieldingTo && !internal.includes(actor.yieldingTo)) {
actor.yieldingTo = null; actor.retreating = false;
}
}
const retreat = actor => {
const obstacles = [...fixedObstacles, ...traffic.filter(o => o !== actor && o.stage !== 'done').map(positionOf)];
let distance = actor.distance;
const target = Math.max(0, distance - step);
while (distance > target + 1e-6) {
const next = Math.max(target, distance - 1.5), p = at(actor.path, next);
if (obstacles.some(o => Math.hypot(p.x - o.x, p.y - o.y) < clearance)) break;
distance = next;
}
return distance;
};
for (let i = 0; i < internal.length; i++) for (let j = i + 1; j < internal.length; j++) {
const a = internal[i], b = internal[j];
if (a.yieldingTo || b.yieldingTo || !a.stopped || !b.stopped || Math.min(a.blockedFor || 0, b.blockedFor || 0) < .5) continue;
const p = positionOf(a), q = positionOf(b);
if (Math.hypot(p.x - q.x, p.y - q.y) > clearance * 1.6) continue;
const candidates = hasPriority(a, b) ? [b, a] : [a, b];
const loser = candidates.find(t => retreat(t) < t.distance - .01);
if (loser) { loser.yieldingTo = loser === a ? b : a; loser.retreating = true; }
}
for (const actor of internal) {
if (!actor.retreating || !actor.yieldingTo) continue;
// A short probe tests whether the whole body has cleared the winning route.
const p = positionOf(actor), probe = measure([p, { x: p.x + .01, y: p.y }]);
if (advance(probe, 0, probe.length, [], [actor.yieldingTo], clearance + 3) >= probe.length) {
actor.retreating = false;
continue;
}
const next = retreat(actor);
actor.distance = next; actor.stopped = true; reversed.add(actor);
if (next <= 0) actor.retreating = false;
}
return reversed;
}