Skip to content

Commit 16e5647

Browse files
committed
update
1 parent 65b518f commit 16e5647

10 files changed

Lines changed: 507 additions & 64 deletions

File tree

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// block ordering, and camera framing for a given block.
33

44
import { N } from './pattern.js';
5-
import { state, idxOf, rowOf, colOf, colourAt, isStitched } from './state.js';
5+
import { state, idxOf, rowOf, colOf, colourAt, isStitched, isOmitted, isMissed } from './state.js';
66
import { view, clampView, draw, stage } from './render.js';
77
import { getRoute } from './planner.js';
88

@@ -40,7 +40,7 @@ export function blocksForColour(v){
4040
const nb = blockCount();
4141
for(let br=0; br<nb; br++){
4242
for(let bc=0; bc<nb; bc++){
43-
if(blockCells(br,bc).some(i => colourAt(i)===v && !isStitched(i))) out.push({br,bc});
43+
if(blockCells(br,bc).some(i => colourAt(i)===v && !isStitched(i) && !isOmitted(i))) out.push({br,bc});
4444
}
4545
}
4646
return out;
@@ -53,7 +53,13 @@ export function blocksForColour(v){
5353
* (task 4.6): call this after a stitch toggle to detect completion.
5454
*/
5555
export function blockIsCompleteForColour(br, bc, v){
56-
return !blockCells(br,bc).some(i => colourAt(i)===v && !isStitched(i));
56+
return !blockCells(br,bc).some(i => colourAt(i)===v && !isStitched(i) && !isOmitted(i));
57+
}
58+
59+
/** blockHasMissedForColour(br, bc, v) -> true when the block holds at least
60+
* one cell of colour v flagged as missed (drives the mini-map highlight). */
61+
export function blockHasMissedForColour(br, bc, v){
62+
return blockCells(br,bc).some(i => colourAt(i)===v && isMissed(i));
5763
}
5864

5965
/**
@@ -64,7 +70,7 @@ export function blockIsCompleteForColour(br, bc, v){
6470
const lastKnownStart = new Map(); // colour -> cell; survives route invalidation
6571
export function startCellFor(v){
6672
const sp = state.startPoints[v];
67-
if (sp!=null && colourAt(sp)===v && !isStitched(sp)){ lastKnownStart.set(v, sp); return sp; }
73+
if (sp!=null && colourAt(sp)===v && !isStitched(sp) && !isOmitted(sp)){ lastKnownStart.set(v, sp); return sp; }
6874
const route = getRoute(v);
6975
if (route && route.start!=null){ lastKnownStart.set(v, route.start); return route.start; }
7076
// route is being re-planned (e.g. right after a mark) — keep the previous

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

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { stage, view, applyZoom, clampView, draw, fit, cellAtClient, setSymbolsOn, symbolsOn } from './render.js';
55
import { N } from './pattern.js';
6-
import { state, colourAt, isStitched, setStitched, hasTieOff, setTieOff, logEvent } from './state.js';
6+
import { state, colourAt, isStitched, isMissed, isOmitted, setStitched, setMissed, setOmitted, hasTieOff, setTieOff, logEvent } from './state.js';
77
import { markDirty } from './persistence.js';
88
import { refreshUI, getBlockIdx, setBlockIdx, updatePosReadout } from './ui.js';
99
import { invalidateRoute, requestRoute, getRoute, routeCells, markRoutePrefix } from './planner.js';
@@ -14,7 +14,7 @@ let markMode = false;
1414
const markToggle = document.getElementById('markToggle');
1515
markToggle.addEventListener('click', ()=>{
1616
markMode = !markMode;
17-
if (markMode){ disarmSetStart(); disarmUpToHere(); }
17+
if (markMode){ disarmSetStart(); disarmUpToHere(); disarmMissedPaint(); }
1818
markToggle.classList.toggle('on', markMode);
1919
markToggle.setAttribute('aria-pressed', String(markMode));
2020
});
@@ -30,7 +30,7 @@ function disarmSetStart(){
3030
setStartBtn.addEventListener('click', ()=>{
3131
if (state.selected==null) return;
3232
setStartArmed = !setStartArmed;
33-
if (setStartArmed){ markMode = false; markToggle.classList.remove('on'); markToggle.setAttribute('aria-pressed','false'); disarmUpToHere(); }
33+
if (setStartArmed){ markMode = false; markToggle.classList.remove('on'); markToggle.setAttribute('aria-pressed','false'); disarmUpToHere(); disarmMissedPaint(); }
3434
setStartBtn.classList.toggle('on', setStartArmed);
3535
setStartBtn.setAttribute('aria-pressed', String(setStartArmed));
3636
});
@@ -48,7 +48,7 @@ upToHereBtn.addEventListener('click', ()=>{
4848
upToHereArmed = !upToHereArmed;
4949
if (upToHereArmed){
5050
markMode = false; markToggle.classList.remove('on'); markToggle.setAttribute('aria-pressed','false');
51-
disarmSetStart();
51+
disarmSetStart(); disarmMissedPaint();
5252
}
5353
upToHereBtn.classList.toggle('on', upToHereArmed);
5454
upToHereBtn.setAttribute('aria-pressed', String(upToHereArmed));
@@ -77,6 +77,41 @@ function markUpToCell(i){
7777
return true;
7878
}
7979

80+
/* ---------- missed / omit paint mode ---------- */
81+
// The "Missed" sheet (ui.js) owns the range form; this owns the paint
82+
// gesture it hands off to. Armed from #missedPaintBtn, it reuses the
83+
// ordinary marking gesture below but writes the missed/omitted flag chosen
84+
// in #missedAction instead of F_STITCHED — including the drag-to-paint and
85+
// paint-again-to-clear behaviour.
86+
let missedPaintArmed = false; // false | 'missed' | 'omit'
87+
const missedBtn = document.getElementById('missedBtn');
88+
const missedPaintBtn = document.getElementById('missedPaintBtn');
89+
const missedActionSel = document.getElementById('missedAction');
90+
function disarmMissedPaint(){
91+
missedPaintArmed = false;
92+
delete missedBtn.dataset.painting;
93+
missedBtn.classList.remove('on');
94+
missedBtn.setAttribute('aria-pressed','false');
95+
}
96+
missedPaintBtn.addEventListener('click', ()=>{
97+
missedPaintArmed = missedActionSel.value;
98+
markMode = false; markToggle.classList.remove('on'); markToggle.setAttribute('aria-pressed','false');
99+
disarmSetStart(); disarmUpToHere();
100+
missedBtn.dataset.painting = '1'; // ui.js reads this: a tap on #missedBtn
101+
missedBtn.classList.add('on'); // stops painting instead of reopening
102+
missedBtn.setAttribute('aria-pressed','true');
103+
document.getElementById('missedSheet').classList.remove('show');
104+
celebrate(missedPaintArmed==='missed'
105+
? 'Paint the squares you missed · tap Missed again to stop'
106+
: 'Paint the squares to drop from the design · tap Missed again to stop');
107+
});
108+
// ui.js owns the button's click (it opens the sheet) and hands the stop back
109+
// here as an event, rather than both modules racing listeners on one element
110+
missedBtn.addEventListener('missed:stop', ()=>{
111+
disarmMissedPaint();
112+
celebrate('Done painting');
113+
});
114+
80115
/* ---------- flip (front/back view) + symbol overlay toggles ---------- */
81116
const flipBtn = document.getElementById('flipBtn');
82117
const viewLabel = document.getElementById('viewLabel');
@@ -159,13 +194,20 @@ const MOVE_THRESH = 8;
159194

160195
function beginGesture(e){
161196
const i = cellAtClient(e.clientX, e.clientY);
162-
if (i<0 || colourAt(i)!==state.selected) return;
163-
const dir = !isStitched(i); // mark unless already stitched
197+
if (i<0) return;
198+
const flag = missedPaintArmed;
199+
// paint mode works on any colour when none is isolated; ordinary marking
200+
// stays scoped to the selected colour as before
201+
if (flag){ if (colourAt(i)===0 || (state.selected!=null && colourAt(i)!==state.selected)) return; }
202+
else if (colourAt(i)!==state.selected) return;
203+
const isOn = flag==='missed' ? isMissed : flag==='omit' ? isOmitted : isStitched;
204+
const dir = !isOn(i); // paint unless the cell is already in that state
164205
gesture = {
165206
startX:e.clientX, startY:e.clientY,
166-
startCell:i, dir, touched:new Set(), moved:false,
207+
startCell:i, dir, flag, touched:new Set(), prev:[], moved:false,
167208
};
168209
toggleCell(i, dir);
210+
if (flag) return; // long-press tie-off is a stitching gesture only
169211
gesture.longPressTimer = setTimeout(()=>{
170212
if (!gesture || gesture.moved) return;
171213
fireTieOff(gesture.startCell);
@@ -174,6 +216,11 @@ function beginGesture(e){
174216
function toggleCell(i, dir){
175217
if (gesture.touched.has(i)) return;
176218
gesture.touched.add(i);
219+
if (gesture.flag){
220+
gesture.prev.push({m: isMissed(i), o: isOmitted(i), s: isStitched(i)});
221+
if (gesture.flag==='missed') setMissed(i, dir); else setOmitted(i, dir);
222+
return;
223+
}
177224
setStitched(i, dir);
178225
}
179226
function fireTieOff(i){
@@ -188,10 +235,22 @@ function commitGesture(){
188235
clearTimeout(gesture.longPressTimer);
189236
const touched = [...gesture.touched];
190237
const dir = gesture.dir;
238+
const flag = gesture.flag;
239+
const prev = gesture.prev;
240+
gesture = null;
241+
if (touched.length && flag){
242+
// one journey entry per paint stroke, carrying the per-cell snapshots
243+
// undoLast needs to put the cells back exactly as they were
244+
const colours = new Set(touched.map(colourAt));
245+
logEvent({ kind: flag==='missed' ? 'missed' : 'omit', c: colours.size===1 ? [...colours][0] : 0, cells: touched, prev });
246+
colours.forEach(v => invalidateRoute(v));
247+
if (state.selected!=null) requestRoute(state.selected);
248+
markDirty(); refreshUI(); draw();
249+
return;
250+
}
191251
if (touched.length){
192252
logEvent({ kind: dir ? 'mark' : 'unmark', c: state.selected, cells: touched });
193253
}
194-
gesture = null;
195254
markDirty(); refreshUI(); draw();
196255
if (dir && touched.length) checkBlockComplete(touched, state.selected);
197256
}
@@ -233,7 +292,12 @@ function abortGesture(){
233292
if (!gesture) return;
234293
clearTimeout(gesture.longPressTimer);
235294
// revert cells already toggled by this gesture, in reverse order
236-
[...gesture.touched].reverse().forEach(i=>setStitched(i, !gesture.dir));
295+
const flag = gesture.flag;
296+
[...gesture.touched].reverse().forEach(i=>{
297+
if (flag==='missed') setMissed(i, !gesture.dir);
298+
else if (flag==='omit') setOmitted(i, !gesture.dir);
299+
else setStitched(i, !gesture.dir);
300+
});
237301
gesture = null;
238302
refreshUI(); draw();
239303
}
@@ -273,6 +337,8 @@ stage.addEventListener('pointerdown',e=>{
273337
setBlockIdx(0); // block order is anchored at the start point, so restart the walk
274338
refreshUI(); draw();
275339
}
340+
} else if (missedPaintArmed){
341+
beginGesture(e);
276342
} else if (markMode && state.selected!=null){
277343
beginGesture(e);
278344
} else {
@@ -309,7 +375,10 @@ stage.addEventListener('pointermove',e=>{
309375
clearTimeout(gesture.longPressTimer);
310376
}
311377
const i = cellAtClient(e.clientX, e.clientY);
312-
if (i>=0 && colourAt(i)===state.selected) toggleCell(i, gesture.dir);
378+
const paintable = i>=0 && (gesture.flag
379+
? colourAt(i)!==0 && (state.selected==null || colourAt(i)===state.selected)
380+
: colourAt(i)===state.selected);
381+
if (paintable) toggleCell(i, gesture.dir);
313382
return; // suppress the pan branch while a marking gesture is active
314383
}
315384
if (pivotDown && Math.hypot(e.clientX-pivotDown.x, e.clientY-pivotDown.y)>MOVE_THRESH){

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

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// persistence.js - localStorage save/load of stitching progress.
22
// load() is exported but NOT invoked here; main.js calls it at bootstrap.
33

4-
import { COLORS, CELL_COUNT, F_STITCHED } from './pattern.js';
4+
import { COLORS, CELL_COUNT, F_STITCHED, F_OMITTED } from './pattern.js';
55
import { state, DEFAULT_SETTINGS, colourAt } from './state.js';
66
import { refreshUI } from './ui.js';
77
import { draw } from './render.js';
@@ -30,7 +30,7 @@ export function decodeFlags(str){
3030
export function save(){
3131
try{
3232
const data = {
33-
v: 1,
33+
v: 2,
3434
flags: encodeFlags(),
3535
log: state.journey,
3636
settings: state.settings,
@@ -40,18 +40,22 @@ export function save(){
4040
state.dirty = false;
4141
}catch(e){ /* ignore quota / serialization errors */ }
4242
}
43+
// v1 saves have no missed/omitted bits set, so the same rebuild works for
44+
// both schema versions — the counts simply come out zero.
4345
export function rebuildStitchedCount(){
4446
state.stitchedCount = new Int32Array(COLORS.length+1);
47+
state.omittedCount = new Int32Array(COLORS.length+1);
4548
for (let i=0;i<CELL_COUNT;i++){
46-
if (state.stitchFlags[i] & F_STITCHED){
47-
const v = colourAt(i);
48-
if (v) state.stitchedCount[v]++;
49-
}
49+
const v = colourAt(i);
50+
if (!v) continue;
51+
if (state.stitchFlags[i] & F_STITCHED) state.stitchedCount[v]++;
52+
if (state.stitchFlags[i] & F_OMITTED) state.omittedCount[v]++;
5053
}
5154
}
5255
export function freshState(){
5356
state.stitchFlags = new Uint8Array(CELL_COUNT);
5457
state.stitchedCount = new Int32Array(COLORS.length+1);
58+
state.omittedCount = new Int32Array(COLORS.length+1);
5559
state.journey = [];
5660
state.settings = Object.assign({}, DEFAULT_SETTINGS);
5761
state.startPoints = {};
@@ -61,7 +65,7 @@ export function load(){
6165
const raw = localStorage.getItem(STORE_KEY);
6266
if (!raw) { freshState(); return; }
6367
const data = JSON.parse(raw);
64-
if (!data || data.v !== 1) { freshState(); return; }
68+
if (!data || (data.v !== 1 && data.v !== 2)) { freshState(); return; }
6569
const flags = decodeFlags(data.flags);
6670
if (flags.length !== CELL_COUNT) { freshState(); return; }
6771
state.stitchFlags = flags;

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
// segmentation and cached, idle-scheduled route computation.
44

55
import { N, COLORS, cells } from './pattern.js';
6-
import { state, idxOf, rowOf, colOf, colourAt, isStitched } from './state.js';
6+
import { state, idxOf, rowOf, colOf, colourAt, isStitched, isMissed, isOmitted } from './state.js';
77
import { draw } from './render.js';
88

99
/* ---------------- 3.1 clustering ---------------- */
1010

1111
// Iterative 8-connectivity flood fill over unstitched cells of colour v.
1212
// Explicit stack, no recursion (largest colour cluster ~1.5k cells).
13+
// Cells omitted from the design (F_OMITTED) are skipped exactly like
14+
// stitched ones: they are not stitching work, so they neither seed nor
15+
// extend a cluster. Cells flagged missed are ordinary unstitched work that
16+
// additionally sets cluster.missed, which orderClusters routes to first.
1317
export function clusterColour(v){
1418
const seen = new Uint8Array(N*N);
1519
const clusters = [];
@@ -18,15 +22,17 @@ export function clusterColour(v){
1822
if (cells[r0][c0] !== v) continue;
1923
const start = idxOf(r0,c0);
2024
if (seen[start]) continue;
21-
if (isStitched(start)) { seen[start]=1; continue; }
25+
if (isStitched(start) || isOmitted(start)) { seen[start]=1; continue; }
2226
const stack = [start];
2327
seen[start] = 1;
2428
const cellsOut = [];
29+
let missed = 0;
2530
let minR=r0, maxR=r0, minC=c0, maxC=c0, sumR=0, sumC=0;
2631
while (stack.length){
2732
const i = stack.pop();
2833
const r = rowOf(i), c = colOf(i);
2934
cellsOut.push(i);
35+
if (isMissed(i)) missed++;
3036
sumR += r; sumC += c;
3137
if (r<minR) minR=r; if (r>maxR) maxR=r;
3238
if (c<minC) minC=c; if (c>maxC) maxC=c;
@@ -39,7 +45,7 @@ export function clusterColour(v){
3945
const ni = idxOf(nr,nc);
4046
if (seen[ni]) continue;
4147
seen[ni] = 1;
42-
if (isStitched(ni)) continue;
48+
if (isStitched(ni) || isOmitted(ni)) continue;
4349
stack.push(ni);
4450
}
4551
}
@@ -48,6 +54,7 @@ export function clusterColour(v){
4854
cells: cellsOut,
4955
centroid: { r: sumR/cellsOut.length, c: sumC/cellsOut.length },
5056
bbox: { minR, maxR, minC, maxC },
57+
missed,
5158
});
5259
}
5360
}
@@ -60,6 +67,18 @@ const PATTERN_CENTRE = { r: 75, c: 75 };
6067

6168
export function suggestStart(v, clusters){
6269
if (!clusters.length) return null;
70+
// backfill: when cells have been flagged as missed, the route starts at
71+
// the missed work rather than wherever the normal heuristic would land,
72+
// so the stitcher is sent straight back to the gap.
73+
if (state.settings.backfillFirst){
74+
const withMissed = clusters.filter(cl => cl.missed);
75+
if (withMissed.length){
76+
let best = withMissed[0];
77+
for (const cl of withMissed) if (cl.missed > best.missed) best = cl;
78+
const missedCells = best.cells.filter(isMissed);
79+
if (missedCells.length) return missedCells[0];
80+
}
81+
}
6382
if (state.settings.origin === 'page'){
6483
// top-left-most cluster: smallest (minR, then minC); then its
6584
// top-left-most cell within that cluster.
@@ -134,9 +153,15 @@ const CONFETTI_MAX_CELLS = 2;
134153
export function orderClusters(clusters, startIdx){
135154
if (clusters.length <= 1) return clusters.slice();
136155
const startPt = { r: rowOf(startIdx), c: colOf(startIdx) };
137-
const isolated = clusters.filter(cl => cl.cells.length <= CONFETTI_MAX_CELLS);
138-
const large = clusters.filter(cl => cl.cells.length > CONFETTI_MAX_CELLS);
139-
const groups = state.settings.confettiFirst ? [isolated, large] : [large, isolated];
156+
// backfill partition (settings.backfillFirst): clusters holding cells the
157+
// stitcher flagged as missed are visited before anything else, ahead of
158+
// the confetti split, so a forgotten patch is caught up on first rather
159+
// than waiting for the tour to wander back.
160+
const backfill = state.settings.backfillFirst ? clusters.filter(cl => cl.missed) : [];
161+
const rest = backfill.length ? clusters.filter(cl => !cl.missed) : clusters;
162+
const isolated = rest.filter(cl => cl.cells.length <= CONFETTI_MAX_CELLS);
163+
const large = rest.filter(cl => cl.cells.length > CONFETTI_MAX_CELLS);
164+
const groups = state.settings.confettiFirst ? [backfill, isolated, large] : [backfill, large, isolated];
140165
const deadline = Date.now() + 200; // shared 200ms 2-opt budget across both partitions
141166
let fromPt = startPt;
142167
let full = [];
@@ -467,9 +492,14 @@ export function planRoute(v){
467492
return { colour: v, start: null, legs: [], hops: [], lengths: [] };
468493
}
469494
const existingStart = state.startPoints[v];
470-
const start = (existingStart!=null && colourAt(existingStart)===v && !isStitched(existingStart))
471-
? existingStart
472-
: suggestStart(v, clusters);
495+
// a pinned start point is honoured unless backfill work is outstanding and
496+
// the pin isn't part of it — the whole point of flagging missed cells is
497+
// to be sent back to them, so they override a stale pin.
498+
const backfillPending = state.settings.backfillFirst && clusters.some(cl => cl.missed);
499+
const pinUsable = existingStart!=null && colourAt(existingStart)===v
500+
&& !isStitched(existingStart) && !isOmitted(existingStart)
501+
&& (!backfillPending || isMissed(existingStart));
502+
const start = pinUsable ? existingStart : suggestStart(v, clusters);
473503

474504
const ordered = orderClusters(clusters, start);
475505

0 commit comments

Comments
 (0)