@@ -5,7 +5,7 @@ import { COLORS, TOTAL, CELL_COUNT, N } from './pattern.js';
55import { state , colourAt , isStitched , isColourComplete , setStitched , logEvent , undoLast , rowOf , colOf } from './state.js' ;
66import { draw , drawMiniMap , setConfettiHeatOn , confettiHeatOn } from './render.js' ;
77import { resetProgress , markDirty } from './persistence.js' ;
8- import { getRoute , requestRoute , getPlanningColour , setOnRouteReady , invalidateAllRoutes , stitchesPerLength } from './planner.js' ;
8+ import { getRoute , requestRoute , getPlanningColour , setOnRouteReady , invalidateAllRoutes , stitchesPerLength , routeCells , markRoutePrefix , invalidateRoute } from './planner.js' ;
99import { blockOrderList , gotoBlock , blockOf , centreOnCell , blockSize , BLOCK_SIZES , blockAtViewCentre , blockCells } from './blocks.js' ;
1010import { stitchesPerHour , estimatedFinish , RAILROADING_SLOWDOWN , LENGTHS_PER_SKEIN , skeinsForLengths } from './insights.js' ;
1111
@@ -60,34 +60,80 @@ function blockTargetCells(){
6060 const v = state . selected ;
6161 return { br, bc, cells : blockCells ( br , bc ) . filter ( i => { const c = colourAt ( i ) ; return c !== 0 && ( v == null || c === v ) ; } ) } ;
6262}
63+ /**
64+ * blockRouteVisit(v, br, bc) - route-aware view of the block: the route's
65+ * *first contiguous visit* to this block (cells the needle reaches before
66+ * the path leaves the block), plus how many of the block's cells the route
67+ * only comes back for later. null when no route is planned yet.
68+ */
69+ function blockRouteVisit ( v , br , bc ) {
70+ const route = getRoute ( v ) ;
71+ if ( ! route ) return null ;
72+ const inBlock = new Set ( blockCells ( br , bc ) ) ;
73+ const cells = routeCells ( route ) ;
74+ let i0 = cells . findIndex ( i => inBlock . has ( i ) ) ;
75+ if ( i0 < 0 ) return { cells, i0 :- 1 , j :- 1 , visit :[ ] , later :0 } ;
76+ let j = i0 ;
77+ while ( j + 1 < cells . length && inBlock . has ( cells [ j + 1 ] ) ) j ++ ;
78+ const visit = cells . slice ( i0 , j + 1 ) ;
79+ let later = 0 ;
80+ for ( let k = j + 1 ; k < cells . length ; k ++ ) if ( inBlock . has ( cells [ k ] ) ) later ++ ;
81+ return { cells, i0, j, visit, later } ;
82+ }
6383markBlockBtn . addEventListener ( 'click' , ( ) => {
6484 const { br, bc, cells } = blockTargetCells ( ) ;
6585 if ( ! cells . length ) { celebrateBulk ( 'Nothing of this colour in this block' ) ; return ; }
86+ const v = state . selected ;
6687 const allDone = cells . every ( isStitched ) ;
67- const on = ! allDone ;
68- const toggled = cells . filter ( i => isStitched ( i ) !== on ) ;
69- toggled . forEach ( i => setStitched ( i , on ) ) ;
70- logEvent ( { kind :'bulk' , c : state . selected ?? 0 , cells : toggled , unmark : ! on } ) ;
88+ if ( allDone ) {
89+ // toggle off: unmark the whole block for this colour (geometric)
90+ cells . forEach ( i => setStitched ( i , false ) ) ;
91+ logEvent ( { kind :'bulk' , c : v ?? 0 , cells, unmark : true } ) ;
92+ refreshUI ( ) ; draw ( ) ;
93+ return ;
94+ }
95+ const rv = v != null ? blockRouteVisit ( v , br , bc ) : null ;
96+ const prevStart = v != null ? state . startPoints [ v ] : undefined ;
97+ let toggled ;
98+ if ( rv && rv . visit . length ) {
99+ // route-aware: only the path's current visit to this block; cells the
100+ // route returns for later stay unstitched so the plan isn't corrupted.
101+ // If the route *starts* in this block, also move the start forward so
102+ // the re-plan continues from where the needle is.
103+ if ( rv . i0 === 0 ) toggled = markRoutePrefix ( v , rv . cells , rv . j , setStitched ) ;
104+ else { toggled = rv . visit . filter ( i => ! isStitched ( i ) ) ; toggled . forEach ( i => setStitched ( i , true ) ) ; }
105+ invalidateRoute ( v ) ; requestRoute ( v ) ;
106+ } else {
107+ toggled = cells . filter ( i => ! isStitched ( i ) ) ;
108+ toggled . forEach ( i => setStitched ( i , true ) ) ;
109+ }
110+ const ev = { kind :'bulk' , c : v ?? 0 , cells : toggled , unmark : false } ;
111+ if ( rv && rv . i0 === 0 ) ev . prevStart = prevStart == null ? null : prevStart ; // start was advanced; undo restores it
112+ logEvent ( ev ) ;
71113 refreshUI ( ) ; draw ( ) ;
72- if ( ! on ) return ;
73- const v = state . selected ;
74114 if ( v != null && isColourComplete ( v ) ) { celebrateBulk ( 'Colour finished!' ) ; return ; }
75- if ( v != null ) {
76- // same auto-advance as a drag-mark finishing a block: the done block has
77- // dropped out of the list, so the next one sits at the same index
115+ const skipped = rv ? rv . later : 0 ;
116+ const blockDone = cells . every ( isStitched ) ;
117+ if ( v != null && blockDone ) {
78118 const list = blockOrderList ( v ) ;
79119 const next = list [ Math . min ( blockIdx , list . length - 1 ) ] ;
80120 if ( next ) { celebrateBulk ( 'Block done! Moving on…' ) ; blockIdx = Math . min ( blockIdx , list . length - 1 ) ; gotoBlock ( next . br , next . bc ) ; refreshBlockNav ( ) ; }
81121 else celebrateBulk ( 'Block done!' ) ;
82- } else celebrateBulk ( 'Block done!' ) ;
122+ } else if ( skipped ) {
123+ celebrateBulk ( `Marked ${ toggled . length } · ${ skipped } left for a later pass` ) ;
124+ } else celebrateBulk ( `Marked ${ toggled . length } stitch${ toggled . length === 1 ?'' :'es' } ` ) ;
83125} ) ;
84126function refreshMarkBlockBtn ( ) {
85- const { cells } = blockTargetCells ( ) ;
127+ const { br , bc , cells } = blockTargetCells ( ) ;
86128 const v = state . selected ;
87129 const what = v != null ? `DMC ${ COLORS [ v - 1 ] [ 0 ] } ` : 'all colours' ;
88130 const done = cells . length && cells . every ( isStitched ) ;
89131 const B = blockSize ( ) ;
90- const lbl = done ? `Unmark this ${ B } ×${ B } block (${ what } )` : `Mark this ${ B } ×${ B } block as stitched (${ what } )` ;
132+ let lbl = done ? `Unmark this ${ B } ×${ B } block (${ what } )` : `Mark this ${ B } ×${ B } block as stitched (${ what } )` ;
133+ if ( ! done && v != null ) {
134+ const rv = blockRouteVisit ( v , br , bc ) ;
135+ if ( rv && rv . later ) lbl += ` — ${ rv . later } cell${ rv . later === 1 ?'' :'s' } belong to later legs and stay unmarked` ;
136+ }
91137 markBlockBtn . title = lbl ; markBlockBtn . setAttribute ( 'aria-label' , lbl ) ;
92138 markBlockBtn . classList . toggle ( 'on' , ! ! done ) ;
93139 markBlockBtn . setAttribute ( 'aria-pressed' , String ( ! ! done ) ) ;
@@ -147,6 +193,8 @@ export function refreshUI(){
147193 : `DMC ${ COLORS [ state . selected - 1 ] [ 0 ] } · ${ state . stitchedCount [ state . selected ] } / ${ COLORS [ state . selected - 1 ] [ 3 ] } ` ;
148194
149195 setStartBtn . disabled = state . selected == null ;
196+ upToHereBtn . disabled = state . selected == null ;
197+ if ( state . selected == null ) { upToHereBtn . classList . remove ( 'on' ) ; upToHereBtn . setAttribute ( 'aria-pressed' , 'false' ) ; }
150198 refreshBlockSizeUI ( ) ;
151199 refreshMarkBlockBtn ( ) ;
152200 refreshRoutePanel ( ) ;
@@ -161,6 +209,7 @@ const markToggle = document.getElementById('markToggle');
161209const undoBtn = document . getElementById ( 'undoBtn' ) ;
162210const readout = document . getElementById ( 'colourReadout' ) ;
163211const setStartBtn = document . getElementById ( 'setStartBtn' ) ;
212+ const upToHereBtn = document . getElementById ( 'upToHereBtn' ) ;
164213undoBtn . addEventListener ( 'click' , undoLast ) ;
165214
166215/* ---------- route summary readout (3.13) ---------- */
0 commit comments