Skip to content

Commit 2c9d5d0

Browse files
tmck-codeclaude
andcommitted
pikachu companion: prominent START marker + centre on start; anchor block order at the start point
- route start drawn with halo, bold ring and START tag; selecting a colour pans to the start once the route is planned - block walk now starts at the block containing the start point and expands outward row by row (serpentine/row-major within rows); last known start cached so the order doesn't flicker while a route re-plans - setting a new start point restarts the block walk Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 00c2f4c commit 2c9d5d0

4 files changed

Lines changed: 105 additions & 18 deletions

File tree

pages/pikachu-stitch-companion/js/blocks.js

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { N } from './pattern.js';
55
import { state, idxOf, rowOf, colOf, colourAt, isStitched } from './state.js';
66
import { view, clampView, draw, stage } from './render.js';
7+
import { getRoute } from './planner.js';
78

89
export const BLOCK_SIZE = 10;
910
export const BLOCKS = N / BLOCK_SIZE; // 15 blocks per axis (150/10)
@@ -49,28 +50,63 @@ export function blockIsCompleteForColour(br, bc, v){
4950
}
5051

5152
/**
52-
* blockOrderList(v) -> blocksForColour(v) reordered per
53-
* state.settings.blockOrder:
54-
* - 'row-major': left-to-right, top-to-bottom throughout (bc ascending
55-
* every row).
56-
* - 'serpentine' (default): left-to-right on even block-rows, right-to-
57-
* left on odd block-rows (boustrophedon), so consecutive blocks are
58-
* always adjacent.
53+
* startCellFor(v) -> cell index the route for colour v starts from (the
54+
* user-set start point if valid, else the planned route's start), or null
55+
* when neither is known yet.
56+
*/
57+
const lastKnownStart = new Map(); // colour -> cell; survives route invalidation
58+
export function startCellFor(v){
59+
const sp = state.startPoints[v];
60+
if (sp!=null && colourAt(sp)===v && !isStitched(sp)){ lastKnownStart.set(v, sp); return sp; }
61+
const route = getRoute(v);
62+
if (route && route.start!=null){ lastKnownStart.set(v, route.start); return route.start; }
63+
// route is being re-planned (e.g. right after a mark) — keep the previous
64+
// anchor so the block order doesn't jump around between frames
65+
return lastKnownStart.has(v) ? lastKnownStart.get(v) : null;
66+
}
67+
68+
/**
69+
* blockOrderList(v, order?) -> blocksForColour(v) in navigation order,
70+
* anchored at the block containing the colour's start point (startCellFor):
71+
* - block-rows are visited outward from the start row (start row first,
72+
* then the row below, the row above, two below, two above, …);
73+
* - within the start row, blocks are visited nearest-first from the start
74+
* block (ties: rightward first);
75+
* - 'row-major': every other row is left-to-right;
76+
* - 'serpentine' (default): every other row begins at the side nearest
77+
* where the previous row ended, so consecutive blocks stay adjacent.
78+
* With no known start point the anchor is the top-left block (legacy).
5979
*/
6080
export function blockOrderList(v, order = state.settings.blockOrder){
6181
const blocks = blocksForColour(v); // already row-major by br,bc
62-
if(order === 'row-major') return blocks;
82+
if(!blocks.length) return blocks;
83+
const start = startCellFor(v);
84+
const anchor = start!=null ? blockOf(start) : { br: blocks[0].br, bc: blocks[0].bc };
6385
const byRow = new Map();
6486
for(const b of blocks){
6587
if(!byRow.has(b.br)) byRow.set(b.br, []);
6688
byRow.get(b.br).push(b);
6789
}
90+
const rows = [...byRow.keys()].sort((a,b)=>{
91+
const da = Math.abs(a-anchor.br), db = Math.abs(b-anchor.br);
92+
return da!==db ? da-db : b-a; // nearer first; tie → the lower row (below) first
93+
});
6894
const out = [];
69-
for(const br of [...byRow.keys()].sort((a,b)=>a-b)){
70-
const row = byRow.get(br);
71-
if(br % 2 === 1) row.reverse();
95+
let lastBc = anchor.bc;
96+
rows.forEach((br, k)=>{
97+
let row = byRow.get(br).slice().sort((a,b)=>a.bc-b.bc);
98+
if(k===0){
99+
row.sort((a,b)=>{
100+
const da = Math.abs(a.bc-anchor.bc), db = Math.abs(b.bc-anchor.bc);
101+
return da!==db ? da-db : b.bc-a.bc;
102+
});
103+
} else if(order !== 'row-major'){
104+
const first = row[0].bc, last = row[row.length-1].bc;
105+
if(Math.abs(last-lastBc) < Math.abs(first-lastBc)) row.reverse();
106+
}
72107
out.push(...row);
73-
}
108+
lastBc = row[row.length-1].bc;
109+
});
74110
return out;
75111
}
76112

