From ff9fd7b1b878b0f3eb77d1901be34d5a075e0326 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Tue, 27 Oct 2026 12:12:44 -0400 Subject: [PATCH] Screw conveyors join across the corner to diagonals (t62) Co-Authored-By: Claude Opus 5.5 --- docs/PROJECT_MAP.md | 22 ++++++---- docs/agent-log.md | 15 +++++++ src/manifest.json | 1 + src/render/diagonal-screws.js | 74 ++++++++++++++++++++++++++++++++ src/render/smooth-shaders.js | 8 +++- src/render/world.js | 80 ++++++----------------------------- src/sim/conveyors.js | 14 ++++++ src/sim/items.js | 7 +++ src/sim/machine-tick.js | 2 +- src/ui/previews.js | 29 ++++--------- test/t_diag_screws.js | 43 +++++++++++++++++++ test/t_smooth.py | 28 ++++++++++++ tools/scenes/t62-joins.js | 26 ++++++++++++ 13 files changed, 250 insertions(+), 99 deletions(-) create mode 100644 src/render/diagonal-screws.js create mode 100644 tools/scenes/t62-joins.js diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index d7f96ca..ffdbc46 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -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. diff --git a/docs/agent-log.md b/docs/agent-log.md index a98c2ae..13f3804 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -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 diff --git a/src/manifest.json b/src/manifest.json index c3cb68b..c2adb70 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -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", diff --git a/src/render/diagonal-screws.js b/src/render/diagonal-screws.js new file mode 100644 index 0000000..96780cc --- /dev/null +++ b/src/render/diagonal-screws.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; +} diff --git a/src/render/smooth-shaders.js b/src/render/smooth-shaders.js index 31e8dbe..89a25bb 100644 --- a/src/render/smooth-shaders.js +++ b/src/render/smooth-shaders.js @@ -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); diff --git a/src/render/world.js b/src/render/world.js index 265244e..77ec99a 100644 --- a/src/render/world.js +++ b/src/render/world.js @@ -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); diff --git a/src/sim/conveyors.js b/src/sim/conveyors.js index 90d2403..6705537 100644 --- a/src/sim/conveyors.js +++ b/src/sim/conveyors.js @@ -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) { diff --git a/src/sim/items.js b/src/sim/items.js index 3135ab9..5d35758 100644 --- a/src/sim/items.js +++ b/src/sim/items.js @@ -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); diff --git a/src/sim/machine-tick.js b/src/sim/machine-tick.js index ce3984e..a0ca921 100644 --- a/src/sim/machine-tick.js +++ b/src/sim/machine-tick.js @@ -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; diff --git a/src/ui/previews.js b/src/ui/previews.js index b462dd5..6846581 100644 --- a/src/ui/previews.js +++ b/src/ui/previews.js @@ -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; diff --git a/test/t_diag_screws.js b/test/t_diag_screws.js index d019ef7..ca14aac 100644 --- a/test/t_diag_screws.js +++ b/test/t_diag_screws.js @@ -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;aocc[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(); diff --git a/test/t_smooth.py b/test/t_smooth.py index 0d10257..f624059 100644 --- a/test/t_smooth.py +++ b/test/t_smooth.py @@ -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();') diff --git a/tools/scenes/t62-joins.js b/tools/scenes/t62-joins.js new file mode 100644 index 0000000..5f3bc5f --- /dev/null +++ b/tools/scenes/t62-joins.js @@ -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 = () => {};