scan-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1823 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/Scanner.ts
6
+ var OverscanState = /* @__PURE__ */ ((OverscanState2) => {
7
+ OverscanState2["FAST"] = "fast";
8
+ OverscanState2["SLOW_BACKWARD"] = "slow_backward";
9
+ return OverscanState2;
10
+ })(OverscanState || {});
11
+ var Scanner = class {
12
+ constructor(surface, config, callbacks = {}) {
13
+ __publicField(this, "surface");
14
+ __publicField(this, "config");
15
+ __publicField(this, "callbacks");
16
+ __publicField(this, "isRunning", false);
17
+ __publicField(this, "timer", null);
18
+ __publicField(this, "stepCount", 0);
19
+ __publicField(this, "overscanState", "fast" /* FAST */);
20
+ __publicField(this, "loopCount", 0);
21
+ this.surface = surface;
22
+ this.config = config;
23
+ this.callbacks = callbacks;
24
+ }
25
+ start() {
26
+ this.isRunning = true;
27
+ this.stepCount = 0;
28
+ this.loopCount = 0;
29
+ this.overscanState = "fast" /* FAST */;
30
+ this.reset();
31
+ this.scheduleNextStep();
32
+ }
33
+ stop() {
34
+ this.isRunning = false;
35
+ if (this.timer) {
36
+ clearTimeout(this.timer);
37
+ this.timer = null;
38
+ }
39
+ this.surface.setFocus([]);
40
+ }
41
+ handleAction(action) {
42
+ if (action === "select") {
43
+ this.handleSelectAction();
44
+ } else if (action === "step") {
45
+ if (this.config.get().scanInputMode === "manual") {
46
+ this.step();
47
+ this.stepCount++;
48
+ this.callbacks.onScanStep?.();
49
+ }
50
+ } else if (action === "reset") {
51
+ this.loopCount = 0;
52
+ this.reset();
53
+ this.stepCount = 0;
54
+ this.overscanState = "fast" /* FAST */;
55
+ if (this.config.get().scanInputMode === "auto") {
56
+ this.isRunning = true;
57
+ if (this.timer) clearTimeout(this.timer);
58
+ this.scheduleNextStep();
59
+ }
60
+ }
61
+ }
62
+ handleSelectAction() {
63
+ const config = this.config.get();
64
+ if (config.criticalOverscan.enabled) {
65
+ if (this.overscanState === "fast" /* FAST */) {
66
+ this.overscanState = "slow_backward" /* SLOW_BACKWARD */;
67
+ if (this.timer) clearTimeout(this.timer);
68
+ this.scheduleNextStep();
69
+ return;
70
+ } else if (this.overscanState === "slow_backward" /* SLOW_BACKWARD */) {
71
+ this.overscanState = "fast" /* FAST */;
72
+ this.doSelection();
73
+ return;
74
+ }
75
+ }
76
+ this.doSelection();
77
+ }
78
+ reportCycleCompleted() {
79
+ this.loopCount++;
80
+ const config = this.config.get();
81
+ if (config.scanLoops > 0 && this.loopCount >= config.scanLoops) {
82
+ this.stop();
83
+ this.loopCount = 0;
84
+ }
85
+ }
86
+ scheduleNextStep() {
87
+ if (!this.isRunning) return;
88
+ const config = this.config.get();
89
+ if (config.scanInputMode === "manual") {
90
+ return;
91
+ }
92
+ let rate;
93
+ if (config.criticalOverscan.enabled && this.overscanState === "slow_backward" /* SLOW_BACKWARD */) {
94
+ rate = config.criticalOverscan.slowRate;
95
+ } else {
96
+ const isFirstItem = this.stepCount === 0;
97
+ rate = isFirstItem && config.initialItemPause > 0 ? config.initialItemPause : config.criticalOverscan.enabled ? config.criticalOverscan.fastRate : config.scanRate;
98
+ }
99
+ if (this.timer) clearTimeout(this.timer);
100
+ this.timer = window.setTimeout(() => {
101
+ this.step();
102
+ this.callbacks.onScanStep?.();
103
+ this.stepCount++;
104
+ this.scheduleNextStep();
105
+ }, rate);
106
+ }
107
+ triggerSelection(index) {
108
+ const item = this.surface.getItemData?.(index);
109
+ if (item?.isEmpty) {
110
+ this.stepCount = 0;
111
+ if (this.timer) clearTimeout(this.timer);
112
+ this.scheduleNextStep();
113
+ return;
114
+ }
115
+ this.surface.setSelected(index);
116
+ this.callbacks.onSelect?.(index);
117
+ }
118
+ triggerRedraw() {
119
+ this.callbacks.onRedraw?.();
120
+ }
121
+ mapContentToGrid(content, _rows, _cols) {
122
+ return content;
123
+ }
124
+ };
125
+
126
+ // src/scanners/RowColumnScanner.ts
127
+ var RowColumnScanner = class extends Scanner {
128
+ constructor() {
129
+ super(...arguments);
130
+ __publicField(this, "level", "rows");
131
+ __publicField(this, "currentRow", -1);
132
+ __publicField(this, "currentCol", -1);
133
+ __publicField(this, "totalRows", 0);
134
+ __publicField(this, "isColumnRow", false);
135
+ __publicField(this, "useBlockScanning", true);
136
+ }
137
+ start() {
138
+ const config = this.config.get();
139
+ this.isColumnRow = config.scanPattern === "column-row";
140
+ this.useBlockScanning = config.scanTechnique === "block";
141
+ this.recalcDimensions();
142
+ super.start();
143
+ }
144
+ recalcDimensions() {
145
+ const totalItems = this.surface.getItemsCount();
146
+ if (this.isColumnRow) {
147
+ this.totalRows = this.surface.getColumns();
148
+ } else {
149
+ this.totalRows = Math.ceil(totalItems / this.surface.getColumns());
150
+ }
151
+ }
152
+ reset() {
153
+ this.level = this.useBlockScanning ? "rows" : "cells";
154
+ this.currentRow = -1;
155
+ this.currentCol = -1;
156
+ this.surface.setFocus([]);
157
+ }
158
+ step() {
159
+ if (this.useBlockScanning && this.level === "rows") {
160
+ this.stepMajor();
161
+ } else {
162
+ this.stepMinor();
163
+ }
164
+ }
165
+ stepMajor() {
166
+ this.currentRow++;
167
+ if (this.currentRow >= this.totalRows) {
168
+ this.currentRow = 0;
169
+ }
170
+ this.highlightMajor(this.currentRow);
171
+ }
172
+ stepMinor() {
173
+ let maxMinor = 0;
174
+ const totalItems = this.surface.getItemsCount();
175
+ const cols = this.surface.getColumns();
176
+ const rows = Math.ceil(totalItems / cols);
177
+ if (this.useBlockScanning) {
178
+ if (this.isColumnRow) {
179
+ const colIdx = this.currentRow;
180
+ const lastRowLength = totalItems % cols || cols;
181
+ if (colIdx < lastRowLength) {
182
+ maxMinor = rows;
183
+ } else {
184
+ maxMinor = rows - 1;
185
+ }
186
+ } else {
187
+ const rowStart = this.currentRow * cols;
188
+ maxMinor = Math.min(cols, totalItems - rowStart);
189
+ }
190
+ this.currentCol++;
191
+ if (this.currentCol >= maxMinor) {
192
+ this.currentCol = 0;
193
+ }
194
+ } else {
195
+ this.currentCol++;
196
+ if (this.currentCol >= totalItems) {
197
+ this.currentCol = 0;
198
+ }
199
+ }
200
+ let index = -1;
201
+ if (this.useBlockScanning) {
202
+ if (this.isColumnRow) {
203
+ index = this.currentCol * cols + this.currentRow;
204
+ } else {
205
+ index = this.currentRow * cols + this.currentCol;
206
+ }
207
+ } else {
208
+ if (this.isColumnRow) {
209
+ const row = Math.floor(this.currentCol / cols);
210
+ const col = this.currentCol % cols;
211
+ index = row * cols + col;
212
+ } else {
213
+ index = this.currentCol;
214
+ }
215
+ }
216
+ if (index >= 0 && index < totalItems) {
217
+ const cfg = this.config.get();
218
+ this.surface.setFocus([index], {
219
+ phase: this.useBlockScanning ? "minor" : "item",
220
+ scanRate: cfg.scanRate,
221
+ scanPattern: cfg.scanPattern,
222
+ scanTechnique: cfg.scanTechnique,
223
+ scanDirection: cfg.scanDirection
224
+ });
225
+ }
226
+ }
227
+ highlightMajor(majorIndex) {
228
+ const cols = this.surface.getColumns();
229
+ const totalItems = this.surface.getItemsCount();
230
+ const indices = [];
231
+ if (this.isColumnRow) {
232
+ const rows = Math.ceil(totalItems / cols);
233
+ for (let r = 0; r < rows; r++) {
234
+ const idx = r * cols + majorIndex;
235
+ if (idx < totalItems) indices.push(idx);
236
+ }
237
+ } else {
238
+ const start = majorIndex * cols;
239
+ const end = Math.min(start + cols, totalItems);
240
+ for (let i = start; i < end; i++) indices.push(i);
241
+ }
242
+ const cfg = this.config.get();
243
+ this.surface.setFocus(indices, {
244
+ phase: "major",
245
+ scanRate: cfg.scanRate,
246
+ scanPattern: cfg.scanPattern,
247
+ scanTechnique: cfg.scanTechnique,
248
+ scanDirection: cfg.scanDirection
249
+ });
250
+ }
251
+ handleAction(action) {
252
+ if (action === "cancel") {
253
+ if (this.useBlockScanning && this.level === "cells") {
254
+ this.level = "rows";
255
+ this.currentCol = -1;
256
+ this.highlightMajor(this.currentRow);
257
+ if (this.timer) clearTimeout(this.timer);
258
+ this.scheduleNextStep();
259
+ } else {
260
+ this.reset();
261
+ }
262
+ } else {
263
+ super.handleAction(action);
264
+ }
265
+ }
266
+ doSelection() {
267
+ if (this.useBlockScanning && this.level === "rows") {
268
+ if (this.currentRow >= 0) {
269
+ this.level = "cells";
270
+ this.currentCol = -1;
271
+ this.surface.setSelected(-1);
272
+ if (this.timer) clearTimeout(this.timer);
273
+ this.scheduleNextStep();
274
+ }
275
+ } else {
276
+ let index = -1;
277
+ const cols = this.surface.getColumns();
278
+ if (this.useBlockScanning) {
279
+ if (this.isColumnRow) {
280
+ index = this.currentCol * cols + this.currentRow;
281
+ } else {
282
+ index = this.currentRow * cols + this.currentCol;
283
+ }
284
+ } else {
285
+ if (this.isColumnRow) {
286
+ const row = Math.floor(this.currentCol / cols);
287
+ const col = this.currentCol % cols;
288
+ index = row * cols + col;
289
+ } else {
290
+ index = this.currentCol;
291
+ }
292
+ }
293
+ if (index >= 0) {
294
+ this.triggerSelection(index);
295
+ this.reset();
296
+ if (this.timer) clearTimeout(this.timer);
297
+ this.scheduleNextStep();
298
+ }
299
+ }
300
+ }
301
+ getCost(itemIndex) {
302
+ const cols = this.surface.getColumns();
303
+ const row = Math.floor(itemIndex / cols);
304
+ const col = itemIndex % cols;
305
+ if (this.useBlockScanning) {
306
+ if (this.isColumnRow) {
307
+ return col + 1 + (row + 1);
308
+ }
309
+ return row + 1 + (col + 1);
310
+ } else {
311
+ if (this.isColumnRow) {
312
+ return col + 1 + row + 1;
313
+ }
314
+ return itemIndex + 1;
315
+ }
316
+ }
317
+ mapContentToGrid(content, rows, cols) {
318
+ if (this.config.get().scanPattern !== "column-row") {
319
+ return content;
320
+ }
321
+ const newContent = new Array(content.length);
322
+ let contentIdx = 0;
323
+ for (let c = 0; c < cols; c++) {
324
+ for (let r = 0; r < rows; r++) {
325
+ if (contentIdx >= content.length) break;
326
+ const gridIndex = r * cols + c;
327
+ if (gridIndex < newContent.length) {
328
+ newContent[gridIndex] = content[contentIdx++];
329
+ }
330
+ }
331
+ }
332
+ return newContent;
333
+ }
334
+ };
335
+
336
+ // src/scanners/LinearScanner.ts
337
+ var LinearScanner = class extends Scanner {
338
+ constructor() {
339
+ super(...arguments);
340
+ __publicField(this, "currentIndex", -1);
341
+ __publicField(this, "totalItems", 0);
342
+ __publicField(this, "direction", 1);
343
+ }
344
+ // 1 for forward, -1 for reverse
345
+ start() {
346
+ this.totalItems = this.countItems();
347
+ this.direction = 1;
348
+ super.start();
349
+ }
350
+ countItems() {
351
+ return this.surface.getItemsCount();
352
+ }
353
+ reset() {
354
+ this.currentIndex = -1;
355
+ this.direction = 1;
356
+ this.loopCount = 0;
357
+ this.surface.setFocus([]);
358
+ }
359
+ step() {
360
+ const config = this.config.get();
361
+ if (config.criticalOverscan.enabled && this.overscanState === "slow_backward") {
362
+ this.currentIndex--;
363
+ if (this.currentIndex < 0) {
364
+ this.currentIndex = this.totalItems - 1;
365
+ this.reportCycleCompleted();
366
+ }
367
+ const cfg2 = this.config.get();
368
+ this.surface.setFocus([this.currentIndex], {
369
+ phase: "item",
370
+ scanRate: cfg2.scanRate,
371
+ scanPattern: cfg2.scanPattern,
372
+ scanTechnique: cfg2.scanTechnique,
373
+ scanDirection: cfg2.scanDirection
374
+ });
375
+ return;
376
+ }
377
+ switch (config.scanDirection) {
378
+ case "circular":
379
+ this.currentIndex++;
380
+ if (this.currentIndex >= this.totalItems) {
381
+ this.currentIndex = 0;
382
+ this.reportCycleCompleted();
383
+ }
384
+ break;
385
+ case "reverse":
386
+ this.currentIndex--;
387
+ if (this.currentIndex < 0) {
388
+ this.currentIndex = this.totalItems - 1;
389
+ this.reportCycleCompleted();
390
+ }
391
+ break;
392
+ case "oscillating":
393
+ if (this.currentIndex >= this.totalItems - 1 && this.direction === 1) {
394
+ this.direction = -1;
395
+ this.reportCycleCompleted();
396
+ } else if (this.currentIndex <= 0 && this.direction === -1) {
397
+ this.direction = 1;
398
+ this.reportCycleCompleted();
399
+ }
400
+ this.currentIndex += this.direction;
401
+ break;
402
+ }
403
+ const cfg = this.config.get();
404
+ this.surface.setFocus([this.currentIndex], {
405
+ phase: "item",
406
+ scanRate: cfg.scanRate,
407
+ scanPattern: cfg.scanPattern,
408
+ scanTechnique: cfg.scanTechnique,
409
+ scanDirection: cfg.scanDirection
410
+ });
411
+ }
412
+ handleAction(action) {
413
+ if (action === "step") {
414
+ if (this.timer) clearTimeout(this.timer);
415
+ this.step();
416
+ this.callbacks.onScanStep?.();
417
+ this.scheduleNextStep();
418
+ } else {
419
+ super.handleAction(action);
420
+ }
421
+ }
422
+ doSelection() {
423
+ if (this.currentIndex >= 0) {
424
+ if (this.currentIndex >= 0) {
425
+ this.triggerSelection(this.currentIndex);
426
+ this.reset();
427
+ if (this.timer) clearTimeout(this.timer);
428
+ this.scheduleNextStep();
429
+ }
430
+ }
431
+ }
432
+ getCost(itemIndex) {
433
+ const config = this.config.get();
434
+ switch (config.scanDirection) {
435
+ case "circular":
436
+ return itemIndex + 1;
437
+ case "reverse":
438
+ return this.totalItems - itemIndex;
439
+ case "oscillating":
440
+ return itemIndex + 1;
441
+ default:
442
+ return itemIndex + 1;
443
+ }
444
+ }
445
+ };
446
+
447
+ // src/scanners/SnakeScanner.ts
448
+ var SnakeScanner = class extends Scanner {
449
+ constructor() {
450
+ super(...arguments);
451
+ __publicField(this, "currentRow", 0);
452
+ __publicField(this, "currentCol", 0);
453
+ __publicField(this, "direction", 1);
454
+ // 1 = Right, -1 = Left
455
+ __publicField(this, "maxRow", 0);
456
+ __publicField(this, "maxCol", 0);
457
+ }
458
+ start() {
459
+ this.updateDimensions();
460
+ super.start();
461
+ }
462
+ updateDimensions() {
463
+ const total = this.surface.getItemsCount();
464
+ this.maxCol = this.surface.getColumns();
465
+ this.maxRow = Math.ceil(total / this.maxCol);
466
+ }
467
+ reset() {
468
+ this.currentRow = 0;
469
+ this.currentCol = 0;
470
+ this.direction = 1;
471
+ const cfg = this.config.get();
472
+ this.surface.setFocus([0], {
473
+ phase: "item",
474
+ scanRate: cfg.scanRate,
475
+ scanPattern: cfg.scanPattern,
476
+ scanTechnique: cfg.scanTechnique,
477
+ scanDirection: cfg.scanDirection
478
+ });
479
+ }
480
+ step() {
481
+ this.currentCol += this.direction;
482
+ if (this.currentCol >= this.maxCol) {
483
+ this.currentCol = this.maxCol - 1;
484
+ this.currentRow++;
485
+ this.direction = -1;
486
+ } else if (this.currentCol < 0) {
487
+ this.currentCol = 0;
488
+ this.currentRow++;
489
+ this.direction = 1;
490
+ }
491
+ if (this.currentRow >= this.maxRow) {
492
+ this.currentRow = 0;
493
+ this.currentCol = 0;
494
+ this.direction = 1;
495
+ }
496
+ const index = this.currentRow * this.maxCol + this.currentCol;
497
+ if (index >= this.surface.getItemsCount()) {
498
+ this.reset();
499
+ return;
500
+ }
501
+ const cfg = this.config.get();
502
+ this.surface.setFocus([index], {
503
+ phase: "item",
504
+ scanRate: cfg.scanRate,
505
+ scanPattern: cfg.scanPattern,
506
+ scanTechnique: cfg.scanTechnique,
507
+ scanDirection: cfg.scanDirection
508
+ });
509
+ }
510
+ handleAction(action) {
511
+ if (action !== "select") {
512
+ super.handleAction(action);
513
+ } else {
514
+ super.handleAction(action);
515
+ }
516
+ }
517
+ doSelection() {
518
+ const index = this.currentRow * this.maxCol + this.currentCol;
519
+ if (index >= 0) {
520
+ this.triggerSelection(index);
521
+ this.reset();
522
+ if (this.timer) clearTimeout(this.timer);
523
+ this.scheduleNextStep();
524
+ }
525
+ }
526
+ getCost(itemIndex) {
527
+ return itemIndex;
528
+ }
529
+ mapContentToGrid(content, rows, cols) {
530
+ const newContent = new Array(content.length);
531
+ let r = 0, c = 0, dir = 1;
532
+ for (let i = 0; i < content.length; i++) {
533
+ const gridIndex = r * cols + c;
534
+ if (gridIndex < newContent.length) {
535
+ newContent[gridIndex] = content[i];
536
+ }
537
+ c += dir;
538
+ if (c >= cols) {
539
+ c = cols - 1;
540
+ r++;
541
+ dir = -1;
542
+ } else if (c < 0) {
543
+ c = 0;
544
+ r++;
545
+ dir = 1;
546
+ }
547
+ if (r >= rows) break;
548
+ }
549
+ return newContent;
550
+ }
551
+ };
552
+
553
+ // src/scanners/QuadrantScanner.ts
554
+ var QuadrantScanner = class extends Scanner {
555
+ constructor() {
556
+ super(...arguments);
557
+ __publicField(this, "level", "quadrant");
558
+ __publicField(this, "currentQuad", -1);
559
+ // 0..3
560
+ __publicField(this, "currentRow", -1);
561
+ // relative to quad
562
+ __publicField(this, "currentCol", -1);
563
+ // relative to row
564
+ // Quadrant bounds (calculated at start)
565
+ __publicField(this, "quads", []);
566
+ }
567
+ start() {
568
+ this.calcQuadrants();
569
+ super.start();
570
+ }
571
+ calcQuadrants() {
572
+ const totalItems = this.surface.getItemsCount();
573
+ const cols = this.surface.getColumns();
574
+ const totalRows = Math.ceil(totalItems / cols);
575
+ const midRow = Math.ceil(totalRows / 2);
576
+ const midCol = Math.ceil(cols / 2);
577
+ this.quads = [
578
+ { rowStart: 0, rowEnd: midRow, colStart: 0, colEnd: midCol },
579
+ // TL
580
+ { rowStart: 0, rowEnd: midRow, colStart: midCol, colEnd: cols },
581
+ // TR
582
+ { rowStart: midRow, rowEnd: totalRows, colStart: 0, colEnd: midCol },
583
+ // BL
584
+ { rowStart: midRow, rowEnd: totalRows, colStart: midCol, colEnd: cols }
585
+ // BR
586
+ ];
587
+ }
588
+ reset() {
589
+ this.level = "quadrant";
590
+ this.currentQuad = -1;
591
+ this.currentRow = -1;
592
+ this.currentCol = -1;
593
+ this.surface.setFocus([]);
594
+ }
595
+ step() {
596
+ if (this.level === "quadrant") {
597
+ this.stepQuadrant();
598
+ } else if (this.level === "row") {
599
+ this.stepRow();
600
+ } else {
601
+ this.stepCell();
602
+ }
603
+ }
604
+ stepQuadrant() {
605
+ this.currentQuad++;
606
+ if (this.currentQuad >= 4) this.currentQuad = 0;
607
+ this.highlightQuad(this.currentQuad);
608
+ }
609
+ stepRow() {
610
+ const q = this.quads[this.currentQuad];
611
+ const rowsInQuad = q.rowEnd - q.rowStart;
612
+ this.currentRow++;
613
+ if (this.currentRow >= rowsInQuad) this.currentRow = 0;
614
+ const actualRow = q.rowStart + this.currentRow;
615
+ this.highlightRowSegment(actualRow, q.colStart, q.colEnd);
616
+ }
617
+ stepCell() {
618
+ const q = this.quads[this.currentQuad];
619
+ const colsInQuad = q.colEnd - q.colStart;
620
+ this.currentCol++;
621
+ if (this.currentCol >= colsInQuad) this.currentCol = 0;
622
+ const actualRow = q.rowStart + this.currentRow;
623
+ const actualCol = q.colStart + this.currentCol;
624
+ const index = actualRow * this.surface.getColumns() + actualCol;
625
+ if (index < this.surface.getItemsCount()) {
626
+ const cfg = this.config.get();
627
+ this.surface.setFocus([index], {
628
+ phase: "item",
629
+ scanRate: cfg.scanRate,
630
+ scanPattern: cfg.scanPattern,
631
+ scanTechnique: cfg.scanTechnique,
632
+ scanDirection: cfg.scanDirection
633
+ });
634
+ } else {
635
+ this.stepCell();
636
+ }
637
+ }
638
+ highlightQuad(qIndex) {
639
+ const q = this.quads[qIndex];
640
+ const indices = [];
641
+ const cols = this.surface.getColumns();
642
+ const totalItems = this.surface.getItemsCount();
643
+ for (let r = q.rowStart; r < q.rowEnd; r++) {
644
+ for (let c = q.colStart; c < q.colEnd; c++) {
645
+ const idx = r * cols + c;
646
+ if (idx < totalItems) indices.push(idx);
647
+ }
648
+ }
649
+ const cfg = this.config.get();
650
+ this.surface.setFocus(indices, {
651
+ phase: "major",
652
+ scanRate: cfg.scanRate,
653
+ scanPattern: cfg.scanPattern,
654
+ scanTechnique: cfg.scanTechnique,
655
+ scanDirection: cfg.scanDirection
656
+ });
657
+ }
658
+ highlightRowSegment(row, colStart, colEnd) {
659
+ const indices = [];
660
+ const cols = this.surface.getColumns();
661
+ const totalItems = this.surface.getItemsCount();
662
+ for (let c = colStart; c < colEnd; c++) {
663
+ const idx = row * cols + c;
664
+ if (idx < totalItems) indices.push(idx);
665
+ }
666
+ const cfg = this.config.get();
667
+ this.surface.setFocus(indices, {
668
+ phase: "minor",
669
+ scanRate: cfg.scanRate,
670
+ scanPattern: cfg.scanPattern,
671
+ scanTechnique: cfg.scanTechnique,
672
+ scanDirection: cfg.scanDirection
673
+ });
674
+ }
675
+ handleAction(action) {
676
+ if (action === "cancel") {
677
+ if (this.level === "cell") {
678
+ this.level = "row";
679
+ this.currentCol = -1;
680
+ this.restartTimer();
681
+ } else if (this.level === "row") {
682
+ this.level = "quadrant";
683
+ this.currentRow = -1;
684
+ this.restartTimer();
685
+ } else {
686
+ this.reset();
687
+ }
688
+ } else {
689
+ super.handleAction(action);
690
+ }
691
+ }
692
+ doSelection() {
693
+ if (this.level === "quadrant") {
694
+ if (this.currentQuad >= 0) {
695
+ this.level = "row";
696
+ this.currentRow = -1;
697
+ this.restartTimer();
698
+ }
699
+ } else if (this.level === "row") {
700
+ if (this.currentRow >= 0) {
701
+ this.level = "cell";
702
+ this.currentCol = -1;
703
+ this.restartTimer();
704
+ }
705
+ } else {
706
+ const q = this.quads[this.currentQuad];
707
+ const idx = (q.rowStart + this.currentRow) * this.surface.getColumns() + (q.colStart + this.currentCol);
708
+ if (idx >= 0) {
709
+ this.triggerSelection(idx);
710
+ this.reset();
711
+ this.restartTimer();
712
+ }
713
+ }
714
+ }
715
+ restartTimer() {
716
+ if (this.timer) clearTimeout(this.timer);
717
+ this.scheduleNextStep();
718
+ }
719
+ getCost(itemIndex) {
720
+ const cols = this.surface.getColumns();
721
+ const totalItems = this.surface.getItemsCount();
722
+ const totalRows = Math.ceil(totalItems / cols);
723
+ const midRow = Math.ceil(totalRows / 2);
724
+ const midCol = Math.ceil(cols / 2);
725
+ const row = Math.floor(itemIndex / cols);
726
+ const col = itemIndex % cols;
727
+ let quadIndex = 0;
728
+ if (row < midRow) {
729
+ if (col < midCol) quadIndex = 0;
730
+ else quadIndex = 1;
731
+ } else {
732
+ if (col < midCol) quadIndex = 2;
733
+ else quadIndex = 3;
734
+ }
735
+ let rowInQuad = row;
736
+ if (row >= midRow) rowInQuad = row - midRow;
737
+ let colInQuad = col;
738
+ if (col >= midCol) colInQuad = col - midCol;
739
+ return quadIndex + 1 + (rowInQuad + 1) + (colInQuad + 1);
740
+ }
741
+ mapContentToGrid(content, rows, cols) {
742
+ const newContent = new Array(content.length);
743
+ let contentIdx = 0;
744
+ const midRow = Math.ceil(rows / 2);
745
+ const midCol = Math.ceil(cols / 2);
746
+ const quads = [
747
+ { rS: 0, rE: midRow, cS: 0, cE: midCol },
748
+ // TL
749
+ { rS: 0, rE: midRow, cS: midCol, cE: cols },
750
+ // TR
751
+ { rS: midRow, rE: rows, cS: 0, cE: midCol },
752
+ // BL
753
+ { rS: midRow, rE: rows, cS: midCol, cE: cols }
754
+ // BR
755
+ ];
756
+ for (const q of quads) {
757
+ for (let r = q.rS; r < q.rE; r++) {
758
+ for (let c = q.cS; c < q.cE; c++) {
759
+ if (contentIdx >= content.length) break;
760
+ const gridIndex = r * cols + c;
761
+ if (gridIndex < newContent.length) {
762
+ newContent[gridIndex] = content[contentIdx++];
763
+ }
764
+ }
765
+ }
766
+ }
767
+ return newContent;
768
+ }
769
+ };
770
+
771
+ // src/scanners/GroupScanner.ts
772
+ var GroupScanner = class extends Scanner {
773
+ constructor() {
774
+ super(...arguments);
775
+ __publicField(this, "level", "group");
776
+ __publicField(this, "currentGroup", -1);
777
+ __publicField(this, "currentRow", -1);
778
+ // relative to group
779
+ __publicField(this, "currentCol", -1);
780
+ __publicField(this, "groups", []);
781
+ }
782
+ start() {
783
+ this.calcGroups();
784
+ super.start();
785
+ }
786
+ calcGroups() {
787
+ const totalItems = this.surface.getItemsCount();
788
+ const cols = this.surface.getColumns();
789
+ const totalRows = Math.ceil(totalItems / cols);
790
+ const groupSize = Math.ceil(totalRows / 3);
791
+ this.groups = [];
792
+ for (let i = 0; i < totalRows; i += groupSize) {
793
+ this.groups.push({
794
+ rowStart: i,
795
+ rowEnd: Math.min(i + groupSize, totalRows)
796
+ });
797
+ }
798
+ }
799
+ reset() {
800
+ this.level = "group";
801
+ this.currentGroup = -1;
802
+ this.currentRow = -1;
803
+ this.currentCol = -1;
804
+ this.surface.setFocus([]);
805
+ }
806
+ step() {
807
+ if (this.level === "group") this.stepGroup();
808
+ else if (this.level === "row") this.stepRow();
809
+ else this.stepCell();
810
+ }
811
+ stepGroup() {
812
+ this.currentGroup++;
813
+ if (this.currentGroup >= this.groups.length) this.currentGroup = 0;
814
+ this.highlightGroup(this.currentGroup);
815
+ }
816
+ stepRow() {
817
+ const g = this.groups[this.currentGroup];
818
+ const rowsInGroup = g.rowEnd - g.rowStart;
819
+ this.currentRow++;
820
+ if (this.currentRow >= rowsInGroup) this.currentRow = 0;
821
+ const actualRow = g.rowStart + this.currentRow;
822
+ this.highlightRow(actualRow);
823
+ }
824
+ stepCell() {
825
+ const cols = this.surface.getColumns();
826
+ this.currentCol++;
827
+ if (this.currentCol >= cols) this.currentCol = 0;
828
+ const g = this.groups[this.currentGroup];
829
+ const actualRow = g.rowStart + this.currentRow;
830
+ const idx = actualRow * cols + this.currentCol;
831
+ if (idx < this.surface.getItemsCount()) {
832
+ const cfg = this.config.get();
833
+ this.surface.setFocus([idx], {
834
+ phase: "item",
835
+ scanRate: cfg.scanRate,
836
+ scanPattern: cfg.scanPattern,
837
+ scanTechnique: cfg.scanTechnique,
838
+ scanDirection: cfg.scanDirection
839
+ });
840
+ } else {
841
+ if (this.currentCol < cols - 1) this.stepCell();
842
+ }
843
+ }
844
+ highlightGroup(gIndex) {
845
+ const g = this.groups[gIndex];
846
+ const indices = [];
847
+ const cols = this.surface.getColumns();
848
+ const totalItems = this.surface.getItemsCount();
849
+ for (let r = g.rowStart; r < g.rowEnd; r++) {
850
+ for (let c = 0; c < cols; c++) {
851
+ const idx = r * cols + c;
852
+ if (idx < totalItems) indices.push(idx);
853
+ }
854
+ }
855
+ const cfg = this.config.get();
856
+ this.surface.setFocus(indices, {
857
+ phase: "major",
858
+ scanRate: cfg.scanRate,
859
+ scanPattern: cfg.scanPattern,
860
+ scanTechnique: cfg.scanTechnique,
861
+ scanDirection: cfg.scanDirection
862
+ });
863
+ }
864
+ highlightRow(row) {
865
+ const cols = this.surface.getColumns();
866
+ const totalItems = this.surface.getItemsCount();
867
+ const indices = [];
868
+ for (let c = 0; c < cols; c++) {
869
+ const idx = row * cols + c;
870
+ if (idx < totalItems) indices.push(idx);
871
+ }
872
+ const cfg = this.config.get();
873
+ this.surface.setFocus(indices, {
874
+ phase: "minor",
875
+ scanRate: cfg.scanRate,
876
+ scanPattern: cfg.scanPattern,
877
+ scanTechnique: cfg.scanTechnique,
878
+ scanDirection: cfg.scanDirection
879
+ });
880
+ }
881
+ handleAction(action) {
882
+ if (action === "cancel") {
883
+ if (this.level === "cell") {
884
+ this.level = "row";
885
+ this.currentCol = -1;
886
+ this.restartTimer();
887
+ } else if (this.level === "row") {
888
+ this.level = "group";
889
+ this.currentRow = -1;
890
+ this.restartTimer();
891
+ } else {
892
+ this.reset();
893
+ }
894
+ } else {
895
+ super.handleAction(action);
896
+ }
897
+ }
898
+ doSelection() {
899
+ if (this.level === "group") {
900
+ if (this.currentGroup >= 0) {
901
+ this.level = "row";
902
+ this.currentRow = -1;
903
+ this.restartTimer();
904
+ }
905
+ } else if (this.level === "row") {
906
+ if (this.currentRow >= 0) {
907
+ this.level = "cell";
908
+ this.currentCol = -1;
909
+ this.restartTimer();
910
+ }
911
+ } else {
912
+ const g = this.groups[this.currentGroup];
913
+ const idx = (g.rowStart + this.currentRow) * this.surface.getColumns() + this.currentCol;
914
+ if (idx >= 0) {
915
+ this.triggerSelection(idx);
916
+ this.reset();
917
+ this.restartTimer();
918
+ }
919
+ }
920
+ }
921
+ restartTimer() {
922
+ if (this.timer) clearTimeout(this.timer);
923
+ this.scheduleNextStep();
924
+ }
925
+ getCost(itemIndex) {
926
+ const cols = this.surface.getColumns();
927
+ const totalItems = this.surface.getItemsCount();
928
+ const totalRows = Math.ceil(totalItems / cols);
929
+ const groupSize = Math.ceil(totalRows / 3);
930
+ const row = Math.floor(itemIndex / cols);
931
+ const col = itemIndex % cols;
932
+ const groupIndex = Math.floor(row / groupSize);
933
+ const rowIndexInGroup = row % groupSize;
934
+ return groupIndex + 1 + (rowIndexInGroup + 1) + (col + 1);
935
+ }
936
+ };
937
+
938
+ // src/scanners/EliminationScanner.ts
939
+ var SWITCH_COLORS = {
940
+ "switch-1": "#2196F3",
941
+ // Blue
942
+ "switch-2": "#F44336",
943
+ // Red
944
+ "switch-3": "#4CAF50",
945
+ // Green
946
+ "switch-4": "#FFEB3B",
947
+ // Yellow
948
+ "switch-5": "#9C27B0",
949
+ // Purple
950
+ "switch-6": "#FF9800",
951
+ // Orange
952
+ "switch-7": "#00BCD4",
953
+ // Cyan
954
+ "switch-8": "#E91E63"
955
+ // Magenta
956
+ };
957
+ var EliminationScanner = class extends Scanner {
958
+ constructor() {
959
+ super(...arguments);
960
+ __publicField(this, "rangeStart", 0);
961
+ __publicField(this, "rangeEnd", 0);
962
+ // Exclusive
963
+ __publicField(this, "currentBlock", 0);
964
+ // Which block is currently highlighted
965
+ __publicField(this, "numSwitches", 4);
966
+ // Default to 4-switch (quadrant)
967
+ __publicField(this, "partitionHistory", []);
968
+ }
969
+ start() {
970
+ const config = this.config.get();
971
+ this.numSwitches = config.eliminationSwitchCount || 4;
972
+ this.rangeStart = 0;
973
+ this.rangeEnd = this.surface.getItemsCount();
974
+ this.partitionHistory = [];
975
+ super.start();
976
+ }
977
+ reset() {
978
+ this.rangeStart = 0;
979
+ this.rangeEnd = this.surface.getItemsCount();
980
+ this.currentBlock = 0;
981
+ this.partitionHistory = [];
982
+ this.clearHighlights();
983
+ }
984
+ clearHighlights() {
985
+ this.surface.setFocus([]);
986
+ if (this.surface.clearItemStyles) {
987
+ this.surface.clearItemStyles();
988
+ }
989
+ }
990
+ step() {
991
+ this.currentBlock = (this.currentBlock + 1) % this.numSwitches;
992
+ this.highlightCurrentBlock();
993
+ }
994
+ highlightCurrentBlock() {
995
+ this.clearHighlights();
996
+ const partitions = this.calculatePartitions(this.rangeStart, this.rangeEnd, this.numSwitches);
997
+ const partition = partitions[this.currentBlock];
998
+ if (!partition) return;
999
+ for (let i = partition.start; i < partition.end; i++) {
1000
+ const switchAction = this.getSwitchAction(this.currentBlock);
1001
+ const color = SWITCH_COLORS[switchAction];
1002
+ this.surface.setItemStyle?.(i, {
1003
+ backgroundColor: color,
1004
+ opacity: 0.4,
1005
+ borderColor: color,
1006
+ borderWidth: 2,
1007
+ boxShadow: `inset 0 0 0 3px ${color}`
1008
+ });
1009
+ }
1010
+ }
1011
+ calculatePartitions(start, end, n) {
1012
+ const partitions = [];
1013
+ const total = end - start;
1014
+ const baseSize = Math.floor(total / n);
1015
+ const remainder = total % n;
1016
+ let currentStart = start;
1017
+ for (let i = 0; i < n; i++) {
1018
+ const size = baseSize + (i < remainder ? 1 : 0);
1019
+ partitions.push({ start: currentStart, end: currentStart + size });
1020
+ currentStart += size;
1021
+ }
1022
+ return partitions;
1023
+ }
1024
+ getSwitchAction(blockIndex) {
1025
+ const switchMap = {
1026
+ 0: "switch-1",
1027
+ 1: "switch-2",
1028
+ 2: "switch-3",
1029
+ 3: "switch-4",
1030
+ 4: "switch-5",
1031
+ 5: "switch-6",
1032
+ 6: "switch-7",
1033
+ 7: "switch-8"
1034
+ };
1035
+ return switchMap[blockIndex] || "switch-1";
1036
+ }
1037
+ handleAction(action) {
1038
+ if (action === "select") {
1039
+ this.doSelection();
1040
+ return;
1041
+ }
1042
+ if (action.toString().startsWith("switch-")) {
1043
+ const switchNum = parseInt(action.toString().split("-")[1]) - 1;
1044
+ if (switchNum >= this.numSwitches) {
1045
+ return;
1046
+ }
1047
+ const rangeSize = this.rangeEnd - this.rangeStart;
1048
+ if (rangeSize <= 1) {
1049
+ if (this.rangeStart >= 0) {
1050
+ this.triggerSelection(this.rangeStart);
1051
+ this.reset();
1052
+ this.restartTimer();
1053
+ }
1054
+ return;
1055
+ }
1056
+ if (switchNum === this.currentBlock) {
1057
+ const partitions = this.calculatePartitions(this.rangeStart, this.rangeEnd, this.numSwitches);
1058
+ const selectedBlock = partitions[switchNum];
1059
+ if (selectedBlock) {
1060
+ this.partitionHistory.push({ start: this.rangeStart, end: this.rangeEnd });
1061
+ this.rangeStart = selectedBlock.start;
1062
+ this.rangeEnd = selectedBlock.end;
1063
+ this.currentBlock = 0;
1064
+ if (this.rangeEnd - this.rangeStart === 1) {
1065
+ this.clearHighlights();
1066
+ const color = SWITCH_COLORS["switch-1"];
1067
+ this.surface.setItemStyle?.(this.rangeStart, {
1068
+ backgroundColor: color,
1069
+ opacity: 0.6,
1070
+ boxShadow: `inset 0 0 0 4px ${color}, 0 0 10px ${color}`,
1071
+ borderColor: color,
1072
+ borderWidth: 2
1073
+ });
1074
+ }
1075
+ }
1076
+ this.restartTimer();
1077
+ }
1078
+ return;
1079
+ }
1080
+ if (action === "cancel" || action === "reset") {
1081
+ if (this.partitionHistory.length > 0) {
1082
+ const previous = this.partitionHistory.pop();
1083
+ this.rangeStart = previous.start;
1084
+ this.rangeEnd = previous.end;
1085
+ this.currentBlock = 0;
1086
+ } else {
1087
+ this.reset();
1088
+ }
1089
+ this.restartTimer();
1090
+ }
1091
+ }
1092
+ doSelection() {
1093
+ const rangeSize = this.rangeEnd - this.rangeStart;
1094
+ if (rangeSize <= 1) {
1095
+ if (this.rangeStart >= 0) {
1096
+ this.triggerSelection(this.rangeStart);
1097
+ this.reset();
1098
+ this.restartTimer();
1099
+ }
1100
+ }
1101
+ }
1102
+ restartTimer() {
1103
+ if (this.timer) clearTimeout(this.timer);
1104
+ this.scheduleNextStep();
1105
+ }
1106
+ getCost(itemIndex) {
1107
+ const n = this.numSwitches;
1108
+ let start = 0;
1109
+ let end = this.surface.getItemsCount();
1110
+ let cost = 0;
1111
+ while (end - start > 1) {
1112
+ const partitions = this.calculatePartitions(start, end, n);
1113
+ let foundPartition = 0;
1114
+ for (let i = 0; i < partitions.length; i++) {
1115
+ if (itemIndex >= partitions[i].start && itemIndex < partitions[i].end) {
1116
+ foundPartition = i;
1117
+ break;
1118
+ }
1119
+ }
1120
+ cost += foundPartition + 1;
1121
+ start = partitions[foundPartition].start;
1122
+ end = partitions[foundPartition].end;
1123
+ }
1124
+ return cost;
1125
+ }
1126
+ // Override to disable step scheduling - elimination waits for specific switch input
1127
+ scheduleNextStep() {
1128
+ if (!this.isRunning) return;
1129
+ if (this.config.get().scanInputMode === "manual") {
1130
+ return;
1131
+ }
1132
+ if (this.timer) clearTimeout(this.timer);
1133
+ const rate = this.config.get().scanRate;
1134
+ this.timer = window.setTimeout(() => {
1135
+ this.step();
1136
+ this.scheduleNextStep();
1137
+ }, rate);
1138
+ }
1139
+ };
1140
+
1141
+ // src/scanners/ContinuousScanner.ts
1142
+ var ContinuousScanner = class extends Scanner {
1143
+ constructor() {
1144
+ super(...arguments);
1145
+ // States differ by technique:
1146
+ // Crosshair: 'x-scan' → 'y-scan' → 'processing' (waits for switch)
1147
+ // Gliding: 'x-scanning' → 'x-capturing' → 'y-scanning' → 'y-capturing' → 'processing' (continuous movement)
1148
+ // Eight-Direction: 'direction-scan' → 'moving' → 'processing' (compass direction selection)
1149
+ __publicField(this, "state", "x-scan");
1150
+ __publicField(this, "xPos", 0);
1151
+ // percentage 0-100
1152
+ __publicField(this, "yPos", 0);
1153
+ // percentage 0-100
1154
+ __publicField(this, "technique", "crosshair");
1155
+ __publicField(this, "numCols", 0);
1156
+ __publicField(this, "numRows", 0);
1157
+ // For gliding cursor
1158
+ __publicField(this, "bufferWidth", 15);
1159
+ // % of screen width for buffer zone
1160
+ __publicField(this, "direction", 1);
1161
+ // 1 = right/down, -1 = left/up
1162
+ __publicField(this, "pauseTimer", null);
1163
+ // For pause before reversing
1164
+ __publicField(this, "bufferLeft", 0);
1165
+ // Left edge of buffer zone (%)
1166
+ __publicField(this, "bufferRight", 0);
1167
+ // Right edge of buffer zone (%)
1168
+ __publicField(this, "bufferTop", 0);
1169
+ // Top edge of buffer zone (%)
1170
+ __publicField(this, "bufferBottom", 0);
1171
+ // Bottom edge of buffer zone (%)
1172
+ __publicField(this, "fineXPos", 0);
1173
+ // Fine line position within buffer zone (%)
1174
+ __publicField(this, "fineYPos", 0);
1175
+ // Fine line position within buffer zone (%)
1176
+ __publicField(this, "lockedXPosition", 0);
1177
+ // Actual X position when locked (%)
1178
+ // Eight-direction mode
1179
+ __publicField(this, "currentDirection", 0);
1180
+ // 0-7 for 8 directions (N, NE, E, SE, S, SW, W, NW)
1181
+ __publicField(this, "compassAngle", 0);
1182
+ // Continuous angle 0-359 for fluid rotation
1183
+ __publicField(this, "compassMode", "continuous");
1184
+ // Fluid vs discrete
1185
+ __publicField(this, "directionStepCounter", 0);
1186
+ // Counter for slowing direction cycling
1187
+ __publicField(this, "directionStepsPerChange", 10);
1188
+ // How many steps before changing direction (fixed-8 mode)
1189
+ __publicField(this, "directions", [
1190
+ { name: "N", angle: 0, dx: 0, dy: -1 },
1191
+ // North
1192
+ { name: "NE", angle: 45, dx: 1, dy: -1 },
1193
+ // Northeast
1194
+ { name: "E", angle: 90, dx: 1, dy: 0 },
1195
+ // East
1196
+ { name: "SE", angle: 135, dx: 1, dy: 1 },
1197
+ // Southeast
1198
+ { name: "S", angle: 180, dx: 0, dy: 1 },
1199
+ // South
1200
+ { name: "SW", angle: 225, dx: -1, dy: 1 },
1201
+ // Southwest
1202
+ { name: "W", angle: 270, dx: -1, dy: 0 },
1203
+ // West
1204
+ { name: "NW", angle: 315, dx: -1, dy: -1 }
1205
+ // Northwest
1206
+ ]);
1207
+ }
1208
+ start() {
1209
+ try {
1210
+ const config = this.config.get();
1211
+ this.technique = config.continuousTechnique || "crosshair";
1212
+ const totalItems = this.surface.getItemsCount();
1213
+ this.numCols = this.surface.getColumns();
1214
+ this.numRows = Math.ceil(totalItems / this.numCols);
1215
+ console.log("[ContinuousScanner] Starting:", {
1216
+ technique: this.technique,
1217
+ numCols: this.numCols,
1218
+ numRows: this.numRows,
1219
+ totalItems
1220
+ });
1221
+ if (this.technique === "gliding") {
1222
+ this.state = "x-scanning";
1223
+ this.xPos = 0;
1224
+ this.yPos = 0;
1225
+ } else if (this.technique === "eight-direction") {
1226
+ this.state = "direction-scan";
1227
+ this.xPos = 50;
1228
+ this.yPos = 50;
1229
+ this.compassMode = config.compassMode || "continuous";
1230
+ this.compassAngle = 0;
1231
+ } else {
1232
+ this.state = "x-scan";
1233
+ this.xPos = 0;
1234
+ this.yPos = 0;
1235
+ }
1236
+ console.log("[ContinuousScanner] Initial state:", this.state);
1237
+ this.emitUpdate();
1238
+ super.start();
1239
+ } catch (error) {
1240
+ console.error("[ContinuousScanner] ERROR in start():", error);
1241
+ throw error;
1242
+ }
1243
+ }
1244
+ stop() {
1245
+ super.stop();
1246
+ if (this.pauseTimer) {
1247
+ window.clearTimeout(this.pauseTimer);
1248
+ this.pauseTimer = null;
1249
+ }
1250
+ }
1251
+ reset() {
1252
+ if (this.technique === "gliding") {
1253
+ this.state = "x-scanning";
1254
+ this.xPos = 0;
1255
+ this.yPos = 0;
1256
+ } else if (this.technique === "eight-direction") {
1257
+ this.state = "direction-scan";
1258
+ this.xPos = 50;
1259
+ this.yPos = 50;
1260
+ } else {
1261
+ this.state = "x-scan";
1262
+ this.xPos = 0;
1263
+ this.yPos = 0;
1264
+ }
1265
+ this.direction = 1;
1266
+ this.fineXPos = 0;
1267
+ this.fineYPos = 0;
1268
+ this.bufferLeft = 0;
1269
+ this.bufferRight = this.bufferWidth;
1270
+ this.bufferTop = 0;
1271
+ this.bufferBottom = this.bufferWidth;
1272
+ this.lockedXPosition = 0;
1273
+ this.currentDirection = 0;
1274
+ this.compassAngle = 0;
1275
+ this.directionStepCounter = 0;
1276
+ this.surface.setFocus([]);
1277
+ this.emitUpdate();
1278
+ }
1279
+ step() {
1280
+ if (this.technique === "eight-direction") {
1281
+ if (this.state === "direction-scan") {
1282
+ if (this.compassMode === "continuous") {
1283
+ this.compassAngle = (this.compassAngle + 2) % 360;
1284
+ } else {
1285
+ this.directionStepCounter++;
1286
+ if (this.directionStepCounter >= this.directionStepsPerChange) {
1287
+ this.currentDirection = (this.currentDirection + 1) % 8;
1288
+ this.directionStepCounter = 0;
1289
+ }
1290
+ this.compassAngle = this.directions[this.currentDirection].angle;
1291
+ }
1292
+ } else if (this.state === "moving") {
1293
+ const dir = this.compassMode === "continuous" ? this.getDirectionFromAngle(this.compassAngle) : this.directions[this.currentDirection];
1294
+ const speed = 0.5;
1295
+ this.xPos += dir.dx * speed;
1296
+ this.yPos += dir.dy * speed;
1297
+ this.xPos = Math.max(0, Math.min(100, this.xPos));
1298
+ this.yPos = Math.max(0, Math.min(100, this.yPos));
1299
+ }
1300
+ } else if (this.technique === "gliding") {
1301
+ if (this.state === "x-scanning") {
1302
+ this.xPos += 0.8 * this.direction;
1303
+ if (this.xPos >= 100) {
1304
+ this.xPos = 100;
1305
+ if (!this.pauseTimer) {
1306
+ this.pauseTimer = window.setTimeout(() => {
1307
+ this.direction = -1;
1308
+ this.pauseTimer = null;
1309
+ }, 100);
1310
+ return;
1311
+ }
1312
+ } else if (this.xPos <= 0) {
1313
+ this.xPos = 0;
1314
+ if (!this.pauseTimer) {
1315
+ this.pauseTimer = window.setTimeout(() => {
1316
+ this.direction = 1;
1317
+ this.pauseTimer = null;
1318
+ }, 100);
1319
+ return;
1320
+ }
1321
+ }
1322
+ this.bufferLeft = Math.max(0, this.xPos - this.bufferWidth / 2);
1323
+ this.bufferRight = Math.min(100, this.xPos + this.bufferWidth / 2);
1324
+ } else if (this.state === "x-capturing") {
1325
+ this.fineXPos += 0.3 * this.direction;
1326
+ if (this.fineXPos >= 100) {
1327
+ this.fineXPos = 100;
1328
+ this.direction = -1;
1329
+ } else if (this.fineXPos <= 0) {
1330
+ this.fineXPos = 0;
1331
+ this.direction = 1;
1332
+ }
1333
+ } else if (this.state === "y-scanning") {
1334
+ this.yPos += 0.8 * this.direction;
1335
+ if (this.yPos >= 100) {
1336
+ this.yPos = 100;
1337
+ if (!this.pauseTimer) {
1338
+ this.pauseTimer = window.setTimeout(() => {
1339
+ this.direction = -1;
1340
+ this.pauseTimer = null;
1341
+ }, 100);
1342
+ return;
1343
+ }
1344
+ } else if (this.yPos <= 0) {
1345
+ this.yPos = 0;
1346
+ if (!this.pauseTimer) {
1347
+ this.pauseTimer = window.setTimeout(() => {
1348
+ this.direction = 1;
1349
+ this.pauseTimer = null;
1350
+ }, 100);
1351
+ return;
1352
+ }
1353
+ }
1354
+ this.bufferTop = Math.max(0, this.yPos - this.bufferWidth / 2);
1355
+ this.bufferBottom = Math.min(100, this.yPos + this.bufferWidth / 2);
1356
+ } else if (this.state === "y-capturing") {
1357
+ this.fineYPos += 0.3 * this.direction;
1358
+ if (this.fineYPos >= 100) {
1359
+ this.fineYPos = 100;
1360
+ this.direction = -1;
1361
+ } else if (this.fineYPos <= 0) {
1362
+ this.fineYPos = 0;
1363
+ this.direction = 1;
1364
+ }
1365
+ }
1366
+ } else {
1367
+ if (this.state === "x-scan") {
1368
+ this.xPos += 0.5;
1369
+ if (this.xPos > 100) this.xPos = 0;
1370
+ } else if (this.state === "y-scan") {
1371
+ this.yPos += 0.5;
1372
+ if (this.yPos > 100) this.yPos = 0;
1373
+ }
1374
+ }
1375
+ if (Math.floor(this.xPos * 2) % 50 === 0) {
1376
+ console.log("[ContinuousScanner] Step:", {
1377
+ state: this.state,
1378
+ xPos: this.xPos,
1379
+ yPos: this.yPos,
1380
+ fineXPos: this.fineXPos,
1381
+ fineYPos: this.fineYPos,
1382
+ bufferLeft: this.bufferLeft,
1383
+ bufferRight: this.bufferRight,
1384
+ technique: this.technique,
1385
+ direction: this.direction,
1386
+ currentDirection: this.currentDirection
1387
+ });
1388
+ }
1389
+ this.emitUpdate();
1390
+ }
1391
+ getDirectionFromAngle(angle) {
1392
+ const radians = angle * Math.PI / 180;
1393
+ const dx = Math.cos(radians);
1394
+ const dy = Math.sin(radians);
1395
+ const normalizedAngle = (angle + 22.5) % 360;
1396
+ const directionIndex = Math.floor(normalizedAngle / 45);
1397
+ const names = ["E", "SE", "S", "SW", "W", "NW", "N", "NE"];
1398
+ const name = names[directionIndex] || "N";
1399
+ return { dx, dy, name };
1400
+ }
1401
+ // Override scheduleNextStep for different refresh rates
1402
+ scheduleNextStep() {
1403
+ if (!this.isRunning) return;
1404
+ if (this.config.get().scanInputMode === "manual") {
1405
+ return;
1406
+ }
1407
+ if (this.timer) clearTimeout(this.timer);
1408
+ const delay = 20;
1409
+ this.timer = window.setTimeout(() => {
1410
+ this.step();
1411
+ this.scheduleNextStep();
1412
+ }, delay);
1413
+ }
1414
+ handleAction(action) {
1415
+ console.log("[ContinuousScanner] handleAction:", {
1416
+ action,
1417
+ state: this.state,
1418
+ technique: this.technique
1419
+ });
1420
+ if (action === "cancel") {
1421
+ console.log("[ContinuousScanner] Cancel - resetting");
1422
+ this.reset();
1423
+ } else {
1424
+ super.handleAction(action);
1425
+ }
1426
+ }
1427
+ doSelection() {
1428
+ if (this.technique === "eight-direction") {
1429
+ if (this.state === "direction-scan") {
1430
+ const dirInfo = this.getDirectionFromAngle(this.compassAngle);
1431
+ console.log("[ContinuousScanner] Transition: direction-scan -> moving, direction:", dirInfo.name, "angle:", this.compassAngle);
1432
+ this.state = "moving";
1433
+ } else if (this.state === "moving") {
1434
+ console.log("[ContinuousScanner] Transition: moving -> processing");
1435
+ this.state = "processing";
1436
+ this.selectFocusedItem();
1437
+ }
1438
+ } else if (this.technique === "gliding") {
1439
+ if (this.state === "x-scanning") {
1440
+ console.log("[ContinuousScanner] Transition: x-scanning -> x-capturing");
1441
+ this.state = "x-capturing";
1442
+ this.fineXPos = 0;
1443
+ this.direction = 1;
1444
+ } else if (this.state === "x-capturing") {
1445
+ console.log("[ContinuousScanner] Transition: x-capturing -> y-scanning");
1446
+ this.state = "y-scanning";
1447
+ this.lockedXPosition = this.bufferLeft + this.fineXPos / 100 * (this.bufferRight - this.bufferLeft);
1448
+ this.yPos = 0;
1449
+ this.fineYPos = 0;
1450
+ this.direction = 1;
1451
+ } else if (this.state === "y-scanning") {
1452
+ console.log("[ContinuousScanner] Transition: y-scanning -> y-capturing");
1453
+ this.state = "y-capturing";
1454
+ this.fineYPos = 0;
1455
+ this.direction = 1;
1456
+ } else if (this.state === "y-capturing") {
1457
+ console.log("[ContinuousScanner] Transition: y-capturing -> processing");
1458
+ this.state = "processing";
1459
+ this.selectFocusedItem();
1460
+ }
1461
+ } else {
1462
+ if (this.state === "x-scan") {
1463
+ console.log("[ContinuousScanner] Transition: x-scan -> y-scan");
1464
+ this.state = "y-scan";
1465
+ this.yPos = 0;
1466
+ } else if (this.state === "y-scan") {
1467
+ console.log("[ContinuousScanner] Transition: y-scan -> processing");
1468
+ this.state = "processing";
1469
+ this.selectAtPoint();
1470
+ }
1471
+ }
1472
+ }
1473
+ selectFocusedItem() {
1474
+ if (this.technique === "eight-direction") {
1475
+ this.selectAtPercent(this.xPos, this.yPos);
1476
+ return;
1477
+ }
1478
+ const actualYPos = this.bufferTop + this.fineYPos / 100 * (this.bufferBottom - this.bufferTop);
1479
+ this.selectAtPercent(this.lockedXPosition, actualYPos);
1480
+ }
1481
+ selectAtPoint() {
1482
+ this.selectAtPercent(this.xPos, this.yPos);
1483
+ }
1484
+ selectAtPercent(xPercent, yPercent) {
1485
+ let index = null;
1486
+ if (this.surface.resolveIndexAtPoint) {
1487
+ index = this.surface.resolveIndexAtPoint(xPercent, yPercent);
1488
+ } else {
1489
+ index = this.estimateIndexAtPercent(xPercent, yPercent);
1490
+ }
1491
+ if (index !== null && index >= 0) {
1492
+ this.triggerSelection(index);
1493
+ }
1494
+ this.reset();
1495
+ }
1496
+ estimateIndexAtPercent(xPercent, yPercent) {
1497
+ const totalItems = this.surface.getItemsCount();
1498
+ if (!totalItems) return null;
1499
+ const cols = Math.max(1, this.surface.getColumns());
1500
+ const rows = Math.max(1, Math.ceil(totalItems / cols));
1501
+ const col = Math.min(cols - 1, Math.max(0, Math.floor(xPercent / 100 * cols)));
1502
+ const row = Math.min(rows - 1, Math.max(0, Math.floor(yPercent / 100 * rows)));
1503
+ const index = row * cols + col;
1504
+ if (index >= totalItems) return totalItems - 1;
1505
+ return index;
1506
+ }
1507
+ emitUpdate() {
1508
+ const dirInfo = this.getDirectionFromAngle(this.compassAngle);
1509
+ const update = {
1510
+ technique: this.technique,
1511
+ state: this.state,
1512
+ xPos: this.xPos,
1513
+ yPos: this.yPos,
1514
+ bufferLeft: this.bufferLeft,
1515
+ bufferRight: this.bufferRight,
1516
+ bufferTop: this.bufferTop,
1517
+ bufferBottom: this.bufferBottom,
1518
+ fineXPos: this.fineXPos,
1519
+ fineYPos: this.fineYPos,
1520
+ lockedXPosition: this.lockedXPosition,
1521
+ compassAngle: this.compassAngle,
1522
+ currentDirection: this.currentDirection,
1523
+ directionName: dirInfo.name,
1524
+ directionDx: dirInfo.dx,
1525
+ directionDy: dirInfo.dy
1526
+ };
1527
+ this.callbacks.onContinuousUpdate?.(update);
1528
+ }
1529
+ getCost(itemIndex) {
1530
+ const cols = this.surface.getColumns();
1531
+ const row = Math.floor(itemIndex / cols);
1532
+ const col = itemIndex % cols;
1533
+ if (this.technique === "eight-direction") {
1534
+ const itemCenterX = (col + 0.5) / cols * 100;
1535
+ const itemCenterY = (row + 0.5) / Math.ceil(this.surface.getItemsCount() / cols) * 100;
1536
+ const distance = Math.sqrt(Math.pow(itemCenterX, 2) + Math.pow(itemCenterY, 2));
1537
+ return 4 + Math.round(distance / 0.5) + 1;
1538
+ } else if (this.technique === "gliding") {
1539
+ const itemCenter = (col + 0.5) / cols * 100;
1540
+ return Math.round(itemCenter / 0.5) + 1;
1541
+ } else {
1542
+ const xPosition = (col + 0.5) / cols * 100;
1543
+ const yPosition = (row + 0.5) / Math.ceil(this.surface.getItemsCount() / cols) * 100;
1544
+ const xSteps = Math.round(xPosition / 0.5);
1545
+ const ySteps = Math.round(yPosition / 0.5);
1546
+ return xSteps + ySteps + 2;
1547
+ }
1548
+ }
1549
+ };
1550
+
1551
+ // src/PredictorManager.ts
1552
+ import { createPredictor } from "@willwade/ppmpredictor";
1553
+ var PredictorManager = class {
1554
+ constructor() {
1555
+ __publicField(this, "predictor");
1556
+ this.predictor = createPredictor({
1557
+ adaptive: true,
1558
+ maxOrder: 5
1559
+ });
1560
+ this.predictor.train("The quick brown fox jumps over the lazy dog. Hello world. How are you?");
1561
+ }
1562
+ addToContext(char) {
1563
+ this.predictor.addToContext(char);
1564
+ }
1565
+ resetContext() {
1566
+ this.predictor.resetContext();
1567
+ }
1568
+ predictNext() {
1569
+ const preds = this.predictor.predictNextCharacter();
1570
+ return preds;
1571
+ }
1572
+ };
1573
+
1574
+ // src/scanners/ProbabilityScanner.ts
1575
+ var ProbabilityScanner = class extends Scanner {
1576
+ constructor() {
1577
+ super(...arguments);
1578
+ __publicField(this, "predictor", new PredictorManager());
1579
+ __publicField(this, "scanOrder", []);
1580
+ __publicField(this, "currentIndex", -1);
1581
+ }
1582
+ start() {
1583
+ this.updateProbabilities();
1584
+ super.start();
1585
+ }
1586
+ reset() {
1587
+ this.currentIndex = -1;
1588
+ this.surface.setFocus([]);
1589
+ }
1590
+ step() {
1591
+ this.currentIndex++;
1592
+ if (this.currentIndex >= this.scanOrder.length) {
1593
+ this.currentIndex = 0;
1594
+ }
1595
+ const actualIndex = this.scanOrder[this.currentIndex];
1596
+ const cfg = this.config.get();
1597
+ this.surface.setFocus([actualIndex], {
1598
+ phase: "item",
1599
+ scanRate: cfg.scanRate,
1600
+ scanPattern: cfg.scanPattern,
1601
+ scanTechnique: cfg.scanTechnique,
1602
+ scanDirection: cfg.scanDirection
1603
+ });
1604
+ }
1605
+ handleAction(action) {
1606
+ if (action === "cancel") {
1607
+ this.reset();
1608
+ } else {
1609
+ super.handleAction(action);
1610
+ }
1611
+ }
1612
+ doSelection() {
1613
+ if (this.currentIndex >= 0) {
1614
+ const actualIndex = this.scanOrder[this.currentIndex];
1615
+ if (actualIndex >= 0) {
1616
+ this.triggerSelection(actualIndex);
1617
+ const data = this.surface.getItemData?.(actualIndex);
1618
+ if (data?.label) {
1619
+ this.predictor.addToContext(data.label.toLowerCase());
1620
+ }
1621
+ this.updateProbabilities();
1622
+ this.triggerRedraw();
1623
+ this.reset();
1624
+ if (this.timer) clearTimeout(this.timer);
1625
+ this.scheduleNextStep();
1626
+ }
1627
+ }
1628
+ }
1629
+ updateProbabilities() {
1630
+ const preds = this.predictor.predictNext();
1631
+ const itemsCount = this.surface.getItemsCount();
1632
+ const itemProbs = [];
1633
+ for (let i = 0; i < itemsCount; i++) {
1634
+ let prob = 1e-4;
1635
+ const data = this.surface.getItemData?.(i);
1636
+ if (data?.label) {
1637
+ const label = data.label.toLowerCase();
1638
+ const p = preds.find((p2) => p2.text.toLowerCase() === label);
1639
+ if (p) prob = p.probability;
1640
+ }
1641
+ itemProbs.push({ index: i, prob });
1642
+ }
1643
+ itemProbs.sort((a, b) => b.prob - a.prob);
1644
+ this.scanOrder = itemProbs.map((ip) => ip.index);
1645
+ }
1646
+ getCost(itemIndex) {
1647
+ const orderIndex = this.scanOrder.indexOf(itemIndex);
1648
+ if (orderIndex === -1) return 999;
1649
+ return orderIndex + 1;
1650
+ }
1651
+ };
1652
+
1653
+ // src/scanners/CauseEffectScanner.ts
1654
+ var CauseEffectScanner = class extends Scanner {
1655
+ start() {
1656
+ this.isRunning = true;
1657
+ this.reset();
1658
+ const count = this.surface.getItemsCount();
1659
+ if (count > 0) {
1660
+ const indices = Array.from({ length: count }, (_, i) => i);
1661
+ const cfg = this.config.get();
1662
+ this.surface.setFocus(indices, {
1663
+ phase: "major",
1664
+ scanRate: cfg.scanRate,
1665
+ scanPattern: cfg.scanPattern,
1666
+ scanTechnique: cfg.scanTechnique,
1667
+ scanDirection: cfg.scanDirection
1668
+ });
1669
+ }
1670
+ }
1671
+ handleAction(action) {
1672
+ if (action === "select" && this.isRunning) {
1673
+ super.handleAction(action);
1674
+ }
1675
+ }
1676
+ doSelection() {
1677
+ const count = this.surface.getItemsCount();
1678
+ if (count > 0) {
1679
+ this.triggerSelection(0);
1680
+ }
1681
+ }
1682
+ step() {
1683
+ }
1684
+ reset() {
1685
+ const count = this.surface.getItemsCount();
1686
+ if (count > 0) {
1687
+ const indices = Array.from({ length: count }, (_, i) => i);
1688
+ const cfg = this.config.get();
1689
+ this.surface.setFocus(indices, {
1690
+ phase: "major",
1691
+ scanRate: cfg.scanRate,
1692
+ scanPattern: cfg.scanPattern,
1693
+ scanTechnique: cfg.scanTechnique,
1694
+ scanDirection: cfg.scanDirection
1695
+ });
1696
+ }
1697
+ }
1698
+ getCost(_itemIndex) {
1699
+ return 1;
1700
+ }
1701
+ };
1702
+
1703
+ // src/scanners/ColorCodeScanner.ts
1704
+ var ColorCodeScanner = class extends Scanner {
1705
+ constructor() {
1706
+ super(...arguments);
1707
+ __publicField(this, "probabilities", []);
1708
+ __publicField(this, "colors", []);
1709
+ }
1710
+ start() {
1711
+ this.isRunning = true;
1712
+ this.initializeBelief();
1713
+ this.assignColors();
1714
+ this.applyColors();
1715
+ }
1716
+ stop() {
1717
+ this.isRunning = false;
1718
+ this.surface.setFocus([]);
1719
+ }
1720
+ handleAction(action) {
1721
+ if (!this.isRunning) return;
1722
+ let clicked = null;
1723
+ if (action === "switch-1" || action === "select") {
1724
+ clicked = "blue";
1725
+ } else if (action === "switch-2" || action === "step") {
1726
+ clicked = "red";
1727
+ } else if (action === "reset") {
1728
+ this.initializeBelief();
1729
+ this.assignColors();
1730
+ this.applyColors();
1731
+ return;
1732
+ } else {
1733
+ return;
1734
+ }
1735
+ this.updateBelief(clicked);
1736
+ this.assignColors();
1737
+ this.applyColors();
1738
+ }
1739
+ step() {
1740
+ }
1741
+ reset() {
1742
+ this.initializeBelief();
1743
+ this.assignColors();
1744
+ this.applyColors();
1745
+ }
1746
+ getCost(_itemIndex) {
1747
+ return 1;
1748
+ }
1749
+ doSelection() {
1750
+ }
1751
+ initializeBelief() {
1752
+ const count = this.surface.getItemsCount();
1753
+ const value = count > 0 ? 1 / count : 0;
1754
+ this.probabilities = new Array(count).fill(value);
1755
+ }
1756
+ assignColors() {
1757
+ const indices = this.probabilities.map((p, i) => ({ p, i })).sort((a, b) => b.p - a.p);
1758
+ let redSum = 0;
1759
+ let blueSum = 0;
1760
+ this.colors = new Array(this.probabilities.length).fill("blue");
1761
+ for (const entry of indices) {
1762
+ if (redSum <= blueSum) {
1763
+ this.colors[entry.i] = "red";
1764
+ redSum += entry.p;
1765
+ } else {
1766
+ this.colors[entry.i] = "blue";
1767
+ blueSum += entry.p;
1768
+ }
1769
+ }
1770
+ }
1771
+ applyColors() {
1772
+ const redColor = "#f9c6c6";
1773
+ const blueColor = "#cfe3ff";
1774
+ for (let i = 0; i < this.colors.length; i++) {
1775
+ this.surface.setItemStyle?.(i, {
1776
+ backgroundColor: this.colors[i] === "red" ? redColor : blueColor,
1777
+ textColor: "#1e1e1e"
1778
+ });
1779
+ }
1780
+ }
1781
+ updateBelief(clicked) {
1782
+ const { errorRate, selectThreshold } = this.config.get().colorCode;
1783
+ const correctProb = 1 - errorRate;
1784
+ let sum = 0;
1785
+ for (let i = 0; i < this.probabilities.length; i++) {
1786
+ const isMatch = this.colors[i] === clicked;
1787
+ const likelihood = isMatch ? correctProb : errorRate;
1788
+ this.probabilities[i] *= likelihood;
1789
+ sum += this.probabilities[i];
1790
+ }
1791
+ if (sum > 0) {
1792
+ for (let i = 0; i < this.probabilities.length; i++) {
1793
+ this.probabilities[i] /= sum;
1794
+ }
1795
+ }
1796
+ let maxIndex = 0;
1797
+ for (let i = 1; i < this.probabilities.length; i++) {
1798
+ if (this.probabilities[i] > this.probabilities[maxIndex]) {
1799
+ maxIndex = i;
1800
+ }
1801
+ }
1802
+ if (this.probabilities[maxIndex] >= selectThreshold) {
1803
+ this.triggerSelection(maxIndex);
1804
+ this.initializeBelief();
1805
+ }
1806
+ }
1807
+ };
1808
+ export {
1809
+ CauseEffectScanner,
1810
+ ColorCodeScanner,
1811
+ ContinuousScanner,
1812
+ EliminationScanner,
1813
+ GroupScanner,
1814
+ LinearScanner,
1815
+ OverscanState,
1816
+ PredictorManager,
1817
+ ProbabilityScanner,
1818
+ QuadrantScanner,
1819
+ RowColumnScanner,
1820
+ Scanner,
1821
+ SnakeScanner
1822
+ };
1823
+ //# sourceMappingURL=index.js.map