PageSourceSearch

https://unglitch.netlify.app/autoroute/index.js

js unglitch.netlify.app collected 2026-10-03 11:33:52 UTC 19,042 bytes, 782 lines download raw bytes

1/*global describe read_png */
2
3var TRC_R = 3;
4var CLR_W = 8;
5
6let bShowDt = false;
7
8let mouse_mode = 0;
9
10let jsr = 0x5EED;
11// jsr = ~~(Math.random()*4294967295)
12
13
14function rand(){
15  jsr^=(jsr<<17);
16  jsr^=(jsr>>>13);
17  jsr^=(jsr<<5);
18  jsr>>>=0;
19  return jsr/4294967295;
20}
21
22
23function load_png(path){
24  let xhr = new XMLHttpRequest();
25  // xhr.responseType = 'arraybuffer';
26  xhr.overrideMimeType('text/plain; charset=x-user-defined');
27  xhr.open("GET", path, false);
28  
29  xhr.send();
30  let res = xhr.response;
31  // console.log(res);
32  let bytes = Uint8Array.from(res, c => c.charCodeAt(0));
33  
34  let [data,w,h] = read_png(bytes);
35  
36  let D = [];
37  for (let i = 0; i < data.length; i+= 4){
38    let r = data[i];
39    let g = data[i+1];
40    let b = data[i+2];
41    let c = (r << 16) | (g << 8) | b;
42    D.push(c);
43  }
44  
45  return [D,w,h];
46}
47
48
49
50
51
52function dist_transform(b,m,n) {
53  // Meijster distance
54  // adapted from https://github.com/parmanoir/Meijster-distance
55  function EDT_f(x, i, g_i) {
56    return (x - i) * (x - i) + g_i * g_i;
57  }
58  function EDT_Sep(i, u, g_i, g_u) {
59    return Math.floor((u * u - i * i + g_u * g_u - g_i * g_i) / (2 * (u - i)));
60  }
61  // First phase
62  let infinity = m + n;
63  let g = new Array(m * n).fill(0);
64  for (let x = 0; x < m; x++) {
65    if (b[x + 0 * m]){
66      g[x + 0 * m] = 0;
67    }else{
68      g[x + 0 * m] = infinity;
69    }
70    // Scan 1
71    for (let y = 1; y < n; y++) {
72      if (b[x + y * m]){
73        g[x + y * m] = 0;
74      }else{
75        g[x + y * m] = 1 + g[x + (y - 1) * m];
76      }
77    }
78    // Scan 2
79    for (let y = n - 2; y >= 0; y--) {
80      if (g[x + (y + 1) * m] < g[x + y * m]){
81        g[x + y * m] = 1 + g[x + (y + 1) * m];
82      }
83    }
84  }
85
86  // Second phase
87  let dt = new Array(m * n).fill(0);
88  let s = new Array(m).fill(0);
89  let t = new Array(m).fill(0);
90  let q = 0;
91  let w;
92  for (let y = 0; y < n; y++) {
93    q = 0;
94    s[0] = 0;
95    t[0] = 0;
96
97    // Scan 3
98    for (let u = 1; u < m; u++) {
99      while (q >= 0 && EDT_f(t[q], s[q], g[s[q] + y * m]) > EDT_f(t[q], u, g[u + y * m])){
100        q--;
101      }
102      if (q < 0) {
103        q = 0;
104        s[0] = u;
105      } else {
106        w = 1 + EDT_Sep(s[q], u, g[s[q] + y * m], g[u + y * m]);
107        if (w < m) {
108          q++;
109          s[q] = u;
110          t[q] = w;
111        }
112      }
113    }
114    // Scan 4
115    for (let u = m - 1; u >= 0; u--) {
116      let d = EDT_f(u, s[q], g[s[q] + y * m]);
117
118      d = Math.floor(Math.sqrt(d));
119      dt[u + y * m] = d;
120      if (u == t[q]) q--;
121    }
122  }
123  return dt;
124}
125
126let W = 128;
127let H = 128;
128let B = new Array(W*H).fill(0);
129let RAD = 3;
130
131let Bhist = [B.slice()];
132
133let cnv = document.createElement("canvas");
134cnv.width = W;
135cnv.height = H;
136let ctx = cnv.getContext('2d',{ willReadFrequently: true });
137ctx.fillRect(0,0,W,H)
138document.body.appendChild(cnv);
139
140display(B);
141
142
143let dtdiv = document.createElement("div");
144document.body.appendChild(dtdiv);
145
146
147function display(im){
148  // for (let i = 0; i < H; i++){
149  //   for (let j = 0; j < W; j++){
150  //     ctx.fillStyle='#'+im[i*W+j].toString(16).padStart(6,'0')
151  //     ctx.fillRect(j*SCL,i*SCL,SCL,SCL);
152  //   }
153  // }
154
155  let imd = ctx.getImageData(0,0,W,H);
156  for (let i = 0; i < im.length; i ++){
157    imd.data[i*4  ] = (im[i]>>16)&0xff;
158    imd.data[i*4+1] = (im[i]>>8)&0xff;
159    imd.data[i*4+2] = im[i]&0xff;
160    imd.data[i*4+3] = 255;
161  }
162  ctx.putImageData(imd,0,0);
163}
164
165function get_color_set(B){
166  return Array.from(new Set(B)).filter(x=>x);
167}
168
169let mouseX;
170let mouseY;
171let mouseIsDown = false;
172
173let curr_color = 0xff3f3f;
174
175
176
177
178
179
180
181let openset = new Set();
182let came_from;
183let gscore;
184let fscore;
185
186let astar_old_size = 0;
187
188function astar(im, x0, y0){
189  
190  let neighbors = [0,0,0,0];
191  let n_neighbors = 0;
192
193  function heur_dist(a){
194    if (im.data[a] == -2){
195      return 0;
196    }else if (im.data[a] == -1){
197      return 1;
198    }else{
199      return im.data[a];
200    }
201  }
202  function reconstruct_path(w, current){
203    let path = [];
204    path.push([current%w, ~~(current/w)]);
205    while ( came_from[current] != -1){
206      current = came_from[current];
207      path.push([current%w, ~~(current/w)]);
208    }
209    return path;
210  }
211
212  function get_neighbors(im, n){
213    let c = n % im.w;
214    let r = ~~(n / im.w);
215    n_neighbors = 0;
216    if (c > 0 && im.data[n-1]<255) neighbors[n_neighbors++] = n-1;
217    if (c < im.w-1 && im.data[n+1]<255) neighbors[n_neighbors++] = n+1;
218    if (r > 0 && im.data[n-im.w]<255) neighbors[n_neighbors++] = n-im.w;
219    if (r < im.h-1 && im.data[n+im.w]<255) neighbors[n_neighbors++] = n+im.w;
220  }
221  
222  let w = im.w;
223  let h = im.h;
224  let w_h = w*h; 
225  
226  if (astar_old_size < w_h){
227    came_from = new Int32Array(w_h);
228    gscore = new Float32Array(w_h);
229    fscore = new Float32Array(w_h);
230    astar_old_size = w_h;
231  }
232
233  let start = y0*w+x0;
234
235  openset.clear();
236  
237  came_from[start] = -1;
238  openset.add(start);
239  
240  for (let i = 0; i < w_h; i++) gscore[i] = Infinity;
241  for (let i = 0; i < w_h; i++) fscore[i] = Infinity;
242  for (let i = 0; i < w_h; i++) came_from[i] = -1;
243
244  gscore[start] = 0;
245  fscore[start] = heur_dist(start);
246  
247  
248  while (openset.size){
249    // console.log(n_openset,im.data.length);
250    
251    let amin = -1;
252    let fmin = Infinity;
253    for (let q of openset){
254      let f = fscore[q];
255      if ( f < fmin){
256        fmin = f;
257        amin = q;
258      }
259    }
260
261    let current = amin;
262    
263    if (im.data[current] == -2){
264      return reconstruct_path(w,current);
265    }
266
267    openset.delete(amin);
268    
269    get_neighbors(im,current);
270
271    for (let i = 0; i < n_neighbors; i++){
272      let neighbor = neighbors[i];
273      let penalty = Math.max(im.data[neighbor],0);
274      
275      let tentative_gscore = gscore[current] + 1 + penalty;
276      if (tentative_gscore < gscore[neighbor]){
277        came_from[neighbor] = current;
278        gscore[neighbor] = tentative_gscore;
279        fscore[neighbor] = gscore[neighbor] + heur_dist(neighbor);
280        openset.add(neighbor);
281      }
282    }
283  }
284  return null;
285}
286
287
288
289function bucketfill(im0,x0,y0,val){
290  let val0 = im0[y0*W+x0];
291  if (val0 == val){
292    return;
293  }
294  let Q = [];
295  Q.push([x0,y0]);
296  while (Q.length){
297    let [x,y] = Q.pop();
298    if (x > 0 && im0[y*W+(x-1)]==val0){
299      im0[y*W+(x-1)] = val;
300      Q.push([x-1,y]);
301    }
302    if (x < W-1 && im0[y*W+(x+1)] == val0){
303      im0[y*W+(x+1)] = val;
304      Q.push([x+1,y]);
305    }
306    if (y > 0 && im0[(y-1)*W+x] == val0){
307      im0[(y-1)*W+x] = val;
308      Q.push([x,y-1]);
309    }
310    if (y < H-1 && im0[(y+1)*W+x]== val0){
311      im0[(y+1)*W+x] = val;
312      Q.push([x,y+1]);
313    }
314  }
315}
316
317function floodblob(im0,x0,y0,val){
318  let Q = [];
319  let im1 = new Array(im0.length).fill(0);
320  Q.push([x0,y0]);
321  im1[y0*W+x0] = -1;
322  while (Q.length){
323    let [x,y] = Q.pop();
324    if (x > 0 && im0[y*W+(x-1)]==val && im1[y*W+(x-1)] == 0){
325      im1[y*W+(x-1)] = -1;
326      Q.push([x-1,y]);
327    }
328    if (x < W-1 && im0[y*W+(x+1)] == val && im1[y*W+(x+1)] ==0){
329      im1[y*W+(x+1)] = -1;
330      Q.push([x+1,y]);
331    }
332    if (y > 0 && im0[(y-1)*W+x] == val && im1[(y-1)*W+x]==0){
333      im1[(y-1)*W+x] = -1;
334      Q.push([x,y-1]);
335    }
336    if (y < H-1 && im0[(y+1)*W+x]== val && im1[(y+1)*W+x]==0){
337      im1[(y+1)*W+x] = -1;
338      Q.push([x,y+1]);
339    }
340  }
341  let needwork = false;
342  for (let i = 0; i < H; i++){
343    for (let j = 0; j < W; j++){
344      if (im1[i*W+j] == 0){
345        if (im0[i*W+j] == val){
346          im1[i*W+j] = -2;
347          needwork = true;
348        // }else if (im0[i*W+j] != 0 && !is_nc(im0[i*W+j])){
349        }else if (im0[i*W+j] != 0){
350          im1[i*W+j] = 255;
351        }
352      }
353    }
354  }
355  if (!needwork) return null;
356  let dt0 = dist_transform(im1.map(x=>x==255),W,H);
357
358  for (let i = 0; i < W*H; i++){
359    if (dt0[i] < CLR_W+TRC_R && im1[i] >= 0){
360      im1[i] = 255;
361    }
362    // if (im0[i] && is_nc(im0[i])){
363    //   im1[i] = 255;
364    // }
365  }
366  
367  // let dt1 = dist_transform(B0.map(x=>x&&(x!=val)),W,H);
368  
369  let dt = dist_transform(im1.map(x=>x==-2),W,H);
370  for (let i = 0; i < W*H; i++){
371    if (im1[i] == 0){
372      im1[i] = Math.min(dt[i]*0.5,254);
373    }
374  }
375  
376  let rc0 = rand();
377  
378  if (rc0 < 0.33){
379    for (let i = 0; i < W*H; i++){
380      if (im1[i] >= 0 && im1[i] < 255){
381              // im1[i] = im1[i]*(255-dt0[i])/255;
382        im1[i] = im1[i]+Math.max(1,250-dt0[i]*5)*0.5;
383        im1[i] = Math.min(im1[i],200);
384      }
385    }
386  }else if (rc0 < 0.67){
387    let dt00 = dist_transform(Bhist[0].map(x=>x&&(x!=val)&&!is_nc(x)),W,H);
388    for (let i = 0; i < W*H; i++){
389      if (im1[i] >= 0 && im1[i] < 255){
390              // im1[i] = im1[i]*(255-dt0[i])/255;
391        im1[i] = im1[i]+Math.max(1,250-dt00[i]*10)*0.3;
392        im1[i] = Math.min(im1[i],200);
393      }
394    }
395  }
396  
397  let rc = rand();
398  let amt = 100+rand()*100;
399
400  for (let i = 0; i < H; i++){
401    for (let j = 0; j < W; j++){
402      if (im1[i*W+j] > 0 && im1[i*W+j] < 255){
403
404        if (rc < 0.2){
405          im1[i*W+j] = im1[i*W+j] + (j/W-0.5)*amt;
406        }else if (rc < 0.4){
407          im1[i*W+j] = im1[i*W+j] + ((W-j)/W-0.5)*amt;
408        }else if (rc < 0.6){
409          im1[i*W+j] = im1[i*W+j] + (i/H-0.5)*amt;
410        }else if (rc < 0.8){
411          im1[i*W+j] = im1[i*W+j] + ((H-i)/H-0.5)*amt;
412        }
413
414        im1[i*W+j] = Math.max(1,Math.min(im1[i*W+j],254));
415      }
416    }
417  }
418
419
420  if (bShowDt){
421    let cnv = document.createElement("canvas");
422    cnv.width = W;
423    cnv.height = H;
424    let ctx = cnv.getContext('2d');
425    let imd = ctx.getImageData(0,0,W,H);
426    for (let i = 0; i < im1.length; i ++){
427      if (im1[i] == -1){
428        imd.data[i*4  ] = 255;
429        imd.data[i*4+1] = 0;
430        imd.data[i*4+2] = 0;
431      }else if (im1[i] == -2){
432        imd.data[i*4  ] = 0;
433        imd.data[i*4+1] = 255;
434        imd.data[i*4+2] = 0;
435      }else{
436        imd.data[i*4  ] = im1[i];
437        imd.data[i*4+1] = im1[i];
438        imd.data[i*4+2] = im1[i];
439      }
440      imd.data[i*4+3] = 255;
441    }
442    ctx.putImageData(imd,0,0);
443    dtdiv.appendChild(cnv);
444  }
445  
446  return im1;
447}
448
449let stamp;
450
451function update_trc_r(){
452  let TRC_W = TRC_R*2+1;
453  stamp = new Array(TRC_W*TRC_W).fill(0);
454  for (let i = 0; i < TRC_W; i++){
455    for (let j = 0; j < TRC_W; j++){
456      let d = Math.hypot(i-TRC_R, j-TRC_R);
457      if (d < TRC_R+0.5){
458        stamp[i*TRC_W+j] = 1;
459      }
460    }
461  }
462}
463
464
465function shuffle(array) {
466  for (let i = array.length - 1; i > 0; i--) {
467    const j = Math.floor(rand() * (i + 1));
468    [array[i], array[j]] = [array[j], array[i]];
469  }
470}
471
472
473function is_nc(val){
474  if (val == 0) return false;
475  let r = (val >> 16)&0xff;
476  let g = (val >> 8)&0xff;
477  let b = (val)&0xff;
478  return r == g && g == b;
479}
480
481function do_iter(B,xhint,yhint){
482  let CS = [];
483  let seen = new Array(B.length).fill(0);
484  let i = yhint*W+xhint;
485  for (let r = 0; r < 10000; r++){
486    if (r){
487      i = ~~(rand()*B.length);
488    }
489    // i = 130*W+198;
490    if (seen[i]) continue;
491    let val = B[i];
492    if (val && !is_nc(val)){
493      
494      let x = i%W;
495      let y = ~~(i/W);
496      let C = floodblob(B,x,y,val);
497
498      if (C){
499        for (let j = 0; j < C.length; j++){
500          if (C[j] == -1){
501            seen[j] = 1;
502          }
503        }
504        CS.push([C,x,y]);
505        break;
506      }
507    }
508  }
509  if (!CS.length){
510    console.log("all good!")
511    return -1;
512  }
513  shuffle(CS);
514  let [C,x,y] = CS[0];
515  let val = B[y*W+x];
516  let css = '#'+val.toString(16).padStart(6,'0');
517  console.log(`%c solving for ${css} at ${x},${y}`,'background: '+css+'; color: white');
518  
519  let path = astar({data:C,w:W,h:H},x,y);
520  if (!path){
521    console.log("no solution!");
522    return -2;
523  }
524  let B0 = Bhist[Bhist.length-1];
525  let TRC_W = TRC_R*2+1;
526  for (let j = 0; j < path.length; j++){
527    let [x,y] = path[j];
528    if (B0[y*W+x] == val){
529      continue;
530    }
531    for (let ki = 0; ki < TRC_W; ki++){
532      for (let kj = 0; kj < TRC_W; kj ++){
533        if (stamp[ki*TRC_W+kj]){
534          B[(y+ki-TRC_R)*W+(x+kj-TRC_R)] = val;
535        }
536      }
537    }
538  }
539  Bhist.push(B.slice());
540  display(B); 
541  return 0;
542}
543
544
545// function solve(){
546//   let Bx = B.slice();
547//   let ret;
548//   do{
549//     ret = do_iter(Bx);
550//   }while (ret == 0);
551  
552//   if (ret == -2){
553//     solve();
554//   }
555//   for (let i = 0; i < B.length; i++) B[i] = Bx[i];
556  
557// }
558
559
560var failure_reaction = function(){
561  let sel = document.getElementById("sel-fail");
562  if (sel.value == 'retry'){
563    return true;
564  }else if (sel.value == 'stop'){
565    throw "failed, stopping as specified.";
566  }else if (sel.value == 'ask'){
567    return confirm("failed. try again?");
568  }
569  throw "up";
570}
571
572function solve(){
573  dtdiv.innerHTML='';
574  
575  console.log("seed: 0x"+jsr.toString(16),"time: "+new Date());
576  Bhist = Bhist.slice(0,1);
577  let Bx = B.slice();
578  function iter(){
579    // console.log("...")
580    let ret = do_iter(Bx);
581    if (!ret){
582      return setTimeout(iter,1);
583    }
584    
585    if (ret == -2){
586      if (failure_reaction()){
587        return setTimeout(solve,1);
588      }
589    }
590    for (let i = 0; i < B.length; i++) B[i] = Bx[i];
591  }
592  iter();
593}
594
595
596
597
598
599
600
601cnv.addEventListener('mousedown',function(event){
602  let box = cnv.getBoundingClientRect();
603  mouseX = event.clientX-box.left;
604  mouseY = event.clientY-box.top;
605  mouseIsDown = true;
606  if (mouse_mode == 0){
607    do_iter(B,mouseX,mouseY)
608  }else if (mouse_mode == 1){
609    mouse_draw();
610  }else if (mouse_mode == 2){
611    curr_color = B[(~~mouseY)*W+(~~mouseX)];
612      
613    update_curr_color();
614  }else if (mouse_mode == 3){
615    
616    bucketfill(B,(~~mouseX),(~~mouseY),curr_color);
617    display(B);
618    Bhist.push(B.slice());
619  }
620})
621cnv.addEventListener('mousemove',function(event){
622  let box = cnv.getBoundingClientRect();
623  mouseX = event.clientX-box.left;
624  mouseY = event.clientY-box.top;
625  if (mouse_mode == 1){
626    if (mouseIsDown){
627      mouse_draw();
628    }
629  }
630})
631cnv.addEventListener('mouseup',function(event){
632  mouseIsDown = false;
633  if (mouse_mode == 1){
634    display(B);
635    Bhist.push(B.slice());
636  }
637})
638function mouse_draw(){
639  let x = ~~(mouseX);
640  let y = ~~(mouseY);
641  for (let i = Math.max(y-RAD,0); i < Math.min(y+RAD+1,H-1); i++){
642    for (let j = Math.max(x-RAD,0); j < Math.min(x+RAD+1,W-1); j++){
643      B[i*W+j] = curr_color;
644    }
645  }
646  ctx.fillStyle = '#'+curr_color.toString(16).padStart(6,'0')
647  ctx.fillRect(x-RAD,y-RAD,(RAD*2+1),(RAD*2+1));
648}
649
650
651let examples = {
652  'hello.D11C.echo':{seed:0xc288bec7/*0xc0e13f7a*/,conf:{TRC_R:3,CLR_W:8},url:'/autoroute/glitch-assets/hello.D11C.echo.traces_exterior.png'},
653  'hello.ftdi.45':{seed:0x88b213fd/*0xaa69c87d*/,conf:{TRC_R:3,CLR_W:8},url:'/autoroute/glitch-assets/hello.ftdi.45.traces_exterior.png'},
654  'hello.ISP.44':{seed:0xd9f6d431/*0xa38dd442*/,conf:{TRC_R:3,CLR_W:8},url:'/autoroute/glitch-assets/hello.ISP.44.traces_exterior.png'},
655  'hello.ILI9341.SW.D21E':{seed:0xed12d2c6,conf:{TRC_R:3,CLR_W:8},url:'/autoroute/glitch-assets/hello.ILI9341.SW.D21E.traces_exterior.png'},
656}
657
658function load_example(k){
659  console.log(k);
660  curr_example = k;
661  [B,W,H] = load_png(examples[k].url);
662  cnv.width = W;
663  cnv.height = H;
664  Bhist = [B.slice()];
665  jsr = examples[k].seed;
666  for (let q in examples[k].conf){
667    window[q] = examples[k].conf[q];
668  }
669  update_trc_r();
670  display(B);
671  make_gui();
672}
673
674let curr_example = 'hello.D11C.echo';
675
676load_example(curr_example);
677
678function update_curr_color(){
679  let sp = document.getElementById('span-curr-color');
680  let s = '#'+curr_color.toString(16).padStart(6,'0');
681  sp.innerHTML = s;
682  sp.style.background=s;
683}
684
685function upload_user_png(){
686  var fileToLoad = document.getElementById("file_upl").files[0];
687  var fileReader = new FileReader();
688  fileReader.onload = function(fileLoadedEvent){
689    let ab = fileLoadedEvent.target.result;
690    
691    let bytes = new Uint8Array(ab);
692    console.log(bytes);
693    let [data,w,h] = read_png(bytes);
694    let D = [];
695    for (let i = 0; i < data.length; i+= 4){
696      let r = data[i];
697      let g = data[i+1];
698      let b = data[i+2];
699      let c = (r << 16) | (g << 8) | b;
700      D.push(c);
701    }
702    B = D;
703    W = w;
704    H = h;
705    cnv.width = W;
706    cnv.height = H;
707    display(B);
708    Bhist = [B.slice()];
709  };
710  fileReader.readAsArrayBuffer(fileToLoad);
711}
712
713let img_msg = `\
714blobs of the same color will be connected (e.g. red->red, blue->blue), except:
715black (#000) is the empty space.
716white and grays are NC (avoided by other tracks, but not connected to any).
717make sure there is no anti-aliasing!
718png only!
719`;
720
721
722function make_gui(){
723  let div= document.getElementById('gui');
724  if (!div) div = document.createElement('div');
725  div.id = 'gui';
726  div.style="font-family:monospace; position:absolute;width:320px;height:720px;top:0px;right:0px;border:1px dotted black;background:whitesmoke;padding:10px;white-space:pre"
727  let htm = `load example: <select id="sel-eg" onchange="load_example(this.value)">
728  `;
729  for (let k in examples){
730    htm += `<option value="${k}" ${curr_example==k?'selected':''}>${k}</option>`;    
731  } 
732  htm += `</select><button onclick="load_example(document.getElementById('sel-eg').value)">⟳</button>
733or upload png: 
734<input type="file" id="file_upl" onchange="upload_user_png()"></input>\
735<button onclick="alert(img_msg)">?</button>
736seed: <input id="inp-seed" value="${jsr}"></input>\
737<button onclick="jsr=Number(document.getElementById('inp-seed').value)">set</button>\
738<button onclick="jsr=(~~(Math.random()*4294967295))>>>0;document.getElementById('inp-seed').value=jsr">gen</button>
739track radius (px, " width-1/2): <input type="number" value="${TRC_R}" min="1" max="99" oninput="TRC_R=Number(this.value)"/>
740clearance    (px):              <input type="number" value="${CLR_W}" min="1" max="99" oninput="CLR_W=Number(this.value)"/>
741upon failure: <select id="sel-fail">
742  <option>retry</option>
743  <option>stop</option>
744  <option>ask</option>
745</select>
746<input type="checkbox" ${bShowDt?'checked':''} onchange="bShowDt=this.checked"/>dbg: show dist transforms
747<button onclick="solve()" style="font-size:32px">solve</button>&lt;--click me
748<button onclick="if (Bhist.length>1){Bhist.pop();B=Bhist[Bhist.length-1].slice();display(B)}">undo</button>\
749<button onclick="B.fill(0);Bhist=[B.slice()];display(B)">clear</button>
750mouse function: <select onchange="mouse_mode=this.selectedIndex">
751  <option>route</option>
752  <option>paint</option>
753  <option>colorpicker</option>
754  <option>bucketfill</option>
755</select>
756current color: <span id="span-curr-color"></span>
757quick colors:
758<div style="white-space:normal">
759`
760  for (let i = 0; i < 64; i++){
761    
762    let r = (((i >> 4)&3) << 6) + 63;
763    let g = (((i >> 2)&3) << 6) + 63;
764    let b = ((i & 3) << 6     ) + 63;
765    let c = (r << 16) | (g << 8) | b;
766    let s = '#'+c.toString(16).padStart(6,'0');
767    htm += `<button onclick="curr_color=${c};update_curr_color()" style="background:${s};font-family:monospace;margin:0px;">${s}</button>`
768  }
769  htm += `<button onclick="curr_color=0;update_curr_color()" style="background:black;font-family:monospace;margin:0px;color:white">#000000</button>`
770htm += `
771</div>
772open console to see progress details.
773
774<a href="https://glitch.com/edit/#!/autoroute">source code</a>
775  `
776  div.innerHTML = htm;
777  document.body.appendChild(div);
778  update_curr_color();
779}
780
781
782make_gui();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.