"Doesn't connect" marks on transport ends that would join one block off (t67 phase 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 15:23:08 -04:00
co-authored by Claude Opus 5.5
parent 99d4889f1e
commit ccca66c0c1
8 changed files with 94 additions and 14 deletions
+14 -14
View File
@@ -281,10 +281,10 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick). Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
- functions: engineIntake, fanBlow, machinesTick - functions: engineIntake, fanBlow, machinesTick
### src/sim/joins.js (73 lines) ### src/sim/joins.js (125 lines)
Travel-directed transport ends. Precomputed probes; world/plan queries allocate nothing. End indices stay geometric: left/right, or up/down for vertical screws. Travel-directed transport ends. Precomputed probes; world/plan queries allocate nothing. End indices stay geometric: left/right, or up/down for vertical screws.
- functions: pieceEnds, joinCandidate, endJoin, pieceJoins - functions: pieceEnds, joinCandidate, endJoin, pieceJoins, endAt, joinIsDrop, joinSnap, nearMiss
- variables: JOIN_ENDS - variables: JOIN_ENDS, NO_PIECE
### src/sim/conveyors.js (211 lines) ### src/sim/conveyors.js (211 lines)
How belts, filters and screw conveyors move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full. How belts, filters and screw conveyors move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full.
@@ -388,7 +388,7 @@ 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/slope-belts.js (169 lines) ### src/render/slope-belts.js (167 lines)
Drawing only: the face stays on its simulation cells; the band hangs below it. Drawing only: the face stays on its simulation cells; the band hangs below it.
- functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat - functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat
- variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner - variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner
@@ -398,7 +398,7 @@ Screw casing geometry shared by both looks, ghosts and placement previews. smoot
- functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock - functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap - variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
### src/render/world.js (405 lines) ### src/render/world.js (404 lines)
Scale colour without changing the smooth-look class. Scale colour without changing the smooth-look class.
- functions: fogColor, drawWorld - functions: fogColor, drawWorld
@@ -406,16 +406,16 @@ Scale colour without changing the smooth-look class.
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.
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks - functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks
### src/render/overlays.js (229 lines) ### src/render/overlays.js (251 lines)
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles. Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
- functions: spawnFloater, drawRain, drawFlowArrows, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles - functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
### src/render/smooth.js (99 lines) ### src/render/smooth.js (99 lines)
Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution. 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/slope-belt-shader.js (126 lines) ### src/render/slope-belt-shader.js (111 lines)
Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js. Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
- variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW - variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW
@@ -480,9 +480,9 @@ The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
### src/ui/selection.js (122 lines) ### src/ui/selection.js (133 lines)
Tool selection: cycling, drill tuning, piece variants and costs. Tool selection: cycling, drill tuning, piece variants and costs.
- functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterBlockAt, chipCost, pieceCostNow, pieceCode, turnSlope - functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, dragPlan, pieceExtra, filterBlockAt, chipCost, pieceCostNow, pieceCode, turnSlope
### src/ui/selection-pickers.js (61 lines) ### src/ui/selection-pickers.js (61 lines)
Selection pickers: filter rules, filter settings and wall materials. What a filter can hold: loose things and bars you have found, plus anything it already holds Selection pickers: filter rules, filter settings and wall materials. What a filter can hold: loose things and bars you have found, plus anything it already holds
@@ -493,7 +493,7 @@ Selection panels: current tool card and bag materials.
- functions: renderSelCard, renderBag - functions: renderSelCard, renderBag
- variables: lastBagKey - variables: lastBagKey
### src/ui/info.js (195 lines) ### src/ui/info.js (200 lines)
Discovery cards, hover info (I) and the cursor label. Discovery cards, hover info (I) and the cursor label.
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText - functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
@@ -506,7 +506,7 @@ Keyboard and mouse listeners.
- functions: isTyping - functions: isTyping
- variables: canvas - variables: canvas
### src/ui/actions.js (244 lines) ### src/ui/actions.js (242 lines)
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick - functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
- variables: ovShown - variables: ovShown
@@ -516,7 +516,7 @@ Gamepad input.
- functions: padPressed, pollPad - functions: padPressed, pollPad
- variables: PAD - variables: PAD
### src/ui/previews.js (240 lines) ### src/ui/previews.js (241 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
@@ -539,7 +539,7 @@ Shared world colours, minimap and full map.
- functions: resetMap, mapArea, updateMap, drawMap, mapFit, drawFullMap, openMap, zoomMap, zoomMinimap - functions: resetMap, mapArea, updateMap, drawMap, mapFit, drawFullMap, openMap, zoomMap, zoomMinimap
- variables: MAP_S, MAP - variables: MAP_S, MAP
### src/ui/loop.js (135 lines) ### src/ui/loop.js (136 lines)
The frame loop and boot. The frame loop and boot.
- functions: scanVisible, frame, boot - functions: scanVisible, frame, boot
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT - variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
+13
View File
@@ -4,6 +4,19 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-27: t67 phase 4, "doesn't connect" marks (Claude Code)
- `nearMiss(bx, by, end, planned)` in `src/sim/joins.js`: an end with no join that would join
seamlessly, with a diagonal piece involved, one block up or down in empty space. Drop joins
count as connected; straight beside straight is never marked. It checks the moved block
through a small override view of `planned`, so ghost maps are never copied.
- Marks: a red ring with a slash at the end, on built pieces and ghosts (`drawJoinMarks` in
`src/render/overlays.js`, called every frame from `src/ui/loop.js`) and at the ends of a drag
preview (`src/ui/previews.js`). Hover says "doesn't connect: place it one block higher/lower"
(`src/ui/info.js`). This covers t58's mark for belts and the same cases for screws.
- Tests: 7 nearMiss checks in `test/t_joins.js`; `python3 test/run_all.py --browser` all passed.
`docs/PROJECT_MAP.md` regenerated.
## 2026-10-27: t67 phase 3 shared drag placement, and snapping (Claude Code) ## 2026-10-27: t67 phase 3 shared drag placement, and snapping (Claude Code)
- `dragPlan(P, a, b, shift)` in `src/ui/selection.js` gives code, direction, variant, blocks and - `dragPlan(P, a, b, shift)` in `src/ui/selection.js` gives code, direction, variant, blocks and
+22
View File
@@ -53,6 +53,28 @@ function drawFlowArrows() {
} }
ctx.restore(); ctx.restore();
} }
// "Doesn't connect": a red ring with a slash at a transport end that would join one block up or
// down (nearMiss), on built pieces, ghosts and the drag preview alike.
function drawJoinMark(bx, by, end, code, v) {
const e = pieceEnds(code, v)[end], ctx = R.ctx, r = Math.max(3, BS * R.zp * 0.24);
const c = worldToScreenPx(bx * BS + BS / 2 + e.dx * BS / 2, by * BS + BS / 2 + e.dy * BS / 2);
ctx.save();
ctx.fillStyle = 'rgba(14,12,11,0.7)'; ctx.beginPath(); ctx.arc(c.x, c.y, r * 1.2, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = '#ef6a55'; ctx.lineWidth = Math.max(1.2, r * 0.3);
ctx.beginPath(); ctx.arc(c.x, c.y, r * 0.75, 0, Math.PI * 2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(c.x - r * 0.53, c.y + r * 0.53); ctx.lineTo(c.x + r * 0.53, c.y - r * 0.53); ctx.stroke();
ctx.restore();
}
function drawJoinMarks() {
const v = viewCells();
const bx0 = Math.max(0, Math.floor(R.camX / BS)), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS));
const by0 = Math.max(0, Math.floor(R.camY / BS)), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS));
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
const b = by * BW + bx, ghost = ghostBlocks.get(b), code = ghost ? ghost[0] : blockP[b], vv = ghost ? ghost[1] : blockV[b];
if (!code || !pieceEnds(code, vv).length) continue;
for (let end = 0; end < 2; end++) if (nearMiss(bx, by, end, ghostBlocks.size ? ghostBlocks : null)) drawJoinMark(bx, by, end, code, vv);
}
}
// Outline a run of blocks: strong while it is being edited, faint when pointed at // Outline a run of blocks: strong while it is being edited, faint when pointed at
function drawRunOutline(run, strong) { function drawRunOutline(run, strong) {
if (!run || !run.length) return; if (!run || !run.length) return;
+20
View File
@@ -103,3 +103,23 @@ function joinSnap(code, v, cells) {
} }
return 0; return 0;
} }
// "Doesn't connect": an end with no join that would join seamlessly, with a diagonal piece
// involved, if its block sat one higher (-1) or lower (+1) in empty space. 0 when no near miss.
// A drop join counts as connected. The moved block is tried through a small override view of
// `planned`, so ghost maps are never copied.
const NO_PIECE = [0, 0];
function nearMiss(bx, by, end, planned = null) {
const b = by * BW + bx, p = planned && planned.get(b), code = p ? p[0] : blockP[b], v = p ? p[1] : blockV[b];
const own = pieceEnds(code, v);
if (!own[end] || endJoin(bx, by, end, planned) >= 0) return 0;
const entry = [code, v];
for (const dy of [-1, 1]) {
const y = by + dy, nb = y * BW + bx;
if (y < 0 || y >= BH || blockP[nb] || (planned && planned.get(nb))) continue;
const view = { get: k => k === b ? NO_PIECE : k === nb ? entry : planned ? planned.get(k) : undefined };
view.has = k => view.get(k) !== undefined;
const j = endJoin(bx, y, end, view);
if (j >= 0 && !joinIsDrop(bx, y, end, j, view) && (own[end].diagonal || endAt(j >> 1, j & 1, view).diagonal)) return dy;
}
return 0;
}
+5
View File
@@ -132,6 +132,11 @@ function hoverInfo(ax, ay) {
if (b === BP_OUTLET) t2.push(v & 4 ? 'closed · press F' : 'open · F closes it'); if (b === BP_OUTLET) t2.push(v & 4 ? 'closed · press F' : 'open · F closes it');
} }
else if (isWall(b) && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} ${isSlope(b) ? 'sloped wall' : 'block'}`; else if (isWall(b) && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} ${isSlope(b) ? 'sloped wall' : 'block'}`;
const jb = ((ay / BS) | 0) * BW + ((ax / BS) | 0);
for (let end = 0; end < 2 && !id && !bar; end++) {
const miss = nearMiss(jb % BW, (jb / BW) | 0, end);
if (miss) { t2.push(`doesn't connect: place it one block ${miss < 0 ? 'higher' : 'lower'}`); warn = warn || "doesn't connect"; break; }
}
const n = nets[netId[i]]; const n = nets[netId[i]];
if (n && isPow[i] && !POWER_FREE) t2.push(`network ${n.supply.toFixed(1)} supply / ${n.demand.toFixed(1)} demand`); if (n && isPow[i] && !POWER_FREE) t2.push(`network ${n.supply.toFixed(1)} supply / ${n.demand.toFixed(1)} demand`);
if (player.lens && !bar) t2.push(fmtT(temp[i])); if (player.lens && !bar) t2.push(fmtT(temp[i]));
+1
View File
@@ -71,6 +71,7 @@ function frame(now) {
const hb = GAME.mouse.in || GAME.usingPad ? filterBlockAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)) : -1; const hb = GAME.mouse.in || GAME.usingPad ? filterBlockAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)) : -1;
if (GAME.editRun) drawRunOutline(GAME.editRun, true); else if (hb >= 0) drawRunOutline(filterRun(hb), false); if (GAME.editRun) drawRunOutline(GAME.editRun, true); else if (hb >= 0) drawRunOutline(filterRun(hb), false);
if (GAME.infoOverlay) drawFlowArrows(); if (GAME.infoOverlay) drawFlowArrows();
drawJoinMarks();
drawFilterTags(!!GAME.infoOverlay, GAME.editRun ? GAME.editRun[0] : hb); drawFilterTags(!!GAME.infoOverlay, GAME.editRun ? GAME.editRun[0] : hb);
drawFloaters(); drawFloaters();
drawRain(); drawRain();
+3
View File
@@ -129,6 +129,9 @@ function drawPreviews() {
chev(); ctx.strokeStyle = 'rgba(255,224,160,0.98)'; ctx.lineWidth = Math.max(1.2, zp * 0.45); chev(); chev(); ctx.strokeStyle = 'rgba(255,224,160,0.98)'; ctx.lineWidth = Math.max(1.2, zp * 0.45); chev();
} }
} }
// ends of the line that would join one block up or down get the "doesn't connect" mark
for (const [bx, by] of cells.length > 1 ? [cells[0], cells[cells.length - 1]] : cells)
for (let end = 0; end < 2; end++) if (nearMiss(bx, by, end, planned)) drawJoinMark(bx, by, end, code, v);
if (GAME.drag && cells.length > 1) { if (GAME.drag && cells.length > 1) {
const label = `${cells.length} × ${costText(pieceCostNow(P), 1)}`; const label = `${cells.length} × ${costText(pieceCostNow(P), 1)}`;
ctx.font = `600 12px ui-monospace, monospace`; ctx.fillStyle = 'rgba(14,12,11,0.85)'; ctx.font = `600 12px ui-monospace, monospace`; ctx.fillStyle = 'rgba(14,12,11,0.85)';
+16
View File
@@ -187,4 +187,20 @@ check('a downhill screw started one block right and one lower than a screw end s
check('a screw that already joins stays put', check('a screw that already joins stays put',
snap('placeBlock(BP_SCREW,1,10,10,true)', 'BP_SCREW', 5, [[11,10],[12,11]]) === 0); snap('placeBlock(BP_SCREW,1,10,10,true)', 'BP_SCREW', 5, [[11,10],[12,11]]) === 0);
check('pieces with no join ends never snap', snap('', 'BP_PIPE', 0, [[11,10]]) === 0); check('pieces with no join ends never snap', snap('', 'BP_PIPE', 0, [[11,10]]) === 0);
// "Doesn't connect" marks (t67 phase 4): nearMiss says which way one block would make the join.
const miss = (setup, expr) => { flatWorld(run, 96, 96, 92); return run(`(()=>{${setup};return ${expr};})()`); };
check('a flat belt level with a downhill belt low end is marked: place it one lower (t58)',
miss('placeBlock(BP_BELT_DOWN,1,10,10,true);placeBlock(BP_BELT_DOWN,1,11,11,true);placeBlock(BP_BELT,1,12,11,true)', 'nearMiss(12,11,0)') === 1);
check('a flat belt one lower at that low end is joined and unmarked',
miss('placeBlock(BP_BELT_DOWN,1,10,10,true);placeBlock(BP_BELT_DOWN,1,11,11,true);placeBlock(BP_BELT,1,12,12,true)', 'endJoin(12,12,0)>=0 && nearMiss(12,12,0)===0 && nearMiss(11,11,1)===0'));
check('a screw end diagonally off a downhill screw start is marked on both pieces',
miss('placeBlock(BP_SCREW,1,10,10,true);placeBlock(BP_SCREW,5,11,11,true)', '[nearMiss(10,10,1),nearMiss(11,11,0)].join()') === '1,-1');
check('a drop join (flat feeding an uphill low end on its row) counts as connected',
miss('placeBlock(BP_BELT,1,10,10,true);placeBlock(BP_BELT_UP,1,11,10,true)', 'nearMiss(10,10,1)===0 && nearMiss(11,10,0)===0'));
check('straight belts one row apart are never marked',
miss('placeBlock(BP_BELT,1,10,10,true);placeBlock(BP_BELT,1,11,11,true)', 'nearMiss(10,10,1)===0 && nearMiss(11,11,0)===0'));
check('no mark when the block that would fix it is taken',
miss('placeBlock(BP_BELT_DOWN,1,10,10,true);placeBlock(BP_BELT_DOWN,1,11,11,true);placeBlock(BP_BELT,1,12,11,true);placeBlock(BP_WALL,RSTONE,12,12,true)', 'nearMiss(12,11,0)') === 0);
check('ghosts are checked through their planned map without copying it',
miss('ghostBlocks.set(10*BW+10,[BP_SCREW,1]);ghostBlocks.set(11*BW+11,[BP_SCREW,5])', 'nearMiss(10,10,1,ghostBlocks)===1 && ghostBlocks.size===2'));
done(); done();