@@ -94,3 +130,20 @@ export function gotoBlock(br, bc){
94130
clampView();
95131
draw();
96132
}
133+
134+
/**
135+
* centreOnCell(idx) - pans (keeping the current zoom, but at least 3x so the
136+
* cell is legible) to put cell idx in the middle of the stage. Honours the
137+
* back-view mirror like gotoBlock.
138+
*/
139+
export function centreOnCell(idx){
140+
const w = stage.clientWidth, h = stage.clientHeight;
141+
view.scale = Math.max(view.scale, 3);
142+
const s = view.base * view.scale;
143+
const col = colOf(idx) + 0.5, row = rowOf(idx) + 0.5;
144+
const cx = (view.backView ? N - col : col) * s;
145+
view.tx = w/2 - cx;
146+
view.ty = h/2 - row*s;
147+
clampView();
148+
draw();
149+
}

pages/pikachu-stitch-companion/js/input.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ stage.addEventListener('pointerdown',e=>{
171171
invalidateRoute(state.selected);
172172
requestRoute(state.selected);
173173
disarmSetStart();
174+
setBlockIdx(0); // block order is anchored at the start point, so restart the walk
174175
refreshUI(); draw();
175176
}
176177
} else if (markMode && state.selected!=null){

pages/pikachu-stitch-companion/js/render.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export function draw(){
8787
const topDir = state.settings.topLegDirection; // '/' or '\\'
8888
const showSymbols = symbolsOn && s>=11;
8989
const textQueue = []; // {x,y,text,dark} — drawn un-mirrored after restore
90+
let startTag = null; // {x,y} grid-space anchor for the START label
9091

9192
// ---- mirrored region: cells, tie-off knots + buried-tail shading, carry hops ----
9293
ctx.save();
@@ -215,10 +216,17 @@ export function draw(){
215216
ctx.beginPath(); ctx.moveTo(px,py-s*0.22); ctx.lineTo(px+s*0.18,py); ctx.lineTo(px,py+s*0.22); ctx.lineTo(px-s*0.18,py);
216217
ctx.closePath(); ctx.fill();
217218
}
218-
// highlighted start point
219+
// highlighted start point: soft halo + bold ring so it reads at any
220+
// zoom, plus a "START" tag (queued so it isn't mirrored / clipped)
219221
const sx=colOf(route.start)*s+s/2, sy=rowOf(route.start)*s+s/2;
220-
ctx.strokeStyle='#fde949'; ctx.lineWidth=Math.max(1.4,s*0.1);
221-
ctx.beginPath(); ctx.arc(sx,sy,Math.max(3,s*0.32),0,Math.PI*2); ctx.stroke();
222+
const rr=Math.max(6,s*0.6);
223+
ctx.fillStyle='rgba(253,233,73,0.22)';
224+
ctx.beginPath(); ctx.arc(sx,sy,rr*1.9,0,Math.PI*2); ctx.fill();
225+
ctx.strokeStyle='#0d0b2c'; ctx.lineWidth=Math.max(4,s*0.22);
226+
ctx.beginPath(); ctx.arc(sx,sy,rr,0,Math.PI*2); ctx.stroke();
227+
ctx.strokeStyle='#fde949'; ctx.lineWidth=Math.max(2,s*0.12);
228+
ctx.beginPath(); ctx.arc(sx,sy,rr,0,Math.PI*2); ctx.stroke();
229+
startTag = {x:sx, y:sy-rr*1.9-2};
222230
}
223231
}
224232

