Shared join module for transport pieces; belt drawing uses it (t67 phase 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 13:00:43 -04:00
co-authored by Claude Opus 5.5
parent 787721ad35
commit 65338b2b27
11 changed files with 274 additions and 38 deletions
+71
View File
@@ -0,0 +1,71 @@
// Geometric transport ends. Tables are built once; querying a world or plan allocates nothing.
// Ends are left/right, except vertical screws (up/down). Travel direction is irrelevant.
const JOIN_ENDS = [[], ...[
['screw-horizontal', 1, 0], ['screw-vertical', 0, 1],
['screw-diagonal', 1, -1], ['screw-diagonal', 1, 1],
['belt-flat', 1, 0], ['belt-sloped', 1, -1], ['belt-sloped', 1, 1]
].map(([kind, dx, dy]) => [-1, 1].map(s => ({
dx: s * dx, dy: s * dy, diagonal: !!(dx && dy),
high: !!(dx && dy && s * dy < 0), low: !!(dx && dy && s * dy > 0), kind,
targets: []
})))];
function pieceEnds(code, v) {
if (code === BP_SCREW) {
if (v < 0 || v > 7) return JOIN_ENDS[0];
return JOIN_ENDS[v < 4 ? (v & 1 ? 1 : 2) : (SCREW_DIRS[v][0] * SCREW_DIRS[v][1] < 0 ? 3 : 4)];
}
if (code === BP_BELT || code === BP_FILTER) return JOIN_ENDS[5];
return isSlopeBelt(code) ? JOIN_ENDS[beltGrade(code) < 0 ? 6 : 7] : JOIN_ENDS[0];
}
// Compile the diagonal-end rule and its inverse into bounded neighbour probes.
for (const ends of JOIN_ENDS) for (const end of ends) {
for (const others of JOIN_ENDS) for (let n = 0; n < others.length; n++) {
const other = others[n], screw = end.kind.startsWith('screw'), otherScrew = other.kind.startsWith('screw');
if (screw !== otherScrew && (end.kind === 'screw-vertical' || other.kind === 'screw-vertical' ||
(end.diagonal && other.diagonal))) continue;
let dx, dy;
if (end.diagonal && other.diagonal) {
if (screw !== otherScrew || end.dx !== -other.dx || end.dy !== -other.dy) continue;
dx = end.dx; dy = end.dy;
} else if (end.diagonal) {
if (other.dx ? other.dx !== -end.dx : other.dy !== -end.dy) continue;
dx = end.dx; dy = other.dx && end.high ? 0 : end.dy;
} else if (other.diagonal) {
if (end.dx ? end.dx !== -other.dx : end.dy !== -other.dy) continue;
dx = -other.dx; dy = end.dx && other.high ? 0 : -other.dy;
} else {
if (end.dx !== -other.dx || end.dy !== -other.dy) continue;
dx = end.dx; dy = end.dy;
}
end.targets.push({ dx, dy, ends: others, end: n });
}
// Prefer a continuing diagonal run to an elbow when both are present.
end.targets.sort((a, b) => Number(b.ends[b.end].diagonal) - Number(a.ends[a.end].diagonal));
}
// Result: neighbour block index * 2 + its end index; -1 means an open end.
function joinCandidate(bx, by, endIndex, planned) {
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return -1;
const b = by * BW + bx, plan = planned && planned.get(b);
const ends = pieceEnds(plan ? plan[0] : blockP[b], plan ? plan[1] : blockV[b]);
const end = ends[endIndex];
if (!end) return -1;
for (let k = 0; k < end.targets.length; k++) {
const target = end.targets[k];
const x = bx + target.dx, y = by + target.dy;
if (x < 0 || y < 0 || x >= BW || y >= BH) continue;
const nb = y * BW + x, next = planned && planned.get(nb);
if (pieceEnds(next ? next[0] : blockP[nb], next ? next[1] : blockV[nb]) === target.ends)
return nb * 2 + target.end;
}
return -1;
}
function endJoin(bx, by, endIndex, planned = null) {
const join = joinCandidate(bx, by, endIndex, planned);
if (join < 0) return -1;
const nb = join >> 1;
// Competing neighbours cannot claim an end that chose a different partner.
return joinCandidate(nb % BW, (nb / BW) | 0, join & 1, planned) === (by * BW + bx) * 2 + endIndex ? join : -1;
}
function pieceJoins(bx, by, planned = null) {
return (endJoin(bx, by, 0, planned) >= 0 ? 1 : 0) | (endJoin(bx, by, 1, planned) >= 0 ? 2 : 0);
}