Screw conveyors join across the corner to diagonals (t62)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 12:12:44 -04:00
co-authored by Claude Opus 5.5
parent 828c71487c
commit ff9fd7b1b8
13 changed files with 250 additions and 99 deletions
+14 -8
View File
@@ -134,6 +134,7 @@ dist/, shots/ build output and screenshots (not committed)
- **The pixel look:** - **The pixel look:**
- `drawWorld` (`src/render/world.js`) fills one pixel per grain; - `drawWorld` (`src/render/world.js`) fills one pixel per grain;
- `diagonal-screws.js` shares screw join and casing geometry between pixels, smooth shading tags and previews;
- `computeLight` (`light.js`) adds coloured light; - `computeLight` (`light.js`) adds coloured light;
- overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards - overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards
(`machines.js`, `overlays.js`). (`machines.js`, `overlays.js`).
@@ -264,9 +265,9 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick). Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
- functions: engineIntake, fanBlow, machinesTick - functions: engineIntake, fanBlow, machinesTick
### src/sim/conveyors.js (194 lines) ### src/sim/conveyors.js (208 lines)
How belts, filters and screw conveyors move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full. How belts, filters and screw conveyors move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full.
- functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, diagonalScrewNext - functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, straightScrewNext, diagonalScrewNext
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty - variables: CARRY_DEPTH, filterR, filterL, filtersDirty
### src/sim/slope-conveyors.js (230 lines) ### src/sim/slope-conveyors.js (230 lines)
@@ -283,7 +284,7 @@ Molds: cavities for castings; ingot troughs empty themselves below. Removal retu
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
- variables: DROSS, grains, barWord - variables: DROSS, grains, barWord
### src/sim/items.js (288 lines) ### src/sim/items.js (295 lines)
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds. Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText - functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
- variables: BAR_W, BAR_H, entCurM, droppedLast - variables: BAR_W, BAR_H, entCurM, droppedLast
@@ -366,11 +367,15 @@ Drawing only: the face stays on its simulation cells; the band hangs below it.
- functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat - functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat
- variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner - variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner
### src/render/world.js (458 lines) ### src/render/diagonal-screws.js (74 lines)
Scale colour without changing the smooth-look class. Screw casing geometry shared by both looks, ghosts and placement previews. smooth-look class of a screw conveyor cell by direction: 9 up, 10 right, 11 down, 12 left
- functions: fogColor, diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock, drawWorld - functions: diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap - variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
### src/render/world.js (402 lines)
Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld
### src/render/machines.js (256 lines) ### src/render/machines.js (256 lines)
Machine overlays: fans, engines, silo and the rest, drawn on top of the world. Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks - functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks
@@ -388,7 +393,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js. Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
- variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW - variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW
### src/render/smooth-shaders.js (369 lines) ### src/render/smooth-shaders.js (373 lines)
Smooth shaders: vertex and fragment programs for the rounded world look. Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS - variables: SM_VS, SM_FS
@@ -481,7 +486,7 @@ Gamepad input.
- functions: padPressed, pollPad - functions: padPressed, pollPad
- variables: PAD - variables: PAD
### src/ui/previews.js (250 lines) ### src/ui/previews.js (239 lines)
Cursor previews: shared grid helpers, air streaks and tool preview dispatch. Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock - functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
@@ -572,3 +577,4 @@ The frame loop and boot.
- `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect. - `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
- `tools/scenes/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand. - `tools/scenes/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand.
- `tools/scenes/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below. - `tools/scenes/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
- `tools/scenes/t62-joins.js`: Self-contained grid. Rows: UR, DR, DL, UL. Each run has both end joins. Columns: horizontal edge, horizontal corner, vertical edge, vertical corner; repeat those four with the straight travel reversed. Camera fits --zoom 6 --viewport 2100x1300. Run after clear-sky.js or by itself. Empty, paused screws expose the whole casing.
+15
View File
@@ -4,6 +4,21 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-27: t62 screw conveyor joins (Claude Code + Codex)
- Straight screws touching a diagonal's end only at the corner now join, in both looks and in
ghosts/previews, and grains and bars cross the joint. Same-row joins were already elbows.
Also removed the seam across the top of connected elbows in the smooth look.
- Files: new `src/render/diagonal-screws.js` (join and casing geometry moved out of
`src/render/world.js`), `src/render/smooth-shaders.js`, `src/ui/previews.js`,
`src/sim/conveyors.js` (`straightScrewNext`), `src/sim/machine-tick.js`, `src/sim/items.js`,
`src/manifest.json`, `docs/PROJECT_MAP.md`, `tools/scenes/t62-joins.js`, tests
`test/t_diag_screws.js`, `test/t_smooth.py`.
- Tests: `python3 test/run_all.py --browser` all passed; determinism diff showed only timing
lines and the new checks.
- Open: the corner join doesn't check that the straight piece ends there, so a straight run
passing over the diagonal's corner also draws a jog. Not playtested by hand.
## 2026-10-27: t66 dev console in every build (Claude Code) ## 2026-10-27: t66 dev console in every build (Claude Code)
- Owner decision: the console ships in production too, with a warning when it's turned on that - Owner decision: the console ships in production too, with a warning when it's turned on that
+1
View File
@@ -43,6 +43,7 @@
"render/setup.js", "render/setup.js",
"render/light.js", "render/light.js",
"render/slope-belts.js", "render/slope-belts.js",
"render/diagonal-screws.js",
"render/world.js", "render/world.js",
"render/machines.js", "render/machines.js",
"render/overlays.js", "render/overlays.js",
+74
View File
@@ -0,0 +1,74 @@
// Screw casing geometry shared by both looks, ghosts and placement previews.
// smooth-look class of a screw conveyor cell by direction: 9 up, 10 right, 11 down, 12 left
const SCREW_CLASS = new Uint8Array(15); SCREW_CLASS[1] = 9; SCREW_CLASS[4] = 10; SCREW_CLASS[2] = 11; SCREW_CLASS[3] = 12;
for (let p = P_SCREW_UR; p <= P_SCREW_UL; p++) SCREW_CLASS[p] = 19 + p - P_SCREW_UR;
// Drawing only: the four transport lanes remain in the square block.
let diagonalJoins = new Uint8Array(0);
function diagonalScrewJoins(bx, by, v, planned = null) {
// Three bits per end: 1..4 edge exits, 5/6 corner horizontal/vertical; bits 6/7 are collars.
const dy = v === 4 || v === 6 ? -1 : 1;
let joins = 0;
for (let end = 0; end < 2; end++) {
const side = end ? 1 : -1, nx = bx + side, ny = by + side * dy;
const nb = ny * BW + nx, next = planned && planned.get(nb);
const nc = next ? next[0] : blockP[nb], nv = next ? next[1] : blockV[nb];
if (nx >= 0 && ny >= 0 && nx < BW && ny < BH &&
nc === BP_SCREW && nv >= 4 && (nv === 4 || nv === 6) === (dy < 0)) continue;
let join = 0;
for (const dir of [side > 0 ? 1 : 3, side * dy > 0 ? 2 : 0]) {
const x = bx + SCREW_DIRS[dir][0], y = by + SCREW_DIRS[dir][1];
if (x < 0 || y < 0 || x >= BW || y >= BH) continue;
const n = y * BW + x, next = planned && planned.get(n);
const code = next ? next[0] : blockP[n], nv = next ? next[1] : blockV[n];
if ((code === BP_SCREW && nv < 4 && (nv & 1) === (dir & 1)) ||
((code === BP_BELT || code === BP_FILTER) && (dir & 1))) { join = dir + 1; break; }
}
if (!join && nx >= 0 && ny >= 0 && nx < BW && ny < BH && nc === BP_SCREW && nv < 4) join = nv & 1 ? 5 : 6;
joins |= join << (end * 3);
if (!join) joins |= 64 << end;
}
return joins;
}
// Scratch coordinates shared by the pixel shading; no allocations in the grain loop.
let diagonalAcross = 0, diagonalAlong = 0, diagonalCap = 0;
function diagonalScrewShape(x, y, b, v, joins) {
const lx = x - (b % BW) * BS - 2, ly = y - ((b / BW) | 0) * BS - 2;
let best = Infinity;
for (let end = 0; end < 2; end++) {
const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1;
const slope = v === 4 || v === 6 ? -1 : 1;
const dx = join >= 5 ? (join === 5 ? side : 0) : join ? SCREW_DIRS[join - 1][0] : side * Math.SQRT1_2;
const dy = join >= 5 ? (join === 6 ? side * slope : 0) : join ? SCREW_DIRS[join - 1][1] : side * slope * Math.SQRT1_2;
const ox = join === 6 ? side * BS : 0, oy = join === 5 ? side * slope * BS : 0;
const along = (lx - ox) * dx + (ly - oy) * dy, across = -(lx - ox) * dy + (ly - oy) * dx;
let radius = Math.hypot(across, Math.min(0, along));
if (join >= 5) {
const t = Math.max(0, Math.min(1, (lx * ox + ly * oy) / (BS * BS)));
radius = Math.min(radius, Math.hypot(lx - ox * t, ly - oy * t));
}
const cap = along - (join ? 2 : 2 * Math.SQRT2);
const sd = Math.max(radius - 2, join || (joins & (64 << end)) ? cap : -10);
if (sd < best) {
best = sd; diagonalAcross = side * across < 0 ? -radius : radius;
diagonalAlong = ((b % BW) * BS + 2 + (((b / BW) | 0) * BS + 2) * (v === 4 || v === 6 ? -1 : 1)) * Math.SQRT1_2 + side * along; diagonalCap = joins & (64 << end) ? cap : -10;
}
}
return best;
}
function diagonalScrewBlock(x, y, smooth) {
const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
if (part[i] >= P_SCREW_UR && part[i] <= P_SCREW_UL) return own;
if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[own]) return -1;
let best = smooth ? 0.75 : 0, found = -1;
for (let k = 0; k < 4; k++) {
const bx = (x >> 2) + (k === 0 ? -1 : k === 1 ? 1 : 0);
const by = (y >> 2) + (k === 2 ? -1 : k === 3 ? 1 : 0);
if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue;
const b = by * BW + bx, v = blockV[b];
if (blockP[b] !== BP_SCREW || v < 4) continue;
const joins = diagonalJoins[b] === undefined ? diagonalScrewJoins(bx, by, v) : diagonalJoins[b];
const sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins);
if (sd <= best) { best = sd; found = b; }
}
return found;
}
+6 -2
View File
@@ -265,10 +265,14 @@ ${SM_SLOPE_DRAW}
int join = (joins >> (end * 3)) & 7; int join = (joins >> (end * 3)) & 7;
float side = end == 0 ? -1.0 : 1.0; float side = end == 0 ? -1.0 : 1.0;
vec2 ray = join == 0 ? vec2(side, side * (nc == 19 || nc == 21 ? -1.0 : 1.0)) * 0.70710678 : vec2 ray = join == 0 ? vec2(side, side * (nc == 19 || nc == 21 ? -1.0 : 1.0)) * 0.70710678 :
join == 5 ? vec2(side, 0) : join == 6 ? vec2(0, side * d.y) :
join == 1 ? vec2(0, -1) : join == 2 ? vec2(1, 0) : join == 3 ? vec2(0, 1) : vec2(-1, 0); join == 1 ? vec2(0, -1) : join == 2 ? vec2(1, 0) : join == 3 ? vec2(0, 1) : vec2(-1, 0);
float t = dot(local, ray), cross2 = dot(local, vec2(-ray.y, ray.x)); vec2 offset = join == 5 ? vec2(0, side * d.y * 4.0) : join == 6 ? vec2(side * 4.0, 0) : vec2(0);
float t = dot(local - offset, ray), cross2 = dot(local - offset, vec2(-ray.y, ray.x));
float radius = length(vec2(cross2, min(0.0, t))), cap = t - (join == 0 ? 2.82842712 : 2.0); float radius = length(vec2(cross2, min(0.0, t))), cap = t - (join == 0 ? 2.82842712 : 2.0);
float sd = max(radius - 2.0, join != 0 || (joins & (64 << end)) != 0 ? cap : -10.0); if (join >= 5) radius = min(radius, length(local - offset * clamp(dot(local, offset) / 16.0, 0.0, 1.0)));
// A connected mouth meets another opaque pipe; fading its edge leaves a seam.
float sd = max(radius - 2.0, (joins & (64 << end)) != 0 ? cap : -10.0);
if (sd < screwSD) { if (sd < screwSD) {
screwSD = sd; screwSD = sd;
a = 2.0 + (side * cross2 < 0.0 ? -radius : radius); a = 2.0 + (side * cross2 < 0.0 ? -radius : radius);
+12 -68
View File
@@ -2,71 +2,6 @@
function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 16) | (((c >>> 8 & 255) * k) << 8) | ((c & 255) * k); } function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 16) | (((c >>> 8 & 255) * k) << 8) | ((c & 255) * k); }
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look. // drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
// ---------- World pixels ---------- // ---------- World pixels ----------
// smooth-look class of a screw conveyor cell by direction: 9 up, 10 right, 11 down, 12 left
const SCREW_CLASS = new Uint8Array(15); SCREW_CLASS[1] = 9; SCREW_CLASS[4] = 10; SCREW_CLASS[2] = 11; SCREW_CLASS[3] = 12;
for (let p = P_SCREW_UR; p <= P_SCREW_UL; p++) SCREW_CLASS[p] = 19 + p - P_SCREW_UR;
// Drawing only: the four transport lanes remain in the square block.
let diagonalJoins = new Uint8Array(0);
function diagonalScrewJoins(bx, by, v, planned = null) {
// Low/high three bits select the two edge exits; bits 6/7 mark free-end collars.
const dy = v === 4 || v === 6 ? -1 : 1;
let joins = 0;
for (let end = 0; end < 2; end++) {
const side = end ? 1 : -1, nx = bx + side, ny = by + side * dy;
const nb = ny * BW + nx, next = planned && planned.get(nb);
const nc = next ? next[0] : blockP[nb], nv = next ? next[1] : blockV[nb];
if (nx >= 0 && ny >= 0 && nx < BW && ny < BH &&
nc === BP_SCREW && nv >= 4 && (nv === 4 || nv === 6) === (dy < 0)) continue;
let join = 0;
for (const dir of [side > 0 ? 1 : 3, side * dy > 0 ? 2 : 0]) {
const x = bx + SCREW_DIRS[dir][0], y = by + SCREW_DIRS[dir][1];
if (x < 0 || y < 0 || x >= BW || y >= BH) continue;
const n = y * BW + x, next = planned && planned.get(n);
const code = next ? next[0] : blockP[n], nv = next ? next[1] : blockV[n];
if ((code === BP_SCREW && nv < 4 && (nv & 1) === (dir & 1)) ||
((code === BP_BELT || code === BP_FILTER) && (dir & 1))) { join = dir + 1; break; }
}
joins |= join << (end * 3);
if (!join) joins |= 64 << end;
}
return joins;
}
// Scratch coordinates shared by the pixel shading; no allocations in the grain loop.
let diagonalAcross = 0, diagonalAlong = 0, diagonalCap = 0;
function diagonalScrewShape(x, y, b, v, joins) {
const lx = x - (b % BW) * BS - 2, ly = y - ((b / BW) | 0) * BS - 2;
let best = Infinity;
for (let end = 0; end < 2; end++) {
const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1;
const dx = join ? SCREW_DIRS[join - 1][0] : side * Math.SQRT1_2;
const dy = join ? SCREW_DIRS[join - 1][1] : side * (v === 4 || v === 6 ? -1 : 1) * Math.SQRT1_2;
const along = lx * dx + ly * dy, across = -lx * dy + ly * dx;
const radius = Math.hypot(across, Math.min(0, along)), cap = along - (join ? 2 : 2 * Math.SQRT2);
const sd = Math.max(radius - 2, join || (joins & (64 << end)) ? cap : -10);
if (sd < best) {
best = sd; diagonalAcross = side * across < 0 ? -radius : radius;
diagonalAlong = ((b % BW) * BS + 2 + (((b / BW) | 0) * BS + 2) * (v === 4 || v === 6 ? -1 : 1)) * Math.SQRT1_2 + side * along; diagonalCap = joins & (64 << end) ? cap : -10;
}
}
return best;
}
function diagonalScrewBlock(x, y, smooth) {
const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
if (part[i] >= P_SCREW_UR && part[i] <= P_SCREW_UL) return own;
if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[own]) return -1;
let best = smooth ? 0.75 : 0, found = -1;
for (let k = 0; k < 4; k++) {
const bx = (x >> 2) + (k === 0 ? -1 : k === 1 ? 1 : 0);
const by = (y >> 2) + (k === 2 ? -1 : k === 3 ? 1 : 0);
if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue;
const b = by * BW + bx, v = blockV[b];
if (blockP[b] !== BP_SCREW || v < 4) continue;
const joins = diagonalJoins[b] === undefined ? diagonalScrewJoins(bx, by, v) : diagonalJoins[b];
const sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins);
if (sd <= best) { best = sd; found = b; }
}
return found;
}
function drawWorld() { function drawWorld() {
const bw = R.bw, bh = R.bh, px = R.px; const bw = R.bw, bh = R.bh, px = R.px;
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY); R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
@@ -404,9 +339,18 @@ function drawWorld() {
let tr = 90, tg = 190, tb = 230; let tr = 90, tg = 190, tb = 230;
if (blocked) { tr = 230; tg = 70; tb = 60; } if (blocked) { tr = 230; tg = 70; tb = 60; }
else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; } else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
const slope = isSlopeBelt(code), joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : 0; const slope = isSlopeBelt(code), diagonal = code === BP_SCREW && v >= 4;
for (let dy = slope ? -9 : 0; dy < (slope ? 8 : BS); dy++) for (let dx = slope ? -5 : 0; dx < (slope ? 9 : BS); dx++) { const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : 0;
for (let dy = slope ? -9 : diagonal ? -4 : 0; dy < (slope || diagonal ? 8 : BS); dy++) for (let dx = slope ? -5 : diagonal ? -4 : 0; dx < (slope ? 9 : diagonal ? 8 : BS); dx++) {
let sd = 0; let sd = 0;
if (diagonal) {
const x = gbx * BS + dx, y = gby * BS + dy;
if (x < 0 || y < 0 || x >= W || y >= H) continue;
const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins);
if (sd > 0) continue;
}
if (slope) { if (slope) {
const x = gbx * BS + dx, y = gby * BS + dy; const x = gbx * BS + dx, y = gby * BS + dy;
if (x < 0 || y < 0 || x >= W || y >= H) continue; if (x < 0 || y < 0 || x >= W || y >= H) continue;
@@ -421,7 +365,7 @@ function drawWorld() {
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0; const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue; if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
const k = sy * bw + sx, c = px[k]; const k = sy * bw + sx, c = px[k];
const edge = slope ? sd > -0.8 : dx === 0 || dy === 0 || dx === BS - 1 || dy === BS - 1; const edge = slope || diagonal ? sd > -0.8 : dx === 0 || dy === 0 || dx === BS - 1 || dy === BS - 1;
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28; const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255; const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a); px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
+14
View File
@@ -176,6 +176,20 @@ function screwIntake(i, p, dx, dy) {
} }
} }
// A straight mouth can feed a diagonal tail one block across the corner.
function straightScrewNext(i, p) {
const dx = SCREW_DX[p], dy = SCREW_DY[p], x = i % W, y = (i / W) | 0;
const xx = x + dx, yy = y + dy, next = i + dx + dy * W;
if (xx < 0 || yy < 0 || xx >= W || yy >= H) return -1;
if (((xx >> 2) === (x >> 2) && (yy >> 2) === (y >> 2)) || part[next] || blockP[(yy >> 2) * BW + (xx >> 2)]) return next;
for (let side = -1; side <= 1; side += 2) {
const nx = xx + (dx ? 0 : side * BS), ny = yy + (dy ? 0 : side * BS);
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const j = ny * W + nx, q = part[j];
if (SCREW_DX[q] === (dx || side) && SCREW_DY[q] === (dy || side)) return j;
}
return next;
}
// Four lanes cross a block in four beats; the casing carries every lane across the corner. // Four lanes cross a block in four beats; the casing carries every lane across the corner.
// Prefer the next diagonal block, then either straight join, then the open head. // Prefer the next diagonal block, then either straight join, then the open head.
function diagonalScrewNext(i, p) { function diagonalScrewNext(i, p) {
+7
View File
@@ -90,6 +90,13 @@ function entScrewTick(e, p) {
if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return; if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return;
const dx = SCREW_DX[p], dy = SCREW_DY[p]; const dx = SCREW_DX[p], dy = SCREW_DY[p];
if (dx && dy) { entDiagonalScrewTick(e, p); return; } if (dx && dy) { entDiagonalScrewTick(e, p); return; }
const edge = entScrewEdge(e, dx, dy, 0), behind = edge - dx - dy * W;
const corner = edge >= 0 ? straightScrewNext(behind, p) : -1;
if (corner >= 0 && corner !== edge) {
const x = (corner % W) & ~3, y = entScrewLane((corner / W) | 0, e.iy);
if (entScrewMove(e, x, y, true)) stats.moved++;
return;
}
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0; let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0;
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) { for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const i = entScrewEdge(e, dx, dy, k); const i = entScrewEdge(e, dx, dy, k);
+1 -1
View File
@@ -130,7 +130,7 @@ function machinesTick() {
if (shifted) continue; if (shifted) continue;
} }
if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) continue; if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) continue;
const dest = dx && dy ? diagonalScrewNext(i, p) : i + dj; const dest = dx && dy ? diagonalScrewNext(i, p) : straightScrewNext(i, p);
if (dest < 0) continue; if (dest < 0) continue;
const nx = dest % W, ny = (dest / W) | 0; const nx = dest % W, ny = (dest / W) | 0;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue; if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
+9 -20
View File
@@ -182,27 +182,16 @@ function previewBlock(code, x, y, w, h, stroke = false, direction = -1, bx = -1,
const ctx = R.ctx; const ctx = R.ctx;
if (code === BP_SCREW && direction >= 4) { if (code === BP_SCREW && direction >= 4) {
const joins = bx >= 0 ? diagonalScrewJoins(bx, by, direction, planned) : 0; const joins = bx >= 0 ? diagonalScrewJoins(bx, by, direction, planned) : 0;
const points = []; const scale = w / BS, step = SET.look === "smooth" ? 0.25 : 1;
for (let end = 0; end < 2; end++) { const b = bx >= 0 ? by * BW + bx : 0, ox = (b % BW) * BS, oy = ((b / BW) | 0) * BS;
const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1; ctx.beginPath();
const d = join ? SCREW_DIRS[join - 1] : [side, side * (direction === 4 || direction === 6 ? -1 : 1)]; for (let yy = -4; yy < 8; yy += step) for (let xx = -4; xx < 8; xx += step) {
points.push([d[0] * w / 2, d[1] * h / 2]); const sd = diagonalScrewShape(ox + xx + step / 2, oy + yy + step / 2, b, direction, joins);
if (sd <= 0 && (!stroke || sd > -step * 1.5)) ctx.rect(x + xx * scale, y + yy * scale, step * scale, step * scale);
} }
const r = w / 2, a = points[0], b = points[1]; if (stroke) { const fill = ctx.fillStyle; ctx.fillStyle = ctx.strokeStyle; ctx.fill(); ctx.fillStyle = fill; }
const al = Math.hypot(...a), bl = Math.hypot(...b); else ctx.fill();
const n0 = [a[1] / al, -a[0] / al], n1 = [-b[1] / bl, b[0] / bl]; return;
const turn = n0[0] * n1[1] - n0[1] * n1[0], dot = n0[0] * n1[0] + n0[1] * n1[1];
ctx.save(); ctx.translate(x + w / 2, y + h / 2); ctx.beginPath();
ctx.moveTo(a[0] + n0[0] * r, a[1] + n0[1] * r);
if (turn >= 0) ctx.lineTo((n0[0] + n1[0]) * r / (1 + dot), (n0[1] + n1[1]) * r / (1 + dot));
else ctx.arc(0, 0, r, Math.atan2(n0[1], n0[0]), Math.atan2(n1[1], n1[0]), true);
ctx.lineTo(b[0] + n1[0] * r, b[1] + n1[1] * r);
ctx.lineTo(b[0] - n1[0] * r, b[1] - n1[1] * r);
if (turn <= 0) ctx.lineTo(-(n0[0] + n1[0]) * r / (1 + dot), -(n0[1] + n1[1]) * r / (1 + dot));
else ctx.arc(0, 0, r, Math.atan2(-n1[1], -n1[0]), Math.atan2(-n0[1], -n0[0]), true);
ctx.lineTo(a[0] - n0[0] * r, a[1] - n0[1] * r); ctx.closePath();
if (stroke) ctx.stroke(); else ctx.fill();
ctx.restore(); return;
} }
if (isSlopeBelt(code)) { if (isSlopeBelt(code)) {
const b = by * BW + bx, joins = bx >= 0 ? slopeBeltConnections(bx, by, code, planned) : 0; const b = by * BW + bx, joins = bx >= 0 ? slopeBeltConnections(bx, by, code, planned) : 0;
+43
View File
@@ -48,6 +48,39 @@ for (const v of [4, 5, 6, 7]) {
const joined = drain(c,500); const joined = drain(c,500);
check(`direction ${v}: straight to diagonal to straight`,joined.out===16 && joined.leaks===0,JSON.stringify(joined)); check(`direction ${v}: straight to diagonal to straight`,joined.out===16 && joined.leaks===0,JSON.stringify(joined));
} }
// Corner-adjacent joints: all axes, both ends, grains and queued bars.
function cornerLine(v, vertical = false) {
const s = line(v), vv = vertical ? (s.dy < 0 ? 0 : 2) : (s.dx > 0 ? 1 : 3);
const tx = s.bx-s.dx, ty = s.by-s.dy, hx = s.head+s.dx, hy = s.hy+s.dy;
run(`for(let k=0;k<3;k++) {
placeBlock(BP_SCREW,${vv},${tx}-k*${vertical?0:s.dx},${ty}-k*${vertical?s.dy:0},true);
placeBlock(BP_SCREW,${vv},${hx}+k*${vertical?0:s.dx},${hy}+k*${vertical?s.dy:0},true);
}`);
return {...s, tail:tx-2*(vertical?0:s.dx), by:ty-2*(vertical?s.dy:0),
head:hx+2*(vertical?0:s.dx), hy:hy+2*(vertical?s.dy:0)};
}
for(const v of [4,5,6,7]) for(const vertical of [false,true]) {
const s=cornerLine(v,vertical);
rect(run,s.tail*4,s.by*4,s.tail*4+3,s.by*4+3,run("SAND"));
const r=run(`(()=>{let out=0,leak=0;for(let k=0;k<700;k++){
simTick();
for(const f of flyers)if(f.m===SAND){
if(${vertical ? (s.dy<0?`f.y<${s.hy*4}`:`f.y>=${(s.hy+1)*4}`) : (s.dx<0?`f.x<${s.head*4}`:`f.x>=${(s.head+1)*4}`)})out++;else leak++;
}flyers=[];
}return {out,leak,inside:mat.reduce((n,m)=>n+(m===SAND),0)};})()`);
check(`corner ${vertical?"vertical":"horizontal"} joins ${v} carry every grain to the far mouth`,r.out===16 && !r.leak,JSON.stringify(r));
cornerLine(v,vertical);
putBar(s.tail*4,s.by*4); putBar(s.tail*4,s.by*4+2);
const bars=run(`(()=>{let out=0,overlap=false;for(let k=0;k<700;k++){
simTick();
for(let a=0;a<ents.length;a++)for(let b=0;b<a;b++) {
const e=ents[a],f=ents[b];if(e.ix<f.ix+4 && f.ix<e.ix+4 && e.iy<f.iy+2 && f.iy<e.iy+2)overlap=true;
}
if(mat.some((m,i)=>occ[i] && (PH[m]===PH_SOLID || PH[m]===PH_POWDER)))overlap=true;
for(const e of ents.slice())if(${vertical ? (s.dy<0?`e.iy+2<=${s.hy*4}`:`e.iy>=${(s.hy+1)*4}`) : (s.dx<0?`e.ix+4<=${s.head*4}`:`e.ix>=${(s.head+1)*4}`)}){out++;removeEnt(e);}
}return {out,overlap,left:ents.length};})()`);
check(`corner ${vertical?"vertical":"horizontal"} joins ${v} carry queued bars`,bars.out===2 && !bars.overlap,JSON.stringify(bars));
}
// A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz. // A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz.
const rates = []; const rates = [];
for (const v of [0, 4, 5]) { for (const v of [0, 4, 5]) {
@@ -169,7 +202,17 @@ for(const [dx,dy,v] of [[5,-4,4],[4,5,5],[-5,4,6],[-4,-5,7]]) {
check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`)); check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`));
} }
// End elbows follow the adjoining straight piece's axis, regardless of flow direction. // End elbows follow the adjoining straight piece's axis, regardless of flow direction.
run(fs.readFileSync('src/render/diagonal-screws.js', 'utf8'));
run(fs.readFileSync('src/render/world.js', 'utf8')); run(fs.readFileSync('src/render/world.js', 'utf8'));
for (const v of [4,5,6,7]) for (const end of [-1,1]) for (let dir=0;dir<4;dir++) {
fresh();
const slope=v===4 || v===6?-1:1, nx=20+end, ny=20+end*slope, shift=end>0?3:0;
run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${dir},${nx},${ny},true);`);
const expected=dir&1?5:6;
check(`corner ${v} end ${end} straight ${dir} selects a closed elbow`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});return (j>>${shift}&7)===${expected} && !(j&${end>0?128:64});})()`));
check(`corner ${v} end ${end} straight ${dir} meets all four mouth lanes`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});return [0.5,1.5,2.5,3.5].every(t=>diagonalScrewShape(${dir&1?82+end*1.99:"80+t+"+end*4},${dir&1?"80+t+"+end*slope*4:82+end*slope*1.99},20*BW+20,${v},j)<=0);})()`));
check(`corner ${v} end ${end} straight ${dir} planned joins match placed joins`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});removeBlock(${nx},${ny});return diagonalScrewJoins(20,20,${v},new Map([[${ny}*BW+${nx},[BP_SCREW,${dir}]]]))===j;})()`));
}
for (const v of [4,5,6,7]) for (let dir=0;dir<4;dir++) for (const code of ['BP_SCREW','BP_BELT','BP_FILTER']) { for (const v of [4,5,6,7]) for (let dir=0;dir<4;dir++) for (const code of ['BP_SCREW','BP_BELT','BP_FILTER']) {
if (code !== 'BP_SCREW' && !(dir & 1)) continue; if (code !== 'BP_SCREW' && !(dir & 1)) continue;
fresh(); fresh();
+28
View File
@@ -97,6 +97,34 @@ with sync_playwright() as p:
}''') }''')
check('sidewall stays behind the grain colour in pixels and smooth', check('sidewall stays behind the grain colour in pixels and smooth',
wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall) wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall)
# All 64 straight/diagonal mouths must have copper casing on both sides of the join.
pg.set_viewport_size({"width": 2100, "height": 1300})
ev("part.fill(0); blockP.fill(0); occ.fill(0); mach.fill(0); ents=[]; ghostBlocks.clear();")
ev("() => {" + (root / "tools/scenes/t62-joins.js").read_text() + "}")
mouths = ev('''() => {
SET.zoom=6; SET.lighting="off"; SET.glow="off"; resizeRender();
bot.x=CAM[0]-bot.w/2;bot.y=CAM[1]-bot.h/2;snapCamera();
const out={};
for(const look of ["pixel","smooth"]) {
SET.look=look;SM.scale=1;drawWorld();
const bad=[];
for(const j of T62_JOINTS) {
const slope=j.v===4 || j.v===6?-1:1;
const end=j.end*SCREW_DIRS[j.v][0];
const dx=j.vertical?0:end,dy=j.vertical?end*slope:0;
const ox=j.corner && j.vertical?end*4:0,oy=j.corner && !j.vertical?end*slope*4:0;
for(const side of [-1,1])for(const along of [-0.3,0.3]) {
const x=j.bx*4+2+ox+dx*(2+along)-dy*side*1.6;
const y=j.by*4+2+oy+dy*(2+along)+dx*side*1.6;
const p=worldToScreenPx(x,y),c=R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data;
if(!(c[0]>c[1]*1.15 && c[1]>c[2]*1.1))bad.push([j,x,y,Array.from(c)]);
}
}
out[look]=bad;
}return out;
}''')
for look, bad in mouths.items():
check(f"{look}: all 64 screw joints have closed casing across both mouth edges", not bad, bad[:2])
# a browser without hardware drawing gets the pixel look and a note # a browser without hardware drawing gets the pixel look and a note
pg2 = b.new_page(viewport={'width': 800, 'height': 500}) pg2 = b.new_page(viewport={'width': 800, 'height': 500})
pg2.add_init_script('localStorage.clear();') pg2.add_init_script('localStorage.clear();')
+26
View File
@@ -0,0 +1,26 @@
// Self-contained grid. Rows: UR, DR, DL, UL. Each run has both end joins.
// Columns: horizontal edge, horizontal corner, vertical edge, vertical corner;
// repeat those four with the straight travel reversed. Camera fits --zoom 6 --viewport 2100x1300.
// Run after clear-sky.js or by itself. Empty, paused screws expose the whole casing.
money = 50000;
for (let y = 64; y < 280; y++) for (let x = 112; x < 456; x++) {
const i = y * W + x;
setCell(i, EMPTY, 20, 0); bgm[i] = 0;
}
for (let x = 112; x < 456; x++) surfY[x] = 280;
window.T62_JOINTS = [];
for (let row = 0; row < 4; row++) for (let col = 0; col < 8; col++) {
const v = 4 + row, [dx, dy] = SCREW_DIRS[v], vertical = !!(col & 2), corner = !!(col & 1);
const x = 33 + col * 10 + (dx < 0 ? 4 : 0), y = 23 + row * 12 + (dy < 0 ? 4 : 0);
const sv = vertical ? (dy < 0 ? 0 : 2) : (dx > 0 ? 1 : 3), straight = col < 4 ? sv : (sv + 2) % 4;
for (let k = 0; k < 3; k++) placeBlock(BP_SCREW, v, x + k * dx, y + k * dy, true);
for (const end of [-1, 1]) {
const ex = x + (end > 0 ? 2 * dx : 0), ey = y + (end > 0 ? 2 * dy : 0);
const nx = ex + (vertical && !corner ? 0 : end * dx), ny = ey + (!vertical && !corner ? 0 : end * dy);
for (let k = 0; k < 2; k++) placeBlock(BP_SCREW, straight, nx + (vertical ? 0 : k * end * dx), ny + (vertical ? k * end * dy : 0), true);
T62_JOINTS.push({v, end, vertical, corner, straight, bx:ex, by:ey});
}
}
updateRoof();
window.CAM = [284, 168];
drawBot = () => {};