@@ -291,6 +299,18 @@ export function draw(){
291299
}
292300
}
293301

302+
// START tag above the route start marker (front view only; drawn here so
303+
// the text isn't mirrored and sits above the cell layers)
304+
if (startTag){
305+
const fs = Math.max(10, Math.min(16, s*0.9));
306+
ctx.font = `800 ${fs}px ui-rounded, system-ui, sans-serif`;
307+
ctx.textAlign='center'; ctx.textBaseline='bottom';
308+
ctx.lineWidth = 4; ctx.strokeStyle='#0d0b2c'; ctx.lineJoin='round';
309+
ctx.strokeText('START', startTag.x, startTag.y);
310+
ctx.fillStyle='#fde949';
311+
ctx.fillText('START', startTag.x, startTag.y);
312+
}
313+
294314
// fine per-cell grid when zoomed close
295315
if (s>=10){
296316
ctx.strokeStyle='rgba(244,241,255,0.07)';

pages/pikachu-stitch-companion/js/ui.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { state, colourAt, isStitched, isColourComplete, setStitched, logEvent, u
66
import { draw, drawMiniMap, setConfettiHeatOn, confettiHeatOn } from './render.js';
77
import { resetProgress, markDirty } from './persistence.js';
88
import { getRoute, requestRoute, getPlanningColour, setOnRouteReady, invalidateAllRoutes, stitchesPerLength } from './planner.js';
9-
import { blockOrderList, gotoBlock, blockOf } from './blocks.js';
9+
import { blockOrderList, gotoBlock, blockOf, centreOnCell } from './blocks.js';
1010
import { stitchesPerHour, estimatedFinish, RAILROADING_SLOWDOWN, LENGTHS_PER_SKEIN, skeinsForLengths } from './insights.js';
1111

1212
/* ---------- legend ---------- */
@@ -23,7 +23,11 @@ COLORS.forEach((c,i)=>{
2323
chip.addEventListener('click', ()=>{
2424
state.selected = (state.selected === idx) ? null : idx;
2525
blockIdx = 0; // reset block-nav position (4.4) on colour change
26-
if (state.selected!=null) requestRoute(state.selected);
26+
centreOnStartPending = state.selected!=null;
27+
if (state.selected!=null){
28+
const r = requestRoute(state.selected);
29+
if (r && r.start!=null){ centreOnStartPending = false; centreOnCell(r.start); }
30+
}
2731
refreshUI(); draw();
2832
});
2933
legend.appendChild(chip);
@@ -139,7 +143,16 @@ export function updatePosReadout(i){
139143
posReadout.textContent = `col ${c+1}, row ${r+1}`;
140144
}
141145
}
142-
setOnRouteReady((v)=>{ if (state.selected===v) refreshRoutePanel(); });
146+
let centreOnStartPending = false; // set by the chip click; consumed once the route is ready
147+
setOnRouteReady((v, route)=>{
148+
if (state.selected!==v) return;
149+
if (centreOnStartPending && route && route.start!=null){
150+
centreOnStartPending = false;
151+
centreOnCell(route.start);
152+
blockIdx = 0; // block order is anchored at the start, so restart the walk
153+
}
154+
refreshUI();
155+
});
143156

144157
function refreshRoutePanel(){
145158
const v = state.selected;

0 commit comments

Comments
 (0)