feat(airport): draw the entry channel to the tray edge
Board-edge runways reach the tray outline with a threshold gate, fixed apron slabs mark each side, and a connected side lights the whole plane-to-stand channel in the neutral hint colour.
This commit is contained in:
+38
-6
@@ -1,6 +1,6 @@
|
||||
// Airport rules. Rendering never supplies occupancy or chooses a destination.
|
||||
import { DIRS, key, opposite, rotateBoard, trace, validateLevel as validateBase, canRotate } from '../../src/core.js';
|
||||
import { PORTS, position, pathPoints, sweepPath } from '../../src/path-math.js';
|
||||
import { PORTS, position, pathPoints, sweepPath, hexPoints } from '../../src/path-math.js';
|
||||
|
||||
// Flat-top edges in screen space. Compass names hid that waiting bays sit on the six sides.
|
||||
export const SIDES = ['上方', '右上方', '右下方', '下方', '左下方', '左上方'];
|
||||
@@ -10,18 +10,50 @@ export function boardRadius(cells) {
|
||||
export function apronDistance(radius) {
|
||||
return Math.sqrt(3) * (radius + 0.5) + 1.15;
|
||||
}
|
||||
// The cell cluster's outline is pointy-top: its six wide flat sides sit half a
|
||||
// step (30°) from the port normals, so bays rest on those sides, never on a point.
|
||||
export function bayNormal(side) {
|
||||
const n = PORTS[side], length = Math.hypot(n.x, n.y), x = n.x / length, y = n.y / length;
|
||||
const c = Math.cos(Math.PI / 6), s = Math.sin(Math.PI / 6);
|
||||
return { x: x * c - y * s, y: x * s + y * c };
|
||||
}
|
||||
export function waitingPose(plane, planes, radius) {
|
||||
const group = planes.filter(p => p.kind === 'arrival' && p.state === 'waiting' && p.side === plane.side);
|
||||
const at = Math.max(0, group.findIndex(p => p.id === plane.id));
|
||||
const offset = group.length ? at * 1.12 : 0;
|
||||
const n = PORTS[plane.side], length = Math.hypot(n.x, n.y), x = n.x / length, y = n.y / length;
|
||||
// The cell cluster's outline is pointy-top: its six wide flat sides sit half a
|
||||
// step (30°) from the port normals, so bays rest on those sides, never on a point.
|
||||
const c = Math.cos(Math.PI / 6), s = Math.sin(Math.PI / 6);
|
||||
const bx = x * c - y * s, by = x * s + y * c;
|
||||
const { x: bx, y: by } = bayNormal(plane.side);
|
||||
const d = apronDistance(radius);
|
||||
return { x: bx * (d + offset), y: by * (d + offset), angle: Math.atan2(-by, -bx) + Math.PI / 2 };
|
||||
}
|
||||
|
||||
// White tray outline in board-local units, shared by drawing, the edge-runway
|
||||
// reach and the runway-read tests. A convex hull, so ray crossings are unambiguous.
|
||||
export const TRAY_PAD = 1.34;
|
||||
const hullCache = new WeakMap();
|
||||
export function trayHull(cells) {
|
||||
if (hullCache.has(cells)) return hullCache.get(cells);
|
||||
const points = cells.flatMap(c => { const p = position(c.q, c.r); return hexPoints(p.x, p.y, TRAY_PAD); });
|
||||
const sorted = [...points].sort((a, b) => a.x - b.x || a.y - b.y), lower = [], upper = [];
|
||||
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
||||
for (const q of sorted) { while (lower.length >= 2 && cross(lower.at(-2), lower.at(-1), q) <= 0) lower.pop(); lower.push(q); }
|
||||
for (const q of sorted.reverse()) { while (upper.length >= 2 && cross(upper.at(-2), upper.at(-1), q) <= 0) upper.pop(); upper.push(q); }
|
||||
const hull = [...lower.slice(0, -1), ...upper.slice(0, -1)];
|
||||
hullCache.set(cells, hull);
|
||||
return hull;
|
||||
}
|
||||
// Distance from `from` (inside the hull) to the tray outline along unit `dir`.
|
||||
export function rimCrossing(hull, from, dir) {
|
||||
let exit = null;
|
||||
for (let i = 0; i < hull.length; i++) {
|
||||
const a = hull[i], b = hull[(i + 1) % hull.length];
|
||||
const ex = b.x - a.x, ey = b.y - a.y, den = dir.x * ey - dir.y * ex;
|
||||
if (Math.abs(den) < 1e-12) continue;
|
||||
const wx = a.x - from.x, wy = a.y - from.y;
|
||||
const t = (wx * ey - wy * ex) / den, u = (wx * dir.y - wy * dir.x) / den;
|
||||
if (t > 0 && u >= -1e-9 && u <= 1 + 1e-9 && (exit === null || t > exit)) exit = t;
|
||||
}
|
||||
return exit;
|
||||
}
|
||||
export const copy = value => structuredClone(value);
|
||||
const onBoard = plane => plane.state === 'ground' || plane.state === 'parked';
|
||||
const catalogCache = new WeakMap();
|
||||
|
||||
Reference in New Issue
Block a user