Diagonal tubes as thick as straight ones, with centred elbows where they join (t35)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 22:40:52 -04:00
co-authored by Claude Opus 5.5
parent 814b51df46
commit 24eb9559f8
8 changed files with 171 additions and 44 deletions
+7 -6
View File
@@ -356,10 +356,10 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
Propagated coloured light from glowing and burning cells. Propagated coloured light from glowing and burning cells.
- functions: builtForLight, looseForLight, computeLight - functions: builtForLight, looseForLight, computeLight
### src/render/world.js (361 lines) ### src/render/world.js (410 lines)
Scale colour without changing the smooth-look class. Scale colour without changing the smooth-look class.
- functions: fogColor, diagonalTubeBlock, drawWorld - functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld
- variables: TUBE_CLASS - variables: TUBE_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
### src/render/machines.js (242 lines) ### src/render/machines.js (242 lines)
Machine overlays: fans, engines, silo and the rest, drawn on top of the world. Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
@@ -374,7 +374,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace - functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes - variables: SM, smBytes
### src/render/smooth-shaders.js (348 lines) ### src/render/smooth-shaders.js (366 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
@@ -467,7 +467,7 @@ Gamepad input.
- functions: padPressed, pollPad - functions: padPressed, pollPad
- variables: PAD - variables: PAD
### src/ui/previews.js (199 lines) ### src/ui/previews.js (216 lines)
Cursor previews: shared grid helpers, air streaks and tool preview dispatch. Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock - functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
@@ -476,7 +476,7 @@ Drill previews: shaken rock cracks, pulse wavefronts and aiming fan. Rock the dr
- functions: crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim - functions: crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim
- variables: CRACK_N, CRACK_LV - variables: CRACK_N, CRACK_LV
### src/ui/preview-selection.js (80 lines) ### src/ui/preview-selection.js (81 lines)
Selection previews: labelled tags, copy and cut boxes, and paste ghosts. a small labelled tag drawn on the world (screen pixels) Selection previews: labelled tags, copy and cut boxes, and paste ghosts. a small labelled tag drawn on the world (screen pixels)
- functions: drawTag, drawSelection, drawPastePreview - functions: drawTag, drawSelection, drawPastePreview
- variables: SEL_COL - variables: SEL_COL
@@ -547,4 +547,5 @@ The frame loop and boot.
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7 - `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
- `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket. - `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
- `tools/scenes/t35-diagonal.js`: Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt. - `tools/scenes/t35-diagonal.js`: Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt.
- `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.
+8
View File
@@ -4,6 +4,14 @@ 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-25: t35 follow-up: diagonal tube elbows (Claude Code, Codex)
- Diagonal tubes are as thick as straight ones, and an end block joining a straight tube, belt or
filter draws an elbow at its centre, so the pipes meet centred and edge to edge; free ends get a
collar; ghosts match (`src/render/world.js`, `smooth-shaders.js`, `src/ui/previews.js`,
`preview-selection.js`). Scene `tools/scenes/t35-joins.js`.
- `test/t_diag_tubes.js`; `run_all.py --browser` ALL PASSED.
## 2026-10-25: t35/t36 follow-ups: wall drags make slopes, diagonal tube art (Claude Code, Codex) ## 2026-10-25: t35/t36 follow-ups: wall drags make slopes, diagonal tube art (Claude Code, Codex)
- Wall piece: a diagonal drag lays slopes (`pieceCode`/`turnSlope` in `src/ui/selection.js`), T flips - Wall piece: a diagonal drag lays slopes (`pieceCode`/`turnSlope` in `src/ui/selection.js`), T flips
+27 -9
View File
@@ -51,7 +51,8 @@ float beltSD(vec2 lb, bool endL, bool endR) {
// the line only rounds off where the whole run ends. // the line only rounds off where the whole run ends.
float beltLineSD(ivec2 c, vec2 lb) { float beltLineSD(ivec2 c, vec2 lb) {
int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0))); int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0)));
return beltSD(lb, isBelt(nl) + isF(nl) < 0.5, isBelt(nr) + isF(nr) < 0.5); return beltSD(lb, isBelt(nl) + isF(nl) < 0.5 && !(nl >= 19 && nl <= 22),
isBelt(nr) + isF(nr) < 0.5 && !(nr >= 19 && nr <= 22));
} }
// The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and // The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and
// back along the bottom. // back along the bottom.
@@ -250,18 +251,35 @@ void main() {
bool vert = nc == 9 || nc == 11; bool vert = nc == 9 || nc == 11;
float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0; float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0;
float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x; float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
float tubeSD = -1.0, capRing = 0.0;
if (nc >= 19) { if (nc >= 19) {
vec2 d = nc == 19 ? vec2(1.0, -1.0) : nc == 20 ? vec2(1.0, 1.0) : nc == 21 ? vec2(-1.0, 1.0) : vec2(-1.0, -1.0); vec2 d = vec2(1.0, nc == 19 || nc == 21 ? -1.0 : 1.0);
float offset = cellAt(tTree, c).r * 255.0 - 128.0; vec4 tag = cellAt(tTree, c);
bool rising = nc == 19 || nc == 21; vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w) - 2.0;
a = 2.0 + (offset + dot(fract(w) - 0.5, vec2(1.0, rising ? 1.0 : -1.0))) * 0.70710678; int joins = int(tag.a * 255.0 + 0.5);
along = dot(w, d) * 0.70710678; sp2 = 1.0; tubeSD = 100.0;
for (int end = 0; end < 2; end++) {
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 == 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));
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 (sd < tubeSD) {
tubeSD = sd;
a = 2.0 + (side * cross2 < 0.0 ? -radius : radius);
along = dot(w - local, d) * 0.70710678 + side * t;
capRing = (joins & (64 << end)) != 0 ? smoothstep(-0.55, -0.15, cap) : 0.0;
}
}
sp2 = nc == 19 || nc == 20 ? 1.0 : -1.0;
} }
float wall = 0.62; float wall = 0.62;
vec3 tc; vec3 tc;
if (a < wall || a > 4.0 - wall) { if (a < wall || a > 4.0 - wall || capRing > 0.5) {
float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore
float cyl = sin(u2 * 1.5708); float cyl = sin(clamp(u2, 0.0, 1.0) * 1.5708);
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl); tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
if (a < wall) tc *= 1.08; if (a < wall) tc *= 1.08;
float ring = 1.0 - smoothstep(0.0, 0.3, min(fract(along * 0.25), 1.0 - fract(along * 0.25)) * 4.0); float ring = 1.0 - smoothstep(0.0, 0.3, min(fract(along * 0.25), 1.0 - fract(along * 0.25)) * 4.0);
@@ -279,7 +297,7 @@ void main() {
if (nc >= 19) { if (nc >= 19) {
bool full = own.r + own.g + own.b > 0.0; bool full = own.r + own.g + own.b > 0.0;
if (full) C = mix(C, own.rgb, 1.0 - smoothstep(0.36, 0.5, length(fract(w) - 0.5))); if (full) C = mix(C, own.rgb, 1.0 - smoothstep(0.36, 0.5, length(fract(w) - 0.5)));
C = mix(C, tc, 1.0 - smoothstep(2.0 - px, 2.0 + px, abs(a - 2.0))); C = mix(C, tc, 1.0 - smoothstep(2.0 - px, 2.0 + px, tubeSD + 2.0));
} else C = tc; } else C = tc;
} }
else if (nc == 6 || (nc >= 15 && nc <= 18)) { else if (nc == 6 || (nc >= 15 && nc <= 18)) {
+67 -18
View File
@@ -6,25 +6,66 @@ function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 1
const TUBE_CLASS = new Uint8Array(15); TUBE_CLASS[1] = 9; TUBE_CLASS[4] = 10; TUBE_CLASS[2] = 11; TUBE_CLASS[3] = 12; const TUBE_CLASS = new Uint8Array(15); TUBE_CLASS[1] = 9; TUBE_CLASS[4] = 10; TUBE_CLASS[2] = 11; TUBE_CLASS[3] = 12;
for (let p = P_TUR; p <= P_TUL; p++) TUBE_CLASS[p] = 19 + p - P_TUR; for (let p = P_TUR; p <= P_TUL; p++) TUBE_CLASS[p] = 19 + p - P_TUR;
// 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);
function diagonalTubeJoins(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_TUBE && 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 + TUBE_DIRS[dir][0], y = by + TUBE_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_TUBE && 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 diagonalTubeShape(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 ? TUBE_DIRS[join - 1][0] : side * Math.SQRT1_2;
const dy = join ? TUBE_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 diagonalTubeBlock(x, y, smooth) { function diagonalTubeBlock(x, y, smooth) {
const i = y * W + x, own = (y >> 2) * BW + (x >> 2); const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
if (part[i] >= P_TUR && part[i] <= P_TUL) return own; if (part[i] >= P_TUR && part[i] <= P_TUL) return own;
if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[own]) return -1; if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[own]) return -1;
// Look in the four adjoining blocks for a pipe crossing this empty corner. let best = smooth ? 0.75 : 0, found = -1;
for (let k = 0; k < 4; k++) { for (let k = 0; k < 4; k++) {
const bx = (x >> 2) + (k === 0 ? -1 : k === 1 ? 1 : 0); const bx = (x >> 2) + (k === 0 ? -1 : k === 1 ? 1 : 0);
const by = (y >> 2) + (k === 2 ? -1 : k === 3 ? 1 : 0); const by = (y >> 2) + (k === 2 ? -1 : k === 3 ? 1 : 0);
if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue; if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue;
const b = by * BW + bx, v = blockV[b]; const b = by * BW + bx, v = blockV[b];
if (blockP[b] !== BP_TUBE || v < 4) continue; if (blockP[b] !== BP_TUBE || v < 4) continue;
const rising = v === 4 || v === 6, lx = x - bx * BS, ly = rising ? 3 - (y - by * BS) : y - by * BS; const joins = diagonalJoins[b] === undefined ? diagonalTubeJoins(bx, by, v) : diagonalJoins[b];
if (Math.abs(lx - ly) > (smooth ? 3 : 1)) continue; const sd = diagonalTubeShape(x + 0.5, y + 0.5, b, v, joins);
const side = lx + ly < 3 ? -1 : 1, nx = bx + side, ny = by + (rising ? -side : side); if (sd <= best) { best = sd; found = b; }
if (nx < 0 || ny < 0 || nx >= BW || ny >= BH) continue;
const nb = ny * BW + nx;
if (blockP[nb] === BP_BELT || (blockP[nb] === BP_TUBE && (blockV[nb] < 4 || ((blockV[nb] === 4 || blockV[nb] === 6) === rising)))) return b;
} }
return -1; return found;
} }
function drawWorld() { function drawWorld() {
const bw = R.bw, bh = R.bh, px = R.px; const bw = R.bw, bh = R.bh, px = R.px;
@@ -44,6 +85,13 @@ function drawWorld() {
for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) { const sx = (i % W) - vx0, sy = ((i / W) | 0) - vy0; if (sx >= 0 && sy >= 0 && sx < bw && sy < bh) mmask[sy * bw + sx] = 1; } for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) { const sx = (i % W) - vx0, sy = ((i / W) | 0) - vy0; if (sx >= 0 && sy >= 0 && sx < bw && sy < bh) mmask[sy * bw + sx] = 1; }
} }
} }
if (diagonalJoins.length !== NB) diagonalJoins = new Uint8Array(NB);
for (let by = Math.max(0, (vy0 >> 2) - 1); by <= Math.min(BH - 1, ((vy0 + bh) >> 2) + 1); by++) {
for (let bx = Math.max(0, (vx0 >> 2) - 1); bx <= Math.min(BW - 1, ((vx0 + bw) >> 2) + 1); bx++) {
const b = by * BW + bx;
if (blockP[b] === BP_TUBE && blockV[b] >= 4) diagonalJoins[b] = diagonalTubeJoins(bx, by, blockV[b]);
}
}
const useFog = SET.fog; const useFog = SET.fog;
const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black
const lighting = SET.lighting !== 'off' && !R.lens; const lighting = SET.lighting !== 'off' && !R.lens;
@@ -178,22 +226,23 @@ function drawWorld() {
// field parts drawn over contents // field parts drawn over contents
let p = part[i]; let p = part[i];
const db = diagonalTubeBlock(x, y, sm); const db = diagonalTubeBlock(x, y, sm);
let diagonalAcross = 0; let diagonal = false;
if (db >= 0) { if (db >= 0) {
const v = blockV[db], rising = v === 4 || v === 6; const v = blockV[db], joins = diagonalJoins[db];
diagonalAcross = x - (db % BW) * BS - (rising ? 3 - (y - ((db / BW) | 0) * BS) : y - ((db / BW) | 0) * BS); const sd = diagonalTubeShape(x + 0.5, y + 0.5, db, v, joins);
p = sm || Math.abs(diagonalAcross) <= 1 ? TUBE_PARTS[v] : 0; p = sm || sd <= 0 ? TUBE_PARTS[v] : 0;
if (sm) tpx[row + bx] = 128 + diagonalAcross; diagonal = !!p;
if (sm) tpx[row + bx] = (joins << 24) | ((y - ((db / BW) | 0) * BS + 8) << 16) | ((x - (db % BW) * BS + 8) << 8);
} }
if (p && sm && (IS_TUBE[p] || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } } if (p && sm && (IS_TUBE[p] || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
else if (p) { else if (p) {
if (IS_TUBE[p]) { if (IS_TUBE[p]) {
const on = netSat(i) > 0; const on = netSat(i) > 0;
const vert = p === P_TU || p === P_TD, diagonal = TDX[p] && TDY[p]; const vert = p === P_TU || p === P_TD;
const axis = diagonal ? (x * TDX[p] + y * TDY[p]) >> 1 : vert ? y : x; const axis = diagonal ? Math.floor(diagonalAlong) : vert ? y : x;
const across = diagonal ? diagonalAcross + 1 : vert ? x & 3 : y & 3; const across = diagonal ? diagonalAcross + 2 : vert ? x & 3 : y & 3;
const dir = diagonal ? -1 : (p === P_TU || p === P_TL) ? 1 : -1; const dir = diagonal ? (blockV[db] >= 6 ? 1 : -1) : (p === P_TU || p === P_TL) ? 1 : -1;
if (diagonal ? across !== 1 : across === 0 || across === 3) { // copper casing on the two long sides; its load shows through if (diagonal ? Math.abs(diagonalAcross) > 1.38 || diagonalCap > -0.55 : across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; } if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
if (!diagonal && (axis & 3) === 0) { r -= 24; g -= 18; b -= 12; } if (!diagonal && (axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
} else { } else {
+2 -1
View File
@@ -72,7 +72,8 @@ function drawPastePreview() {
const [x, y, w, h] = blockRectPx(ox, oy, bp.bw, bp.bh); const [x, y, w, h] = blockRectPx(ox, oy, bp.bw, bp.bh);
ctx.strokeStyle = 'rgba(92,186,166,0.85)'; ctx.setLineDash([zp, zp]); ctx.lineWidth = Math.max(1, zp * 0.3); ctx.strokeStyle = 'rgba(92,186,166,0.85)'; ctx.setLineDash([zp, zp]); ctx.lineWidth = Math.max(1, zp * 0.3);
ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); ctx.strokeRect(x, y, w, h); ctx.setLineDash([]);
for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; previewBlock(code, bx2, by2, bw2, bh2); } const planned = new Map(bp.blocks.map(([dx, dy, code, v]) => [(oy + dy) * BW + ox + dx, [code, v]]));
for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; previewBlock(code, bx2, by2, bw2, bh2, false, code === BP_TUBE ? v : -1, ox + dx, oy + dy, planned); }
for (const [type, dx, dy] of bp.machines) { const P = PIECE_BY_ID[type]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, P.bw, P.bh); ctx.strokeStyle = 'rgba(255,210,122,0.8)'; ctx.strokeRect(mx, my, mw, mh); } for (const [type, dx, dy] of bp.machines) { const P = PIECE_BY_ID[type]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, P.bw, P.bh); ctx.strokeStyle = 'rgba(255,210,122,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
for (const [k, dx, dy] of bp.molds) { const md = MOLDS[k]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, md.bw, md.bh); ctx.strokeStyle = 'rgba(255,190,90,0.8)'; ctx.strokeRect(mx, my, mw, mh); } for (const [k, dx, dy] of bp.molds) { const md = MOLDS[k]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, md.bw, md.bh); ctx.strokeStyle = 'rgba(255,190,90,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
ctx.restore(); ctx.restore();
+25 -8
View File
@@ -112,6 +112,7 @@ function drawPreviews() {
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; } if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0); const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0);
const afford = canAfford(pieceCostNow(P), cells.length); const afford = canAfford(pieceCostNow(P), cells.length);
const planned = code === BP_TUBE && dIdx >= 4 ? new Map(cells.map(([x, y]) => [y * BW + x, [BP_TUBE, dIdx]])) : null;
for (const [bx, by] of cells) { for (const [bx, by] of cells) {
const err = canPlaceBlock(bx, by, code), rb = by * BW + bx; const err = canPlaceBlock(bx, by, code), rb = by * BW + bx;
const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, code, v, pieceExtra(P)); const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, code, v, pieceExtra(P));
@@ -119,10 +120,10 @@ function drawPreviews() {
const ok = (!err || swap) && !ghost && afford; const ok = (!err || swap) && !ghost && afford;
const [x, y, w, h] = blockRectPx(bx, by, 1, 1); const [x, y, w, h] = blockRectPx(bx, by, 1, 1);
ctx.fillStyle = ghost ? 'rgba(90,190,230,0.22)' : ok ? (swap ? 'rgba(255,190,90,0.5)' : `rgba(${col},0.55)`) : 'rgba(239,106,85,0.35)'; ctx.fillStyle = ghost ? 'rgba(90,190,230,0.22)' : ok ? (swap ? 'rgba(255,190,90,0.5)' : `rgba(${col},0.55)`) : 'rgba(239,106,85,0.35)';
previewBlock(code, x, y, w, h, false, P.id === 'tube' ? dIdx : -1); previewBlock(code, x, y, w, h, false, P.id === 'tube' ? dIdx : -1, bx, by, planned);
ctx.strokeStyle = ghost ? 'rgba(90,190,230,0.8)' : ok ? 'rgba(236,226,210,0.8)' : 'rgba(239,106,85,0.9)'; ctx.lineWidth = Math.max(1, zp * 0.25); ctx.strokeStyle = ghost ? 'rgba(90,190,230,0.8)' : ok ? 'rgba(236,226,210,0.8)' : 'rgba(239,106,85,0.9)'; ctx.lineWidth = Math.max(1, zp * 0.25);
if (ghost) ctx.setLineDash([zp, zp * 0.7]); if (ghost) ctx.setLineDash([zp, zp * 0.7]);
previewBlock(code, x, y, w, h, true, P.id === 'tube' ? dIdx : -1); ctx.setLineDash([]); previewBlock(code, x, y, w, h, true, P.id === 'tube' ? dIdx : -1, bx, by, planned); ctx.setLineDash([]);
if (P.dirs && (P.id === 'belt' || P.id === 'tube' || P.filter || P.id === 'pump' || P.id === 'outlet')) { if (P.dirs && (P.id === 'belt' || P.id === 'tube' || P.filter || P.id === 'pump' || P.id === 'outlet')) {
// a chevron on every block, pointing the way it will carry (or pump, or pour) // a chevron on every block, pointing the way it will carry (or pump, or pour)
const [dx, dy] = P.id === 'tube' ? TUBE_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx]; const [dx, dy] = P.id === 'tube' ? TUBE_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
@@ -179,14 +180,30 @@ function drawPreviews() {
drawCursorLabel(p.x, p.y, t === 'vacuum' ? GAME.brush[GAME.tool] : t === 'ignite' || t === 'drill' ? 1 : 2); drawCursorLabel(p.x, p.y, t === 'vacuum' ? GAME.brush[GAME.tool] : t === 'ignite' || t === 'drill' ? 1 : 2);
} }
function previewBlock(code, x, y, w, h, stroke = false, direction = -1) { function previewBlock(code, x, y, w, h, stroke = false, direction = -1, bx = -1, by = -1, planned = null) {
const ctx = R.ctx; const ctx = R.ctx;
if (code === BP_TUBE && direction >= 4) { if (code === BP_TUBE && direction >= 4) {
const rising = direction === 4 || direction === 6; const joins = bx >= 0 ? diagonalTubeJoins(bx, by, direction, planned) : 0;
ctx.save(); ctx.translate(x + w / 2, y + h / 2); ctx.rotate(rising ? -Math.PI / 4 : Math.PI / 4); const points = [];
const length = w * Math.SQRT2, width = w * 0.75; for (let end = 0; end < 2; end++) {
if (stroke) ctx.strokeRect(-length / 2, -width / 2, length, width); const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1;
else ctx.fillRect(-length / 2, -width / 2, length, width); const d = join ? TUBE_DIRS[join - 1] : [side, side * (direction === 4 || direction === 6 ? -1 : 1)];
points.push([d[0] * w / 2, d[1] * h / 2]);
}
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; ctx.restore(); return;
} }
if (!isSlope(code)) { if (stroke) ctx.strokeRect(x, y, w, h); else ctx.fillRect(x, y, w, h); return; } if (!isSlope(code)) { if (stroke) ctx.strokeRect(x, y, w, h); else ctx.fillRect(x, y, w, h); return; }
+14 -2
View File
@@ -168,8 +168,19 @@ for(const [dx,dy,v] of [[5,-4,4],[4,5,5],[-5,4,6],[-4,-5,7]]) {
check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.tube,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v); check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.tube,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v);
check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`)); check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`));
} }
// The drawing can bridge a joint only in unoccupied air; transport parts stay square. // End elbows follow the adjoining straight piece's axis, regardless of flow direction.
run(fs.readFileSync('src/render/world.js', 'utf8')); run(fs.readFileSync('src/render/world.js', 'utf8'));
for (const v of [4,5,6,7]) for (let dir=0;dir<4;dir++) for (const code of ['BP_TUBE','BP_BELT','BP_FILTER']) {
if (code !== 'BP_TUBE' && !(dir & 1)) continue;
fresh();
const [dx,dy]=run(`TUBE_DIRS[${dir}]`);
run(`placeBlock(BP_TUBE,${v},20,20,true);placeBlock(${code},${dir},${20+dx},${20+dy},true);`);
const end = dx ? dx>0 : (v===4 || v===6 ? dy<0 : dy>0);
check(`diagonal ${v} bends toward ${code} direction ${dir}`,run(`(diagonalTubeJoins(20,20,${v}) >> ${end?3:0} & 7)`)===dir+1);
check(`planned ${code} direction ${dir} bends the diagonal ${v} ghost too`,run(`(()=>{removeBlock(${20+dx},${20+dy});const planned=new Map([[${20+dy}*BW+${20+dx},[${code},${dir}]]]);const j=diagonalTubeJoins(20,20,${v},planned);placeBlock(${code},${dir},${20+dx},${20+dy},true);return (j >> ${end?3:0} & 7)===${dir+1};})()`));
check(`diagonal ${v} meets ${code} direction ${dir} across its centred edge`,run(`(()=>{const b=20*BW+20,j=diagonalTubeJoins(20,20,${v});return [0.5,1.5,2.5,3.5].every(t=>diagonalTubeShape(${dx ? 82+dx*1.99 : '80+t'},${dy ? 82+dy*1.99 : '80+t'},b,${v},j)<=0);})()`));
}
// The drawing can bridge a joint only in unoccupied air; transport parts stay square.
for (const v of [4,5,6,7]) { for (const v of [4,5,6,7]) {
flatWorld(run,240,192,176); flatWorld(run,240,192,176);
const rising = v===4 || v===6; const rising = v===4 || v===6;
@@ -179,7 +190,8 @@ for (const v of [4,5,6,7]) {
for(const obstacle of ['mat[j]=STONE','mat[j]=SAND','part[j]=P_PIPE','mach[j]=1','occ[j]=1','blockP[(j/W>>2)*BW+(j%W>>2)]=BP_WALL']) { for(const obstacle of ['mat[j]=STONE','mat[j]=SAND','part[j]=P_PIPE','mach[j]=1','occ[j]=1','blockP[(j/W>>2)*BW+(j%W>>2)]=BP_WALL']) {
check(`diagonal ${v} leaves ${obstacle} visible`,run(`(()=>{const j=${y}*W+${x};${obstacle};const hidden=diagonalTubeBlock(${x},${y},true);mat[j]=EMPTY;part[j]=0;mach[j]=0;occ[j]=0;blockP[(${y}>>2)*BW+(${x}>>2)]=0;return hidden<0;})()`)); check(`diagonal ${v} leaves ${obstacle} visible`,run(`(()=>{const j=${y}*W+${x};${obstacle};const hidden=diagonalTubeBlock(${x},${y},true);mat[j]=EMPTY;part[j]=0;mach[j]=0;occ[j]=0;blockP[(${y}>>2)*BW+(${x}>>2)]=0;return hidden<0;})()`));
} }
check(`diagonal ${v} keeps its continuation uncapped`,run(`(diagonalTubeJoins(20,20,${v}) & 128)===0 && (diagonalTubeJoins(21,${rising ? 19 : 21},${v}) & 64)===0`));
run(`removeBlock(21,${rising ? 19 : 21});`); run(`removeBlock(21,${rising ? 19 : 21});`);
check(`diagonal ${v} has no stray corner after its end`,run(`diagonalTubeBlock(${x},${y},false)===-1`)); check(`diagonal ${v} has no stray corner after its end`,run(`diagonalTubeBlock(${x+1},${rising ? y-1 : y+1},false)===-1`));
} }
done(); done();
+21
View File
@@ -0,0 +1,21 @@
// Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
const jx = BX + 5, jy = BY - 3;
for (let k = 1; k <= 4; k++) placeBlock(BP_TUBE, 0, jx, jy + k, true);
for (let k = 0; k < 6; k++) placeBlock(BP_TUBE, 4, jx + k, jy - k, true);
for (let k = 1; k <= 4; k++) placeBlock(BP_TUBE, 0, jx + 5, jy - 5 - k, true);
const hx = BX + 21, hy = BY - 3;
for (let k = 1; k <= 4; k++) placeBlock(BP_BELT, 1, hx - k, hy, true);
for (let k = 0; k < 6; k++) placeBlock(BP_TUBE, 4, hx + k, hy - k, true);
for (let k = 1; k <= 4; k++) placeBlock(BP_BELT, 1, hx + 5 + k, hy - 5, true);
for (let k = 0; k < 4; k++) placeBlock(BP_TUBE, 5, BX + 32 + k, BY - 10 + k, true);
window.JOINTS = {
verticalIn: [jx * 4 + 2, jy * 4 + 4],
verticalOut: [(jx + 5) * 4 + 2, (jy - 5) * 4],
beltIn: [hx * 4, hy * 4 + 2],
beltOut: [(hx + 6) * 4, (hy - 5) * 4 + 2],
free: [(BX + 35) * 4 + 4, (BY - 7) * 4 + 4]
};
window.CAM = [(BX + 20) * 4, (BY - 5) * 4];
// Keep the camera-following bot out of the joint close-ups.
drawBot = () => {};