Screw conveyors join across the corner to diagonals (t62)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+14
-8
@@ -134,6 +134,7 @@ dist/, shots/ build output and screenshots (not committed)
|
||||
|
||||
- **The pixel look:**
|
||||
- `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;
|
||||
- overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards
|
||||
(`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).
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
### 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
|
||||
- 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.
|
||||
- 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
|
||||
@@ -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
|
||||
- variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner
|
||||
|
||||
### src/render/world.js (458 lines)
|
||||
Scale colour without changing the smooth-look class.
|
||||
- functions: fogColor, diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock, drawWorld
|
||||
### src/render/diagonal-screws.js (74 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
|
||||
- functions: diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock
|
||||
- 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)
|
||||
Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
|
||||
- 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.
|
||||
- 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.
|
||||
- variables: SM_VS, SM_FS
|
||||
|
||||
@@ -481,7 +486,7 @@ Gamepad input.
|
||||
- functions: padPressed, pollPad
|
||||
- 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.
|
||||
- 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/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/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.
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
- Owner decision: the console ships in production too, with a warning when it's turned on that
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"render/setup.js",
|
||||
"render/light.js",
|
||||
"render/slope-belts.js",
|
||||
"render/diagonal-screws.js",
|
||||
"render/world.js",
|
||||
"render/machines.js",
|
||||
"render/overlays.js",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -265,10 +265,14 @@ ${SM_SLOPE_DRAW}
|
||||
int join = (joins >> (end * 3)) & 7;
|
||||
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 :
|
||||
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);
|
||||
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 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) {
|
||||
screwSD = sd;
|
||||
a = 2.0 + (side * cross2 < 0.0 ? -radius : radius);
|
||||
|
||||
+12
-68
@@ -2,71 +2,6 @@
|
||||
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.
|
||||
// ---------- 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() {
|
||||
const bw = R.bw, bh = R.bh, px = R.px;
|
||||
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;
|
||||
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; }
|
||||
const slope = isSlopeBelt(code), joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : 0;
|
||||
for (let dy = slope ? -9 : 0; dy < (slope ? 8 : BS); dy++) for (let dx = slope ? -5 : 0; dx < (slope ? 9 : BS); dx++) {
|
||||
const slope = isSlopeBelt(code), diagonal = code === BP_SCREW && v >= 4;
|
||||
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;
|
||||
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) {
|
||||
const x = gbx * BS + dx, y = gby * BS + dy;
|
||||
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;
|
||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||
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 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);
|
||||
|
||||
@@ -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.
|
||||
// Prefer the next diagonal block, then either straight join, then the open head.
|
||||
function diagonalScrewNext(i, p) {
|
||||
|
||||
@@ -90,6 +90,13 @@ function entScrewTick(e, p) {
|
||||
if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return;
|
||||
const dx = SCREW_DX[p], dy = SCREW_DY[p];
|
||||
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;
|
||||
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
||||
const i = entScrewEdge(e, dx, dy, k);
|
||||
|
||||
@@ -130,7 +130,7 @@ function machinesTick() {
|
||||
if (shifted) 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;
|
||||
const nx = dest % W, ny = (dest / W) | 0;
|
||||
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
||||
|
||||
+9
-20
@@ -182,27 +182,16 @@ function previewBlock(code, x, y, w, h, stroke = false, direction = -1, bx = -1,
|
||||
const ctx = R.ctx;
|
||||
if (code === BP_SCREW && direction >= 4) {
|
||||
const joins = bx >= 0 ? diagonalScrewJoins(bx, by, direction, planned) : 0;
|
||||
const points = [];
|
||||
for (let end = 0; end < 2; end++) {
|
||||
const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1;
|
||||
const d = join ? SCREW_DIRS[join - 1] : [side, side * (direction === 4 || direction === 6 ? -1 : 1)];
|
||||
points.push([d[0] * w / 2, d[1] * h / 2]);
|
||||
const scale = w / BS, step = SET.look === "smooth" ? 0.25 : 1;
|
||||
const b = bx >= 0 ? by * BW + bx : 0, ox = (b % BW) * BS, oy = ((b / BW) | 0) * BS;
|
||||
ctx.beginPath();
|
||||
for (let yy = -4; yy < 8; yy += step) for (let xx = -4; xx < 8; xx += step) {
|
||||
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];
|
||||
const al = Math.hypot(...a), bl = Math.hypot(...b);
|
||||
const n0 = [a[1] / al, -a[0] / al], n1 = [-b[1] / bl, b[0] / bl];
|
||||
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 (stroke) { const fill = ctx.fillStyle; ctx.fillStyle = ctx.strokeStyle; ctx.fill(); ctx.fillStyle = fill; }
|
||||
else ctx.fill();
|
||||
return;
|
||||
}
|
||||
if (isSlopeBelt(code)) {
|
||||
const b = by * BW + bx, joins = bx >= 0 ? slopeBeltConnections(bx, by, code, planned) : 0;
|
||||
|
||||
@@ -48,6 +48,39 @@ for (const v of [4, 5, 6, 7]) {
|
||||
const joined = drain(c,500);
|
||||
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.
|
||||
const rates = [];
|
||||
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)})`));
|
||||
}
|
||||
// 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'));
|
||||
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']) {
|
||||
if (code !== 'BP_SCREW' && !(dir & 1)) continue;
|
||||
fresh();
|
||||
|
||||
@@ -97,6 +97,34 @@ with sync_playwright() as p:
|
||||
}''')
|
||||
check('sidewall stays behind the grain colour in pixels and smooth',
|
||||
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
|
||||
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
|
||||
pg2.add_init_script('localStorage.clear();')
|
||||
|
||||
@@ -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 = () => {};
|
||||
Reference in New Issue
Block a user