44import { N } from './pattern.js' ;
55import { state , idxOf , rowOf , colOf , colourAt , isStitched } from './state.js' ;
66import { view , clampView , draw , stage } from './render.js' ;
7+ import { getRoute } from './planner.js' ;
78
89export const BLOCK_SIZE = 10 ;
910export 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 */
6080export 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+ }
0 commit comments