Vierte Scan-Funktion "Zählen": laufende Anzeige, keine Buchung
Neue Funktion in scan-modes.js (Merkmal continuousCount), verdrahtet in main.js ueber denselben Zeitgeber/dieselbe Ueberlappungssperre wie die Barcode-Dauersuche (continuousSearchLoop ruft jetzt runCountAttempt() zusaetzlich zu runSearchAttempt(); jeder bricht selbst ab, wenn die Funktion nicht passt) - kein zweiter, paralleler Mechanismus. Kein "Modul scannen"-Knopf, keine Treffer-Rueckmeldung, kein roter Dialog, keine Stapel-Zuweisung; die grosse Zaehlanzeige (scan-view.js/styles.css) ist reine Anzeige und speichert nichts. buildRecognitionAdapters() liefert fuer "Zählen" zusaetzlich leere Adapter (Absicherung gegen den seltenen Datei-Ersatzweg ohne Kamera). Die drei bestehenden Funktionen bleiben unveraendert; einzige notwendige Anpassung an scan-modes.test.js war die Anzahl-Pruefung (3 -> 4 Funktionen). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+47
-4
@@ -3,6 +3,7 @@
|
||||
import { startCamera, grabFrameRegion, imageDataFromFile } from './camera.js';
|
||||
import { decodeBarcodes } from './barcode.js';
|
||||
import { runOcr, isOcrAvailable } from './ocr.js';
|
||||
import { countObjects } from './count-objects.js';
|
||||
import { recognize } from './pipeline.js';
|
||||
import { normalizeToken } from './spec.js';
|
||||
import { getScanMode } from './scan-modes.js';
|
||||
@@ -92,12 +93,17 @@ function applyMode(modeId) {
|
||||
currentModeId = modeId;
|
||||
const mode = getScanMode(modeId);
|
||||
view.setMode(mode.label);
|
||||
// "Modul scannen" hat nur dort etwas zu tun, wo es keine laufende Suche
|
||||
// gibt (Funktion 3) - siehe Kommentar in scan-view.js/setCaptureVisible.
|
||||
view.setCaptureVisible(!mode.continuousSearch);
|
||||
// "Modul scannen" hat nur dort etwas zu tun, wo es weder laufende
|
||||
// Codesuche noch laufende Zaehlung gibt (Funktion 3) - siehe Kommentar in
|
||||
// scan-view.js/setCaptureVisible.
|
||||
view.setCaptureVisible(!mode.continuousSearch && !mode.continuousCount);
|
||||
if (!mode.continuousSearch) {
|
||||
view.setFrameDetected(false);
|
||||
}
|
||||
// Die grosse Zaehlanzeige gehoert ausschliesslich zu Funktion "Zählen" -
|
||||
// beim Verlassen sofort ausblenden, statt auf den naechsten (dann gar
|
||||
// nicht mehr laufenden) Durchlauf der Dauerzaehlung zu warten.
|
||||
view.setCountVisible(mode.continuousCount);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,15 +357,52 @@ async function runSearchAttempt() {
|
||||
await processCapture(() => Promise.resolve(region));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein einzelner Durchlauf der Dauerzaehlung (Funktion "Zählen"): Ausschnitt
|
||||
* in nativer Aufloesung holen, Fundstuecke zaehlen (siehe count-objects.js)
|
||||
* und die grosse Anzeige aktualisieren. Anders als runSearchAttempt() wird
|
||||
* hier nie gebucht und nichts gesperrt - reine Anzeige, busy bleibt
|
||||
* unberuehrt, weil kein Ergebnis in die Sitzung geht und daher auch kein
|
||||
* "Modul scannen", keine Treffer-Rueckmeldung und keine Stapel-Zuweisung
|
||||
* dazugehoeren (siehe scan-modes.js).
|
||||
*/
|
||||
async function runCountAttempt() {
|
||||
// Nur in Funktion "Zählen" aktiv - siehe scan-modes.js.
|
||||
if (!getScanMode(currentModeId).continuousCount) return;
|
||||
|
||||
// Dieselbe Pausenbedingung wie bei der Barcode-Dauersuche (laufende
|
||||
// Erkennung oder offenes Vollbild-Overlay) - siehe searchPaused() oben.
|
||||
if (searchPaused()) return;
|
||||
|
||||
let region;
|
||||
try {
|
||||
region = grabFrameRegion(view.video);
|
||||
} catch {
|
||||
// Kamera liefert noch keine brauchbare Bildgroesse - naechster Durchlauf
|
||||
// versucht es erneut.
|
||||
return;
|
||||
}
|
||||
|
||||
const { count } = countObjects(region);
|
||||
view.setCount(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Laeuft, solange die Kamera aktiv ist. Ein neuer Versuch beginnt erst, wenn
|
||||
* der vorherige vollstaendig fertig ist - nie ueberlappend.
|
||||
* der vorherige vollstaendig fertig ist - nie ueberlappend. Fuehrt pro
|
||||
* Durchlauf sowohl den Barcode- als auch den Zaehl-Versuch aus; jeder der
|
||||
* beiden bricht selbst sofort ab, wenn die gewaehlte Funktion nicht zu ihm
|
||||
* passt (siehe runSearchAttempt()/runCountAttempt()) - so bleibt es bei
|
||||
* genau einem Zeitgeber und derselben Ueberlappungssperre fuer alle
|
||||
* fortlaufenden Funktionen, statt einen zweiten, parallelen Mechanismus
|
||||
* fuer "Zählen" danebenzustellen.
|
||||
*/
|
||||
async function continuousSearchLoop() {
|
||||
for (;;) {
|
||||
const started = performance.now();
|
||||
try {
|
||||
await runSearchAttempt();
|
||||
await runCountAttempt();
|
||||
} catch {
|
||||
// Ein einzelner fehlgeschlagener Versuch darf die Dauersuche nicht
|
||||
// dauerhaft abbrechen.
|
||||
|
||||
+21
-1
@@ -1,4 +1,4 @@
|
||||
// Die drei Scan-Funktionen, die der Nutzer vor dem Scannen waehlt. Reines
|
||||
// Die vier Scan-Funktionen, die der Nutzer vor dem Scannen waehlt. Reines
|
||||
// Modul ohne Browser-Zugriff (kein window/document/navigator) - einzige
|
||||
// Quelle fuer Kennung, Beschriftung, Codearten und Verhalten je Funktion.
|
||||
// Sowohl die Auswahl-Oberflaeche als auch main.js lesen ausschliesslich von
|
||||
@@ -11,6 +11,12 @@
|
||||
// RAM-Etiketten sind DataMatrix, nicht QR - eine auf QR beschraenkte Funktion
|
||||
// faende auf dieser Ware nichts. Die Kennung heisst weiterhin `qrcode`, siehe
|
||||
// Kommentar dort.
|
||||
//
|
||||
// Funktion 4 ("Zaehlen") ist reine Anzeige: Sie liest weder Codes noch Text
|
||||
// und bucht nichts in die Sitzung - deshalb `continuousSearch: false` und
|
||||
// `useOcr: false`, obwohl sie (wie die Barcode-Dauersuche) fortwaehrend
|
||||
// laeuft, solange die Kamera aktiv ist. Dafuer eigenes Merkmal
|
||||
// `continuousCount`, siehe main.js.
|
||||
|
||||
/**
|
||||
* @typedef {object} ScanMode
|
||||
@@ -22,6 +28,10 @@
|
||||
* @property {boolean} continuousSearch Ob waehrend die Kamera laeuft
|
||||
* fortwaehrend nach Codes gesucht wird (statt nur auf Knopfdruck).
|
||||
* @property {boolean} useOcr Ob die Texterkennung (Tesseract) genutzt wird.
|
||||
* @property {boolean} continuousCount Ob waehrend die Kamera laeuft
|
||||
* fortwaehrend die Fundstuecke im Zielrahmen gezaehlt werden (siehe
|
||||
* count-objects.js) - reine Anzeige, ohne Buchung. Nur bei Funktion
|
||||
* "Zaehlen" true.
|
||||
*/
|
||||
|
||||
/** @type {ScanMode[]} */
|
||||
@@ -56,6 +66,16 @@ export const SCAN_MODES = [
|
||||
continuousSearch: false,
|
||||
useOcr: true,
|
||||
},
|
||||
{
|
||||
id: 'count',
|
||||
label: 'Zählen',
|
||||
// Keine Codes, keine Texterkennung - reine Bildauswertung (siehe
|
||||
// count-objects.js), die nichts bucht.
|
||||
barcodeFormats: [],
|
||||
continuousSearch: false,
|
||||
useOcr: false,
|
||||
continuousCount: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
// Funktion 3 (Text erkennen): echte Texterkennung; die Barcode-Dekodierung
|
||||
// liefert sofort eine leere Liste, ohne dass decodeBarcodes ueberhaupt
|
||||
// aufgerufen wird.
|
||||
// Funktion 4 (Zaehlen): weder Barcode-Dekodierung noch Texterkennung -
|
||||
// recognize() (pipeline.js) wird im normalen Ablauf fuer diese Funktion gar
|
||||
// nicht aufgerufen (siehe main.js: kein Knopf, keine Dauersuche im Sinne
|
||||
// dieses Moduls); dieselbe leere Ruecklieferung wie in Funktion 3 dient hier
|
||||
// nur als Absicherung fuer den seltenen Fall, dass der Ersatzweg ohne Kamera
|
||||
// (Dateiauswahl) waehrend gewaehlter Funktion "Zaehlen" ausgeloest wird.
|
||||
|
||||
import { getScanMode } from './scan-modes.js';
|
||||
|
||||
@@ -24,7 +30,7 @@ export function buildRecognitionAdapters(modeId, adapters) {
|
||||
const mode = getScanMode(modeId);
|
||||
|
||||
return {
|
||||
decodeBarcodes: mode.useOcr
|
||||
decodeBarcodes: (mode.useOcr || mode.continuousCount)
|
||||
? async () => []
|
||||
: (imageData) => adapters.decodeBarcodes(imageData, mode.barcodeFormats),
|
||||
runOcr: mode.useOcr
|
||||
|
||||
@@ -52,6 +52,25 @@ body {
|
||||
border-color: var(--green);
|
||||
}
|
||||
|
||||
/* Grosse Zaehlanzeige der Funktion "Zählen" (siehe count-objects.js) - liegt
|
||||
ueber dem Kamerabild, aber unterhalb des Zielrahmens optisch zentriert.
|
||||
pointer-events: none, damit sie den Rahmen/die Kamera nicht fuer
|
||||
Beruehrungen blockiert - diese Funktion hat ohnehin keinen Knopf. Riesige
|
||||
Schriftgroesse mit Schlagschatten statt Kontrastfarbe, damit die Zahl auf
|
||||
jedem Untergrund (hell wie dunkel) aus Armlaenge lesbar bleibt. */
|
||||
.count-display {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: min(35vw, 220px);
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 12px rgba(0, 0, 0, 0.9), 0 0 3px rgba(0, 0, 0, 0.9);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stacks {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SCAN_MODES } from '../scan-modes.js';
|
||||
*/
|
||||
function describeMode(mode) {
|
||||
if (mode.useOcr) return 'Auf Knopfdruck';
|
||||
if (mode.continuousCount) return 'Zählt fortlaufend, solange die Kamera läuft';
|
||||
return 'Läuft von selbst, sobald ein Code im Bild ist';
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
||||
<div class="camera">
|
||||
<video id="preview" playsinline muted></video>
|
||||
<div class="frame"></div>
|
||||
<div class="count-display" id="count-display" hidden aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
<button class="action capture" id="capture" hidden>Modul scannen</button>
|
||||
@@ -30,6 +31,7 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
||||
const frameEl = root.querySelector('.frame');
|
||||
const captureEl = root.querySelector('#capture');
|
||||
const modeButtonEl = root.querySelector('#mode-button');
|
||||
const countDisplayEl = root.querySelector('#count-display');
|
||||
|
||||
// FRAME_INSET (camera.js) ist die einzige Quelle der Rahmenmasse - sowohl
|
||||
// fuer diesen sichtbaren Rahmen als auch fuer den nativen Ausschnitt
|
||||
@@ -129,5 +131,27 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
||||
setFrameDetected(detected) {
|
||||
frameEl.classList.toggle('detected', detected);
|
||||
},
|
||||
|
||||
/**
|
||||
* Blendet die grosse Zaehlanzeige ein oder aus (nur in Funktion
|
||||
* "Zählen" sichtbar, siehe applyMode() in main.js). Beim Ausblenden wird
|
||||
* die zuletzt angezeigte Zahl geloescht, damit beim naechsten Wechsel in
|
||||
* diese Funktion nicht kurz eine veraltete Zahl aufblitzt.
|
||||
* @param {boolean} visible
|
||||
*/
|
||||
setCountVisible(visible) {
|
||||
countDisplayEl.hidden = !visible;
|
||||
if (!visible) countDisplayEl.textContent = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Zeigt die zuletzt ermittelte Anzahl gefundener Fundstuecke (siehe
|
||||
* count-objects.js) - gross und aus Armlaenge lesbar, aktualisiert sich
|
||||
* mit jedem Durchlauf der Dauerzaehlung.
|
||||
* @param {number} count
|
||||
*/
|
||||
setCount(count) {
|
||||
countDisplayEl.textContent = String(count);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+13
-4
@@ -2,9 +2,9 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { SCAN_MODES, getScanMode } from '../src/scan-modes.js';
|
||||
|
||||
test('genau drei Funktionen in fester Reihenfolge', () => {
|
||||
assert.equal(SCAN_MODES.length, 3);
|
||||
assert.deepEqual(SCAN_MODES.map((mode) => mode.id), ['barcode', 'qrcode', 'ocr']);
|
||||
test('genau vier Funktionen in fester Reihenfolge', () => {
|
||||
assert.equal(SCAN_MODES.length, 4);
|
||||
assert.deepEqual(SCAN_MODES.map((mode) => mode.id), ['barcode', 'qrcode', 'ocr', 'count']);
|
||||
});
|
||||
|
||||
test('Funktion 1 (Strichcode): eindimensionale Codearten, laufende Suche, keine Texterkennung', () => {
|
||||
@@ -28,7 +28,7 @@ test('Funktion 2 (QR-Code): QR-Varianten und DataMatrix, Aztec, PDF417', () => {
|
||||
assert.equal(mode.useOcr, false);
|
||||
});
|
||||
|
||||
test('die drei Funktionen haben paarweise verschiedene Kennungen und Beschriftungen', () => {
|
||||
test('die vier Funktionen haben paarweise verschiedene Kennungen und Beschriftungen', () => {
|
||||
const ids = SCAN_MODES.map((mode) => mode.id);
|
||||
const labels = SCAN_MODES.map((mode) => mode.label);
|
||||
assert.equal(new Set(ids).size, ids.length, 'Kennungen muessen paarweise verschieden sein');
|
||||
@@ -43,6 +43,15 @@ test('Funktion 3 (Text erkennen): keine Codearten, keine laufende Suche, Texterk
|
||||
assert.equal(mode.useOcr, true);
|
||||
});
|
||||
|
||||
test('Funktion 4 (Zählen): keine Codearten, keine laufende Codesuche, keine Texterkennung, dafuer laufende Zaehlung', () => {
|
||||
const mode = getScanMode('count');
|
||||
assert.equal(mode.label, 'Zählen');
|
||||
assert.deepEqual(mode.barcodeFormats, []);
|
||||
assert.equal(mode.continuousSearch, false);
|
||||
assert.equal(mode.useOcr, false);
|
||||
assert.equal(mode.continuousCount, true);
|
||||
});
|
||||
|
||||
test('unbekannte Kennung wirft statt undefined zurueckzugeben', () => {
|
||||
assert.throws(() => getScanMode('unbekannt'));
|
||||
});
|
||||
|
||||
@@ -67,6 +67,19 @@ test('decodeBarcodes bekommt die Codearten der jeweiligen Funktion uebergeben',
|
||||
assert.deepEqual(receivedFormats, ['QRCode', 'MicroQRCode', 'RMQRCode', 'DataMatrix', 'Aztec', 'PDF417']);
|
||||
});
|
||||
|
||||
test('Funktion 4 (Zaehlen): weder Barcode-Dekodierung noch Texterkennung werden aufgerufen', async () => {
|
||||
let barcodeCalled = false;
|
||||
let ocrCalled = false;
|
||||
const adapters = buildRecognitionAdapters('count', {
|
||||
decodeBarcodes: async () => { barcodeCalled = true; return []; },
|
||||
runOcr: async () => { ocrCalled = true; return 'irgendein Text'; },
|
||||
});
|
||||
const result = await recognize({}, adapters);
|
||||
assert.equal(barcodeCalled, false, 'decodeBarcodes darf in Funktion 4 nie aufgerufen werden');
|
||||
assert.equal(ocrCalled, false, 'runOcr darf in Funktion 4 nie aufgerufen werden');
|
||||
assert.equal(result.confidence, 'red');
|
||||
});
|
||||
|
||||
test('Funktion 3 uebergibt keine Codearten, weil decodeBarcodes gar nicht aufgerufen wird', async () => {
|
||||
let called = false;
|
||||
const adapters = buildRecognitionAdapters('ocr', {
|
||||
|
||||
Reference in New Issue
Block a user