From 787721ad354ee0e33c180c75d6ca2f11f0452526 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Tue, 27 Oct 2026 12:32:23 -0400 Subject: [PATCH] Corner placements of screw conveyors stay separate ends (t62 follow-up) Co-Authored-By: Claude Opus 5.5 --- docs/DESIGN.md | 5 +++ docs/PROJECT_MAP.md | 12 +++---- docs/agent-log.md | 13 +++++++ src/render/diagonal-screws.js | 16 +++------ src/render/smooth-shaders.js | 5 +-- src/sim/conveyors.js | 22 ++++-------- src/sim/items.js | 9 +---- src/sim/machine-tick.js | 2 +- test/t_diag_screws.js | 64 ++++++++++++++++------------------- test/t_smooth.py | 62 ++++++++++++++++++++++----------- tools/scenes/t62-joins.js | 7 ++-- 11 files changed, 115 insertions(+), 102 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1268b85..9928fd1 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -486,6 +486,11 @@ above, including: 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 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 diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index ffdbc46..15d55b3 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -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). - 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. -- 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 ### 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 - 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. - 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 @@ -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 - 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 - functions: diagonalScrewJoins, diagonalScrewShape, diagonalScrewBlock - 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. - 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. - 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/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. +- `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. diff --git a/docs/agent-log.md b/docs/agent-log.md index 13f3804..4e8f9a0 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -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 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) - Straight screws touching a diagonal's end only at the corner now join, in both looks and in diff --git a/src/render/diagonal-screws.js b/src/render/diagonal-screws.js index 96780cc..e90bb90 100644 --- a/src/render/diagonal-screws.js +++ b/src/render/diagonal-screws.js @@ -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. 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. + // Three bits per end: 1..4 edge exits; bits 6/7 are collars. const dy = v === 4 || v === 6 ? -1 : 1; let joins = 0; 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)) || ((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; } @@ -37,15 +36,10 @@ function diagonalScrewShape(x, y, b, v, joins) { 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 dx = join ? SCREW_DIRS[join - 1][0] : side * Math.SQRT1_2; + const dy = join ? SCREW_DIRS[join - 1][1] : side * slope * Math.SQRT1_2; + const along = lx * dx + ly * dy, across = -lx * dy + ly * dx; + const radius = Math.hypot(across, Math.min(0, along)); const cap = along - (join ? 2 : 2 * Math.SQRT2); const sd = Math.max(radius - 2, join || (joins & (64 << end)) ? cap : -10); if (sd < best) { diff --git a/src/render/smooth-shaders.js b/src/render/smooth-shaders.js index 89a25bb..7dfbbb8 100644 --- a/src/render/smooth-shaders.js +++ b/src/render/smooth-shaders.js @@ -265,12 +265,9 @@ ${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); - 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 t = dot(local, ray), cross2 = dot(local, vec2(-ray.y, ray.x)); 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. float sd = max(radius - 2.0, (joins & (64 << end)) != 0 ? cap : -10.0); if (sd < screwSD) { diff --git a/src/sim/conveyors.js b/src/sim/conveyors.js index 6705537..11a3fea 100644 --- a/src/sim/conveyors.js +++ b/src/sim/conveyors.js @@ -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. // Prefer the next diagonal block, then either straight join, then the open head. function diagonalScrewNext(i, p) { @@ -198,11 +184,15 @@ function diagonalScrewNext(i, p) { if (xx < 0 || xx >= W) return -1; if ((xx >> 2) === (x >> 2)) return i + dx; 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 (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); 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. - 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; } diff --git a/src/sim/items.js b/src/sim/items.js index 5d35758..8811d26 100644 --- a/src/sim/items.js +++ b/src/sim/items.js @@ -90,13 +90,6 @@ 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); @@ -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; 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); - 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 (entScrewMove(e, x, y, true)) stats.moved++; return; diff --git a/src/sim/machine-tick.js b/src/sim/machine-tick.js index a0ca921..ce3984e 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) : straightScrewNext(i, p); + const dest = dx && dy ? diagonalScrewNext(i, p) : i + dj; 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/test/t_diag_screws.js b/test/t_diag_screws.js index ca14aac..708f18e 100644 --- a/test/t_diag_screws.js +++ b/test/t_diag_screws.js @@ -48,38 +48,35 @@ 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++; +// Corner placements are separate mouths, on both axes and in either transfer direction. +for (const v of [4,5,6,7]) for (const vertical of [false,true]) for (const fromDiagonal of [false,true]) { + fresh(); + const [dx,dy]=run(`SCREW_DIRS[${v}]`), sv=vertical?(dy<0?0:2):(dx>0?1:3); + const sx=20+(fromDiagonal?0:-dx), sy=20+(fromDiagonal?0:-dy); + const tx=20+(fromDiagonal?dx:0), ty=20+(fromDiagonal?dy:0); + const source=fromDiagonal?v:sv, target=fromDiagonal?sv:v; + const label=`corner ${vertical?'vertical':'horizontal'} ${v} ${fromDiagonal?'diagonal to straight':'straight to diagonal'}`; + run(`placeBlock(BP_SCREW,${source},${sx},${sy},true);placeBlock(BP_SCREW,${target},${tx},${ty},true);rebuildPower();`); + 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++){ + tick+=2;tickStamp++;machinesTick(); + for(let i=0;i>2,y=(i/W|0)>>2; + if(x===${tx} && y===${ty})crossed++; + if(x!==${sx} || y!==${sy})setCell(i,EMPTY,20,0); }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)); + }return crossed;})()`); + check(`${label} does not pass grains`,grains===0,grains); + run('mat.forEach((m,i)=>{if(m===SAND)setCell(i,EMPTY,20,0);});'); + for(const lane of [0,1,2]) { + putBar(sx*4,sy*4+lane); + const bar=run(`(()=>{const e=ents[0];let crossed=false;for(let k=0;k<80;k++){ + tick+=8;entMark(e,0);entScrewTick(e,SCREW_PARTS[${source}]);entMark(e,1); + if((e.ix>>2)===${tx} && (e.iy>>2)===${ty})crossed=true; + if(!entScrewAt(e.ix,e.iy))break; + }removeEnt(e);return crossed;})()`); + check(`${label} does not pass a bar in lane ${lane}`,!bar); + } } // A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz. 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(); 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} 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} 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} 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']) { diff --git a/test/t_smooth.py b/test/t_smooth.py index f624059..8e017a4 100644 --- a/test/t_smooth.py +++ b/test/t_smooth.py @@ -97,34 +97,58 @@ 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. + # Edge joins keep closed elbows; corner placements match two isolated free ends. 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)]); + const now=performance.now.bind(performance),frozen=now(); + performance.now=()=>frozen; + try { + const out={}; + const draw=()=>{SM.scale=1;drawWorld();}; + for(const look of ["pixel","smooth"]) { + SET.look=look;draw(); + 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; + if(j.corner)continue; + 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)]); + } } - } - out[look]=bad; - }return out; + const sample=(bx,by)=>{ + const colors=[]; + 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*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(): - 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 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 index 5f3bc5f..2014bb9 100644 --- a/tools/scenes/t62-joins.js +++ b/tools/scenes/t62-joins.js @@ -1,6 +1,7 @@ -// 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. +// 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. money = 50000; for (let y = 64; y < 280; y++) for (let x = 112; x < 456; x++) {