33
44import { stage , view , applyZoom , clampView , draw , fit , cellAtClient , setSymbolsOn , symbolsOn } from './render.js' ;
55import { 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' ;
77import { markDirty } from './persistence.js' ;
88import { refreshUI , getBlockIdx , setBlockIdx , updatePosReadout } from './ui.js' ;
99import { invalidateRoute , requestRoute , getRoute , routeCells , markRoutePrefix } from './planner.js' ;
@@ -14,7 +14,7 @@ let markMode = false;
1414const markToggle = document . getElementById ( 'markToggle' ) ;
1515markToggle . 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(){
3030setStartBtn . 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 ---------- */
81116const flipBtn = document . getElementById ( 'flipBtn' ) ;
82117const viewLabel = document . getElementById ( 'viewLabel' ) ;
@@ -159,13 +194,20 @@ const MOVE_THRESH = 8;
159194
160195function 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){
174216function 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}
179226function 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 ) {
0 commit comments