From ccca66c0c17eb69a12ae8842a65f8eae493f2410 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Tue, 27 Oct 2026 15:23:08 -0400 Subject: [PATCH] "Doesn't connect" marks on transport ends that would join one block off (t67 phase 4) Co-Authored-By: Claude Opus 5.5 --- docs/PROJECT_MAP.md | 28 ++++++++++++++-------------- docs/agent-log.md | 13 +++++++++++++ src/render/overlays.js | 22 ++++++++++++++++++++++ src/sim/joins.js | 20 ++++++++++++++++++++ src/ui/info.js | 5 +++++ src/ui/loop.js | 1 + src/ui/previews.js | 3 +++ test/t_joins.js | 16 ++++++++++++++++ 8 files changed, 94 insertions(+), 14 deletions(-) diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 81623f4..92a04d8 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -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). - 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. -- functions: pieceEnds, joinCandidate, endJoin, pieceJoins -- variables: JOIN_ENDS +- functions: pieceEnds, joinCandidate, endJoin, pieceJoins, endAt, joinIsDrop, joinSnap, nearMiss +- variables: JOIN_ENDS, NO_PIECE ### 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. @@ -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. - 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. - functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat - 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 - 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. - 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. - 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. -- 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) Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution. - functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace - 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. - 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 - 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. -- 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) 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 - variables: lastBagKey -### src/ui/info.js (195 lines) +### src/ui/info.js (200 lines) Discovery cards, hover info (I) and the cursor label. - functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText @@ -506,7 +506,7 @@ Keyboard and mouse listeners. - functions: isTyping - 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 - functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick - variables: ovShown @@ -516,7 +516,7 @@ Gamepad input. - functions: padPressed, pollPad - 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. - 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 - variables: MAP_S, MAP -### src/ui/loop.js (135 lines) +### src/ui/loop.js (136 lines) The frame loop and boot. - functions: scanVisible, frame, boot - variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT diff --git a/docs/agent-log.md b/docs/agent-log.md index 4061c97..002c476 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: 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) - `dragPlan(P, a, b, shift)` in `src/ui/selection.js` gives code, direction, variant, blocks and diff --git a/src/render/overlays.js b/src/render/overlays.js index 85e5b53..19b1c4a 100644 --- a/src/render/overlays.js +++ b/src/render/overlays.js @@ -53,6 +53,28 @@ function drawFlowArrows() { } 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 function drawRunOutline(run, strong) { if (!run || !run.length) return; diff --git a/src/sim/joins.js b/src/sim/joins.js index 5519760..8ca3aa1 100644 --- a/src/sim/joins.js +++ b/src/sim/joins.js @@ -103,3 +103,23 @@ function joinSnap(code, v, cells) { } 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; +} diff --git a/src/ui/info.js b/src/ui/info.js index 2e98013..d0b733e 100644 --- a/src/ui/info.js +++ b/src/ui/info.js @@ -132,6 +132,11 @@ function hoverInfo(ax, ay) { 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'}`; + 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]]; 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])); diff --git a/src/ui/loop.js b/src/ui/loop.js index ee6cf75..6561c49 100644 --- a/src/ui/loop.js +++ b/src/ui/loop.js @@ -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; if (GAME.editRun) drawRunOutline(GAME.editRun, true); else if (hb >= 0) drawRunOutline(filterRun(hb), false); if (GAME.infoOverlay) drawFlowArrows(); + drawJoinMarks(); drawFilterTags(!!GAME.infoOverlay, GAME.editRun ? GAME.editRun[0] : hb); drawFloaters(); drawRain(); diff --git a/src/ui/previews.js b/src/ui/previews.js index 983ee05..53a0c5f 100644 --- a/src/ui/previews.js +++ b/src/ui/previews.js @@ -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(); } } + // 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) { const label = `${cells.length} × ${costText(pieceCostNow(P), 1)}`; ctx.font = `600 12px ui-monospace, monospace`; ctx.fillStyle = 'rgba(14,12,11,0.85)'; diff --git a/test/t_joins.js b/test/t_joins.js index d002b1d..a4d7137 100644 --- a/test/t_joins.js +++ b/test/t_joins.js @@ -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', 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); +// "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();