from pathlib import Path root=Path(r'G:\项目\NumberSwipe-Variants\campaign-50') p=root/'ui.mjs';s=p.read_text(encoding='utf-8') s=s.replace("let toastTimer=null;", "let toastTimer=null;let channelViews=[];") s=s.replace("const bad=violations(level,path),badIds=new Set(bad.flatMap(b=>b.cells));", "const bad=violations(level,path),badIds=new Set(bad.flatMap(b=>b.cells));channelViews=[];") s=s.replace("if(c.direction)drawChannel(c.direction,x,y-(value?unit*.23:0),Math.min(unit*.25,value?unit*.14:22),foreground);", "if(c.direction){const radius=Math.min(unit*.26,28),state=directionState(c,badIds);drawDirectionPath(c,x,y);drawChannel(c.direction,x,y,radius,state);channelViews.push({cell:c.id,kind:c.direction,center:[x,y],radius,state});}") s=s.replace("if(value){ctx.font='800 '+font+'px '+BOARD_FONT;ctx.fillText", "if(value&&c.direction){const size=Math.max(10,Math.min(18,unit*.22));ctx.fillStyle=visited||start?'#FFFFFF':UI_THEME.cell;roundedRect(x+unit*.105,y-unit*.38,unit*.28,unit*.28,unit*.07);ctx.fill();ctx.fillStyle=UI_THEME.ink;ctx.font='800 '+size+'px '+BOARD_FONT;ctx.fillText(String(value),x+unit*.245,y-unit*.24);}\n else if(value){ctx.font='800 '+font+'px '+BOARD_FONT;ctx.fillText") a=s.index('function drawChannel(');b=s.index('function drawBarrier(',a) s=s[:a]+'''function sideOf(c,id){const a=level.cells.find(a=>a.id===id);return a.xc.x?'r':a.y0){const [dx,dy]=vectors[sideOf(c,path[k-1])];ctx.moveTo(x+dx*unit*.44,y+dy*unit*.44);ctx.lineTo(x,y);}else ctx.moveTo(x,y);if(kvector[k]);ctx.beginPath();ctx.moveTo(x+a[0]*r,y+a[1]*r);ctx.lineTo(x,y);ctx.lineTo(x+b[0]*r,y+b[1]*r);ctx.lineCap='round';ctx.lineJoin='round';ctx.lineWidth=10;ctx.strokeStyle=state==='correct'?'#C8C2FF':state==='error'?'#FFD5D7':'#DFE1EC';ctx.stroke();ctx.lineWidth=5;ctx.strokeStyle=state==='correct'?'#FFFFFF':state==='error'?'#E5484D':UI_THEME.ink;ctx.stroke();} ''' +s[b:] s=s.replace("records:records.length,screen:", "records:records.length,directionMarkers:channelViews,screen:") p.write_text(s,encoding='utf-8') p=root/'tests/ui.test.mjs';s=p.read_text(encoding='utf-8-sig').replace("import {solve}","import {solve,graph}") s=s.replace('append(e){this.children.push(e);}', 'append(...e){this.children.push(...e);}') s=s.replace("const window={devicePixelRatio:1", "get('screen-play').className='screen hidden';get('screen-select').className='screen';const window={devicePixelRatio:1") s+=''' test('direction glyph stays centered and same size on landing, exit, undo and errors',()=>{const ui=boot();ui.select(11);const l=pack.levels[10],p=solve(l)[0],c=l.cells.find(c=>c.direction),at=p.indexOf(c.id);const initial=ui.snapshot().directionMarkers.find(m=>m.cell===c.id);assert.equal(initial.state,'idle');for(const id of p.slice(0,at+1))ui.press(id);let active=ui.snapshot().directionMarkers.find(m=>m.cell===c.id);assert.deepEqual(active.center,initial.center);assert.equal(active.radius,initial.radius);assert.equal(active.state,'correct');assert.ok(ui.draws.some(d=>d.kind==='stroke'&&d.colour==='#BEB8FF'));ui.press(p[at+1]);active=ui.snapshot().directionMarkers.find(m=>m.cell===c.id);assert.deepEqual(active.center,initial.center);assert.equal(active.radius,initial.radius);ui.get('undo').click();assert.deepEqual(ui.snapshot().directionMarkers.find(m=>m.cell===c.id).center,initial.center);ui.get('restart').click();const g=graph(l);let wrong=null;function search(prefix){if(wrong)return;const last=prefix.at(-1);if(last===c.id){if(prefix.length>1){const from=l.cells.find(x=>x.id===prefix.at(-2));const side=from.xc.x?'r':from.ym.cell===c.id);assert.equal(bad.state,'error');assert.deepEqual(bad.center,initial.center);assert.equal(bad.radius,initial.radius);}); ''' p.write_text(s,encoding='utf-8')