diff --git a/src/main.js b/src/main.js index 0497bf8..24aa5b3 100644 --- a/src/main.js +++ b/src/main.js @@ -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. diff --git a/src/scan-modes.js b/src/scan-modes.js index dddb5bd..1edab0b 100644 --- a/src/scan-modes.js +++ b/src/scan-modes.js @@ -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, + }, ]; /** diff --git a/src/scan-recognition.js b/src/scan-recognition.js index c2f03d1..54d3554 100644 --- a/src/scan-recognition.js +++ b/src/scan-recognition.js @@ -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 diff --git a/src/styles.css b/src/styles.css index 4612f92..b22eb12 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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; diff --git a/src/ui/mode-dialog.js b/src/ui/mode-dialog.js index 68deb0d..80b0cd6 100644 --- a/src/ui/mode-dialog.js +++ b/src/ui/mode-dialog.js @@ -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'; } diff --git a/src/ui/scan-view.js b/src/ui/scan-view.js index d33b1d1..13f31e1 100644 --- a/src/ui/scan-view.js +++ b/src/ui/scan-view.js @@ -10,6 +10,7 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile