Corner placements of screw conveyors stay separate ends (t62 follow-up)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 12:32:23 -04:00
co-authored by Claude Opus 5.5
parent ff9fd7b1b8
commit 787721ad35
11 changed files with 115 additions and 102 deletions
+5
View File
@@ -486,6 +486,11 @@ above, including:
on warns first that the save will be marked, and the mark stays with the world; any future on warns first that the save will be marked, and the mark stays with the world; any future
achievements won't unlock in a marked world. Besides money, items and upgrades it may have achievements won't unlock in a marked world. Besides money, items and upgrades it may have
game speed and other things just for fun. game speed and other things just for fun.
- **Screw conveyors only join a diagonal on the same row (t62, 2026-10-27).** A straight screw
joins a diagonal when it sits right beside the diagonal's end block, and that join is drawn
as an elbow. When the diagonal instead starts one block lower or higher, so the two only touch
corner to corner, they stay two separate ends and nothing passes between them. The owner tried
a corner join and found it looked odd, and building it that way is unusual anyway.
## Ideas not built yet ## Ideas not built yet
+6 -6
View File
@@ -265,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 (208 lines) ### src/sim/conveyors.js (198 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, straightScrewNext, diagonalScrewNext - functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, 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)
@@ -284,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 (295 lines) ### src/sim/items.js (288 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
@@ -367,7 +367,7 @@ 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/diagonal-screws.js (74 lines) ### src/render/diagonal-screws.js (68 lines)
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 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: diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock - functions: diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap - variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
@@ -393,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 (373 lines) ### src/render/smooth-shaders.js (370 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
@@ -577,4 +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. - `tools/scenes/t62-joins.js`: Self-contained grid. Rows: UR, DR, DL, UL. Each run shows both ends. Columns: horizontal edge, horizontal corner, vertical edge, vertical corner. Corner columns stay unconnected, with two separate ends. 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.
+13
View File
@@ -4,6 +4,19 @@ 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 corner placements stay separate (Claude Code + Codex)
- Owner tried the corner join and found the jog odd; decision (recorded in DESIGN.md): only
same-row joins connect. Removed join kinds 5/6 and `straightScrewNext`; `diagonalScrewNext`
and `entDiagonalScrewTick` now continue across a corner only into a diagonal running the same
way, so a corner straight piece no longer receives grains or bars. Kept the shared geometry
file and the smooth-look elbow seam fix.
- Files: `src/render/diagonal-screws.js`, `src/render/smooth-shaders.js`, `src/sim/conveyors.js`,
`src/sim/items.js`, `src/sim/machine-tick.js`, `test/t_diag_screws.js`, `test/t_smooth.py`,
`tools/scenes/t62-joins.js`, `docs/PROJECT_MAP.md`, `docs/DESIGN.md`.
- Tests: `python3 test/run_all.py --browser` all passed; determinism diff changed only the
replaced corner checks.
## 2026-10-27: t62 screw conveyor joins (Claude Code + Codex) ## 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 - Straight screws touching a diagonal's end only at the corner now join, in both looks and in
+5 -11
View File
@@ -5,7 +5,7 @@ for (let p = P_SCREW_UR; p <= P_SCREW_UL; p++) SCREW_CLASS[p] = 19 + p - P_SCREW
// Drawing only: the four transport lanes remain in the square block. // Drawing only: the four transport lanes remain in the square block.
let diagonalJoins = new Uint8Array(0); let diagonalJoins = new Uint8Array(0);
function diagonalScrewJoins(bx, by, v, planned = null) { 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. // Three bits per end: 1..4 edge exits; bits 6/7 are collars.
const dy = v === 4 || v === 6 ? -1 : 1; const dy = v === 4 || v === 6 ? -1 : 1;
let joins = 0; let joins = 0;
for (let end = 0; end < 2; end++) { for (let end = 0; end < 2; end++) {
@@ -23,7 +23,6 @@ function diagonalScrewJoins(bx, by, v, planned = null) {
if ((code === BP_SCREW && nv < 4 && (nv & 1) === (dir & 1)) || if ((code === BP_SCREW && nv < 4 && (nv & 1) === (dir & 1)) ||
((code === BP_BELT || code === BP_FILTER) && (dir & 1))) { join = dir + 1; break; } ((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); joins |= join << (end * 3);
if (!join) joins |= 64 << end; if (!join) joins |= 64 << end;
} }
@@ -37,15 +36,10 @@ function diagonalScrewShape(x, y, b, v, joins) {
for (let end = 0; end < 2; end++) { for (let end = 0; end < 2; end++) {
const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1; const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1;
const slope = v === 4 || v === 6 ? -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 dx = 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 dy = 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 * dx + ly * dy, across = -lx * dy + ly * dx;
const along = (lx - ox) * dx + (ly - oy) * dy, across = -(lx - ox) * dy + (ly - oy) * dx; const radius = Math.hypot(across, Math.min(0, along));
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 cap = along - (join ? 2 : 2 * Math.SQRT2);
const sd = Math.max(radius - 2, join || (joins & (64 << end)) ? cap : -10); const sd = Math.max(radius - 2, join || (joins & (64 << end)) ? cap : -10);
if (sd < best) { if (sd < best) {
+1 -4
View File
@@ -265,12 +265,9 @@ ${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);
vec2 offset = join == 5 ? vec2(0, side * d.y * 4.0) : join == 6 ? vec2(side * 4.0, 0) : vec2(0); float t = dot(local, ray), cross2 = dot(local, vec2(-ray.y, ray.x));
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);
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. // 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); float sd = max(radius - 2.0, (joins & (64 << end)) != 0 ? cap : -10.0);
if (sd < screwSD) { if (sd < screwSD) {
+6 -16
View File
@@ -176,20 +176,6 @@ 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) {
@@ -198,11 +184,15 @@ function diagonalScrewNext(i, p) {
if (xx < 0 || xx >= W) return -1; if (xx < 0 || xx >= W) return -1;
if ((xx >> 2) === (x >> 2)) return i + dx; if ((xx >> 2) === (x >> 2)) return i + dx;
const yy = y + dy * BS; const yy = y + dy * BS;
if (yy >= 0 && yy < H && IS_SCREW[part[yy * W + xx]]) return yy * W + xx; if (yy >= 0 && yy < H) {
const q = part[yy * W + xx];
if (q >= P_SCREW_UR && q <= P_SCREW_UL && SCREW_DX[q] === dx && SCREW_DY[q] === dy) return yy * W + xx;
}
if (IS_SCREW[part[y * W + xx]]) return y * W + xx; if (IS_SCREW[part[y * W + xx]]) return y * W + xx;
if (dy > 0 && yy < H && (mat[yy * W + xx] === BELTL || mat[yy * W + xx] === BELTR)) return ((yy & ~3) - 1) * W + xx; if (dy > 0 && yy < H && (mat[yy * W + xx] === BELTL || mat[yy * W + xx] === BELTR)) return ((yy & ~3) - 1) * W + xx;
const hy = by + (dy > 0 ? BS : -1), hx = bx + (y & 3); const hy = by + (dy > 0 ? BS : -1), hx = bx + (y & 3);
if (hy >= 0 && hy < H && IS_SCREW[part[hy * W + hx]]) return hy * W + hx; if (hy >= 0 && hy < H && IS_SCREW[part[hy * W + hx]]) return hy * W + hx;
// An open mouth discharges along the diagonal, clear of the last block. // An open mouth discharges along the diagonal, clear of the last block.
return yy >= 0 && yy < H ? yy * W + xx : -1; // A corner screw is a separate end, not an open discharge cell.
return yy >= 0 && yy < H && !IS_SCREW[part[yy * W + xx]] ? yy * W + xx : -1;
} }
+1 -8
View File
@@ -90,13 +90,6 @@ 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);
@@ -124,7 +117,7 @@ function entDiagonalScrewTick(e, p) {
const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3; const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3;
for (const [ox, oy] of [[dx, dy], [dx, 0], [0, dy]]) { for (const [ox, oy] of [[dx, dy], [dx, 0], [0, dy]]) {
const x = bx + ox * BS, y = by + oy * BS + lane, q = entScrewAt(x, y); const x = bx + ox * BS, y = by + oy * BS + lane, q = entScrewAt(x, y);
if (!q) continue; if (!q || (ox && oy && (q < P_SCREW_UR || q > P_SCREW_UL || SCREW_DX[q] !== dx || SCREW_DY[q] !== dy))) continue;
if (SCREW_DX[q] * dx + SCREW_DY[q] * dy < 0) return; if (SCREW_DX[q] * dx + SCREW_DY[q] * dy < 0) return;
if (entScrewMove(e, x, y, true)) stats.moved++; if (entScrewMove(e, x, y, true)) stats.moved++;
return; return;
+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) : straightScrewNext(i, p); const dest = dx && dy ? diagonalScrewNext(i, p) : i + dj;
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;
+30 -34
View File
@@ -48,38 +48,35 @@ 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. // Corner placements are separate mouths, on both axes and in either transfer direction.
function cornerLine(v, vertical = false) { for (const v of [4,5,6,7]) for (const vertical of [false,true]) for (const fromDiagonal of [false,true]) {
const s = line(v), vv = vertical ? (s.dy < 0 ? 0 : 2) : (s.dx > 0 ? 1 : 3); fresh();
const tx = s.bx-s.dx, ty = s.by-s.dy, hx = s.head+s.dx, hy = s.hy+s.dy; const [dx,dy]=run(`SCREW_DIRS[${v}]`), sv=vertical?(dy<0?0:2):(dx>0?1:3);
run(`for(let k=0;k<3;k++) { const sx=20+(fromDiagonal?0:-dx), sy=20+(fromDiagonal?0:-dy);
placeBlock(BP_SCREW,${vv},${tx}-k*${vertical?0:s.dx},${ty}-k*${vertical?s.dy:0},true); const tx=20+(fromDiagonal?dx:0), ty=20+(fromDiagonal?dy:0);
placeBlock(BP_SCREW,${vv},${hx}+k*${vertical?0:s.dx},${hy}+k*${vertical?s.dy:0},true); const source=fromDiagonal?v:sv, target=fromDiagonal?sv:v;
}`); const label=`corner ${vertical?'vertical':'horizontal'} ${v} ${fromDiagonal?'diagonal to straight':'straight to diagonal'}`;
return {...s, tail:tx-2*(vertical?0:s.dx), by:ty-2*(vertical?s.dy:0), run(`placeBlock(BP_SCREW,${source},${sx},${sy},true);placeBlock(BP_SCREW,${target},${tx},${ty},true);rebuildPower();`);
head:hx+2*(vertical?0:s.dx), hy:hy+2*(vertical?s.dy:0)}; rect(run,sx*4,sy*4,sx*4+3,sy*4+3,run('SAND'));
} const grains=run(`(()=>{let crossed=0;for(let k=0;k<80;k++){
for(const v of [4,5,6,7]) for(const vertical of [false,true]) { tick+=2;tickStamp++;machinesTick();
const s=cornerLine(v,vertical); for(let i=0;i<N;i++)if(mat[i]===SAND){
rect(run,s.tail*4,s.by*4,s.tail*4+3,s.by*4+3,run("SAND")); const x=i%W>>2,y=(i/W|0)>>2;
const r=run(`(()=>{let out=0,leak=0;for(let k=0;k<700;k++){ if(x===${tx} && y===${ty})crossed++;
simTick(); if(x!==${sx} || y!==${sy})setCell(i,EMPTY,20,0);
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=[]; }flyers=[];
}return {out,leak,inside:mat.reduce((n,m)=>n+(m===SAND),0)};})()`); }return crossed;})()`);
check(`corner ${vertical?"vertical":"horizontal"} joins ${v} carry every grain to the far mouth`,r.out===16 && !r.leak,JSON.stringify(r)); check(`${label} does not pass grains`,grains===0,grains);
cornerLine(v,vertical); run('mat.forEach((m,i)=>{if(m===SAND)setCell(i,EMPTY,20,0);});');
putBar(s.tail*4,s.by*4); putBar(s.tail*4,s.by*4+2); for(const lane of [0,1,2]) {
const bars=run(`(()=>{let out=0,overlap=false;for(let k=0;k<700;k++){ putBar(sx*4,sy*4+lane);
simTick(); const bar=run(`(()=>{const e=ents[0];let crossed=false;for(let k=0;k<80;k++){
for(let a=0;a<ents.length;a++)for(let b=0;b<a;b++) { tick+=8;entMark(e,0);entScrewTick(e,SCREW_PARTS[${source}]);entMark(e,1);
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((e.ix>>2)===${tx} && (e.iy>>2)===${ty})crossed=true;
} if(!entScrewAt(e.ix,e.iy))break;
if(mat.some((m,i)=>occ[i] && (PH[m]===PH_SOLID || PH[m]===PH_POWDER)))overlap=true; }removeEnt(e);return crossed;})()`);
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);} check(`${label} does not pass a bar in lane ${lane}`,!bar);
}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 = [];
@@ -208,9 +205,8 @@ for (const v of [4,5,6,7]) for (const end of [-1,1]) for (let dir=0;dir<4;dir++)
fresh(); fresh();
const slope=v===4 || v===6?-1:1, nx=20+end, ny=20+end*slope, shift=end>0?3:0; 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);`); 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} keeps its free-end collar`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});return (j>>${shift}&7)===0 && !!(j&${end>0?128:64});})()`));
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} leaves the free-end geometry unchanged`,run(`(()=>{const b=20*BW+20,j=diagonalScrewJoins(20,20,${v});removeBlock(${nx},${ny});const alone=diagonalScrewJoins(20,20,${v});placeBlock(BP_SCREW,${dir},${nx},${ny},true);return j===alone && [0.5,1.5,2.5,3.5].every(t=>diagonalScrewShape(82+${end}*2,80+t,b,${v},j)===diagonalScrewShape(82+${end}*2,80+t,b,${v},alone));})()`));
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;})()`)); 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']) {
+43 -19
View File
@@ -97,34 +97,58 @@ 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. # Edge joins keep closed elbows; corner placements match two isolated free ends.
pg.set_viewport_size({"width": 2100, "height": 1300}) 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("part.fill(0); blockP.fill(0); occ.fill(0); mach.fill(0); ents=[]; ghostBlocks.clear();")
ev("() => {" + (root / "tools/scenes/t62-joins.js").read_text() + "}") ev("() => {" + (root / "tools/scenes/t62-joins.js").read_text() + "}")
mouths = ev('''() => { mouths = ev('''() => {
SET.zoom=6; SET.lighting="off"; SET.glow="off"; resizeRender(); 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(); bot.x=CAM[0]-bot.w/2;bot.y=CAM[1]-bot.h/2;snapCamera();
const out={}; const now=performance.now.bind(performance),frozen=now();
for(const look of ["pixel","smooth"]) { performance.now=()=>frozen;
SET.look=look;SM.scale=1;drawWorld(); try {
const bad=[]; const out={};
for(const j of T62_JOINTS) { const draw=()=>{SM.scale=1;drawWorld();};
const slope=j.v===4 || j.v===6?-1:1; for(const look of ["pixel","smooth"]) {
const end=j.end*SCREW_DIRS[j.v][0]; SET.look=look;draw();
const dx=j.vertical?0:end,dy=j.vertical?end*slope:0; const bad=[];
const ox=j.corner && j.vertical?end*4:0,oy=j.corner && !j.vertical?end*slope*4:0; for(const j of T62_JOINTS) {
for(const side of [-1,1])for(const along of [-0.3,0.3]) { const slope=j.v===4 || j.v===6?-1:1;
const x=j.bx*4+2+ox+dx*(2+along)-dy*side*1.6; const end=j.end*SCREW_DIRS[j.v][0];
const y=j.by*4+2+oy+dy*(2+along)+dx*side*1.6; const dx=j.vertical?0:end,dy=j.vertical?end*slope:0;
const p=worldToScreenPx(x,y),c=R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data; if(j.corner)continue;
if(!(c[0]>c[1]*1.15 && c[1]>c[2]*1.1))bad.push([j,x,y,Array.from(c)]); for(const side of [-1,1])for(const along of [-0.3,0.3]) {
const x=j.bx*4+2+dx*(2+along)-dy*side*1.6;
const y=j.by*4+2+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)]);
}
} }
} const sample=(bx,by)=>{
out[look]=bad; const colors=[];
}return out; for(let y=0;y<4;y++)for(let x=0;x<4;x++) {
const p=worldToScreenPx(bx*4+x+0.5,by*4+y+0.5);
colors.push(...R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);
}return colors.join(',');
};
for(const j of T62_JOINTS.filter(j=>j.corner)) {
const [dx,dy]=SCREW_DIRS[j.v],sx=j.bx+j.end*dx,sy=j.by+j.end*dy;
const collar=sample(j.bx,j.by),straight=sample(sx,sy);
const joins=diagonalScrewJoins(j.bx,j.by,j.v),end=j.end*dx>0?1:0;
if(!(joins & (64<<end)) || (joins>>(end*3)&7))bad.push(['missing collar',j]);
removeBlock(sx,sy);draw();
if(sample(j.bx,j.by)!==collar)bad.push(['diagonal differs from isolated end',j]);
placeBlock(BP_SCREW,j.straight,sx,sy,true);
removeBlock(j.bx,j.by);draw();
if(sample(sx,sy)!==straight)bad.push(['straight differs from normal end',j]);
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);draw();
}
out[look]=bad;
}return out;
} finally {performance.now=now;}
}''') }''')
for look, bad in mouths.items(): for look, bad in mouths.items():
check(f"{look}: all 64 screw joints have closed casing across both mouth edges", not bad, bad[:2]) check(f"{look}: edge elbows stay closed and corners show two separate free ends", 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();')
+4 -3
View File
@@ -1,6 +1,7 @@
// Self-contained grid. Rows: UR, DR, DL, UL. Each run has both end joins. // Self-contained grid. Rows: UR, DR, DL, UL. Each run shows both ends.
// Columns: horizontal edge, horizontal corner, vertical edge, vertical corner; // Columns: horizontal edge, horizontal corner, vertical edge, vertical corner.
// repeat those four with the straight travel reversed. Camera fits --zoom 6 --viewport 2100x1300. // Corner columns stay unconnected, with two separate ends.
// 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. // Run after clear-sky.js or by itself. Empty, paused screws expose the whole casing.
money = 50000; money = 50000;
for (let y = 64; y < 280; y++) for (let x = 112; x < 456; x++) { for (let y = 64; y < 280; y++) for (let x = 112; x < 456; x++) {