@@ -32,19 +32,20 @@ export interface UseCardAutoScanOptions {
3232const FRAME_MS = 100
3333/** Long edge of the downscaled frame sent for detection (more = sharper quad). */
3434const PROC_EDGE = 640
35- /** "No card" detections before re-arming after a shot (card removed). */
36- const LOST_TICKS_REARM = 3
37- /** Floor between two captures (anti double-shot). */
38- const MIN_COOLDOWN_MS = 1500
35+ /**
36+ * Floor between two captures. Short on purpose: as soon as it elapses the next
37+ * confirmed card fires, even if the previous card is still in frame — no more
38+ * "remove the card to re-arm" gate (the cause of stuck "Présentez une carte").
39+ */
40+ const MIN_COOLDOWN_MS = 600
3941/**
4042 * How many shots we take in a burst — we keep the sharpest for OCR. Pikacheck-
41- * style: a slightly larger burst over a longer window so the user can pivot
42- * the card during capture (revealing a glary corner, exposing missing text…)
43- * and the sharpest / clearest angle still wins.
43+ * style: a longer window so the user has time to pivot the card during capture
44+ * (revealing a glary corner, exposing missing text…) and the best angle wins.
4445 */
45- const BURST_COUNT = 3
46+ const BURST_COUNT = 4
4647/** Gap between burst shots — wide enough for hand movement to expose new info. */
47- const BURST_INTERVAL_MS = 280
48+ const BURST_INTERVAL_MS = 300
4849/** Lerp weight (new vs previous) when tracking the displayed quad. */
4950const SMOOTH_LERP = 0.5
5051/** Drift above this fraction of the long edge ⇒ new scene, snap instead of lerp. */
@@ -97,7 +98,6 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
9798 let displayedCorners : Pt [ ] | null = null
9899 let pendingCorners : Pt [ ] | null = null
99100 let missTicks = 0
100- let lostTicks = 0
101101 let lastCaptureAt = 0
102102
103103 let burstCorners : Pt [ ] | null = null
@@ -303,7 +303,6 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
303303 }
304304 const file = new File ( [ blob ] , `card-${ Date . now ( ) } .jpg` , { type : 'image/jpeg' } )
305305 lastCaptureAt = Date . now ( )
306- lostTicks = 0
307306 void Promise . resolve ( onCapture ( file ) ) . finally ( finishCooldown )
308307 } ,
309308 'image/jpeg' ,
@@ -335,19 +334,20 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
335334 lastCorners = null
336335 displayedCorners = null
337336 quad . value = null
338- if ( phase . value === 'cooldown' ) {
339- lostTicks += 1
340- if ( lostTicks >= LOST_TICKS_REARM ) {
341- phase . value = 'watching'
342- }
343- } else if ( phase . value !== 'idle' && ! capturing ) {
337+ if ( phase . value !== 'idle' && ! capturing ) {
344338 phase . value = 'watching'
345339 }
346340 }
347341 return
348342 }
349343 missTicks = 0
350344
345+ // Cooldown is now purely time-based — once it elapses, the next confirmed
346+ // quad fires regardless of whether the previous card left the frame.
347+ if ( phase . value === 'cooldown' && Date . now ( ) - lastCaptureAt >= MIN_COOLDOWN_MS ) {
348+ phase . value = 'watching'
349+ }
350+
351351 // Voting: wait for two consecutive detections within VOTE_DRIFT_FRAC of
352352 // each other before trusting the result. Hand-shake / texture flicker
353353 // produces wildly varying quads and gets filtered here.
@@ -367,20 +367,18 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
367367 displayedCorners = smoothCorners ( displayedCorners , confirmed , longEdge )
368368 const shaped = forceCardShape ( displayedCorners )
369369 quad . value = [ shaped [ 0 ] ! , shaped [ 1 ] ! , shaped [ 2 ] ! , shaped [ 3 ] ! ]
370- lostTicks = 0
371370
372371 lastCorners = corners
373372
374- if ( phase . value === 'cooldown' || capturing ) {
373+ if ( capturing ) {
375374 return
376375 }
377376
378- // No "hold steady" wait — Pikacheck-style. As soon as a card is confirmed
379- // (vote passed) and we're past the cooldown, we shoot a burst. The user is
380- // free to pivot/move the card during the burst; each shot uses the freshest
381- // corners and the sharpest wins.
377+ // No "hold steady" wait and no "card must leave" gate — Pikacheck-style.
378+ // As soon as a card is confirmed (vote passed) and the short time cooldown
379+ // is past, we shoot a burst. The user is free to swap cards directly; each
380+ // burst shot uses the freshest corners so a pivot during capture wins.
382381 if ( busy . value || Date . now ( ) - lastCaptureAt < MIN_COOLDOWN_MS ) {
383- phase . value = 'watching'
384382 return
385383 }
386384
@@ -498,7 +496,6 @@ export function useCardAutoScan(opts: UseCardAutoScanOptions) {
498496 burstCorners = null
499497 burstShots = [ ]
500498 burstAwaiting = 0
501- lostTicks = 0
502499 quad . value = null
503500 ready . value = false
504501 phase . value = 'idle'
0 commit comments