@mleonard9/vin-scanner 1.4.0 → 1.4.3
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/README.md +6 -38
- package/ios/VisionCameraBarcodeScanner.m +4 -4
- package/ios/VisionCameraTextRecognition.m +5 -5
- package/lib/commonjs/ManualVinInput.js +1 -2
- package/lib/commonjs/ManualVinInput.js.map +1 -1
- package/lib/commonjs/PendingVinBanner.js.map +1 -1
- package/lib/commonjs/TextVinPrompt.js.map +1 -1
- package/lib/commonjs/haptics.js +1 -1
- package/lib/commonjs/haptics.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/scanBarcodes.js.map +1 -1
- package/lib/commonjs/scanText.js.map +1 -1
- package/lib/commonjs/useVinScanner.js.map +1 -1
- package/lib/commonjs/vinUtils.js +8 -8
- package/lib/commonjs/vinUtils.js.map +1 -1
- package/lib/module/ManualVinInput.js +1 -2
- package/lib/module/ManualVinInput.js.map +1 -1
- package/lib/module/PendingVinBanner.js.map +1 -1
- package/lib/module/TextVinPrompt.js.map +1 -1
- package/lib/module/haptics.js +1 -1
- package/lib/module/haptics.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/scanBarcodes.js.map +1 -1
- package/lib/module/scanText.js.map +1 -1
- package/lib/module/useVinScanner.js.map +1 -1
- package/lib/module/vinUtils.js +8 -8
- package/lib/module/vinUtils.js.map +1 -1
- package/lib/typescript/src/ManualVinInput.d.ts.map +1 -1
- package/lib/typescript/src/PendingVinBanner.d.ts.map +1 -1
- package/lib/typescript/src/TextVinPrompt.d.ts.map +1 -1
- package/lib/typescript/src/haptics.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/scanBarcodes.d.ts.map +1 -1
- package/lib/typescript/src/scanText.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useVinScanner.d.ts.map +1 -1
- package/lib/typescript/src/vinUtils.d.ts.map +1 -1
- package/package.json +1 -3
- package/src/ManualVinInput.tsx +44 -5
- package/src/PendingVinBanner.tsx +4 -5
- package/src/TextVinPrompt.tsx +10 -2
- package/src/haptics.ts +2 -5
- package/src/index.tsx +46 -22
- package/src/scanBarcodes.ts +5 -1
- package/src/scanText.ts +5 -1
- package/src/types.ts +9 -4
- package/src/useVinScanner.ts +38 -28
- package/src/vinUtils.ts +112 -70
package/src/ManualVinInput.tsx
CHANGED
|
@@ -10,8 +10,32 @@ type ManualVinInputProps = {
|
|
|
10
10
|
placeholder?: string;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
const VIN_CHARS = [
|
|
14
|
-
|
|
13
|
+
const VIN_CHARS = [
|
|
14
|
+
'A',
|
|
15
|
+
'B',
|
|
16
|
+
'C',
|
|
17
|
+
'D',
|
|
18
|
+
'E',
|
|
19
|
+
'F',
|
|
20
|
+
'G',
|
|
21
|
+
'H',
|
|
22
|
+
'J',
|
|
23
|
+
'K',
|
|
24
|
+
'L',
|
|
25
|
+
'M',
|
|
26
|
+
'N',
|
|
27
|
+
'P',
|
|
28
|
+
'R',
|
|
29
|
+
'S',
|
|
30
|
+
'T',
|
|
31
|
+
'U',
|
|
32
|
+
'V',
|
|
33
|
+
'W',
|
|
34
|
+
'X',
|
|
35
|
+
'Y',
|
|
36
|
+
'Z',
|
|
37
|
+
];
|
|
38
|
+
const DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
15
39
|
|
|
16
40
|
export function ManualVinInput({
|
|
17
41
|
initialValue = '',
|
|
@@ -43,7 +67,14 @@ export function ManualVinInput({
|
|
|
43
67
|
<View style={styles.container}>
|
|
44
68
|
<TextInput
|
|
45
69
|
value={value}
|
|
46
|
-
onChangeText={(txt) =>
|
|
70
|
+
onChangeText={(txt) =>
|
|
71
|
+
setValue(
|
|
72
|
+
txt
|
|
73
|
+
.replace(/[^A-HJ-NPR-Z0-9]/gi, '')
|
|
74
|
+
.slice(0, 17)
|
|
75
|
+
.toUpperCase()
|
|
76
|
+
)
|
|
77
|
+
}
|
|
47
78
|
style={[styles.input, showError && styles.errorInput]}
|
|
48
79
|
placeholder={placeholder}
|
|
49
80
|
autoCapitalize="characters"
|
|
@@ -61,7 +92,7 @@ export function ManualVinInput({
|
|
|
61
92
|
<Key label="⌫" onPress={handleBackspace} wide />
|
|
62
93
|
</View>
|
|
63
94
|
<Pressable
|
|
64
|
-
style={[styles.submit, { backgroundColor: buttonColor
|
|
95
|
+
style={[styles.submit, { backgroundColor: buttonColor }]}
|
|
65
96
|
disabled={!isValid}
|
|
66
97
|
onPress={handleSubmit}
|
|
67
98
|
>
|
|
@@ -72,7 +103,15 @@ export function ManualVinInput({
|
|
|
72
103
|
);
|
|
73
104
|
}
|
|
74
105
|
|
|
75
|
-
function Key({
|
|
106
|
+
function Key({
|
|
107
|
+
label,
|
|
108
|
+
onPress,
|
|
109
|
+
wide,
|
|
110
|
+
}: {
|
|
111
|
+
label: string;
|
|
112
|
+
onPress: () => void;
|
|
113
|
+
wide?: boolean;
|
|
114
|
+
}) {
|
|
76
115
|
return (
|
|
77
116
|
<Pressable onPress={onPress} style={[styles.key, wide && styles.keyWide]}>
|
|
78
117
|
<Text style={styles.keyText}>{label}</Text>
|
package/src/PendingVinBanner.tsx
CHANGED
|
@@ -43,14 +43,13 @@ export function PendingVinBanner({
|
|
|
43
43
|
return (
|
|
44
44
|
<Animated.View
|
|
45
45
|
pointerEvents={visible ? 'auto' : 'none'}
|
|
46
|
-
style={[
|
|
47
|
-
styles.container,
|
|
48
|
-
{ transform: [{ translateY }], opacity: anim },
|
|
49
|
-
]}
|
|
46
|
+
style={[styles.container, { transform: [{ translateY }], opacity: anim }]}
|
|
50
47
|
>
|
|
51
48
|
<View style={styles.header}>
|
|
52
49
|
<Text style={styles.label}>
|
|
53
|
-
{candidates.length > 1
|
|
50
|
+
{candidates.length > 1
|
|
51
|
+
? `${candidates.length} VINs detected`
|
|
52
|
+
: 'VIN detected'}
|
|
54
53
|
</Text>
|
|
55
54
|
{onDismiss && (
|
|
56
55
|
<Pressable onPress={onDismiss} hitSlop={12} style={styles.dismiss}>
|
package/src/TextVinPrompt.tsx
CHANGED
|
@@ -38,12 +38,20 @@ export function TextVinPrompt({
|
|
|
38
38
|
if (!visible) return null;
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
|
-
<Modal
|
|
41
|
+
<Modal
|
|
42
|
+
transparent
|
|
43
|
+
animationType="fade"
|
|
44
|
+
visible={visible}
|
|
45
|
+
onRequestClose={onDismiss}
|
|
46
|
+
>
|
|
42
47
|
<View style={styles.backdrop}>
|
|
43
48
|
<View style={styles.sheet}>
|
|
44
49
|
<Text style={styles.title}>{title}</Text>
|
|
45
50
|
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
|
|
46
|
-
<ScrollView
|
|
51
|
+
<ScrollView
|
|
52
|
+
style={styles.list}
|
|
53
|
+
contentContainerStyle={styles.listContent}
|
|
54
|
+
>
|
|
47
55
|
{candidates.map((candidate) => (
|
|
48
56
|
<View key={candidate.value} style={styles.row}>
|
|
49
57
|
<Text style={styles.vin} numberOfLines={1}>
|
package/src/haptics.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
let haptic: any = null;
|
|
2
2
|
try {
|
|
3
3
|
// Optional dependency: react-native-haptic-feedback
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
haptic = require('react-native-haptic-feedback');
|
|
6
6
|
} catch {
|
|
7
7
|
haptic = null;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
type HapticType =
|
|
11
|
-
| 'success'
|
|
12
|
-
| 'warning'
|
|
13
|
-
| 'impactLight';
|
|
10
|
+
type HapticType = 'success' | 'warning' | 'impactLight';
|
|
14
11
|
|
|
15
12
|
const trigger = (type: HapticType) => {
|
|
16
13
|
if (!haptic?.default?.trigger) {
|
package/src/index.tsx
CHANGED
|
@@ -46,7 +46,9 @@ export const Camera = forwardRef(function Camera(
|
|
|
46
46
|
const handleDetections = useRunOnJS(
|
|
47
47
|
(payload: WorkletPayload) => {
|
|
48
48
|
const candidates = buildVinCandidates(payload, resolvedOptions);
|
|
49
|
-
const barcodeCandidates = candidates.filter(
|
|
49
|
+
const barcodeCandidates = candidates.filter(
|
|
50
|
+
(c) => c.source === 'barcode'
|
|
51
|
+
);
|
|
50
52
|
const textCandidates = candidates.filter((c) => c.source === 'text');
|
|
51
53
|
|
|
52
54
|
if (barcodeCandidates.length > 0) {
|
|
@@ -64,7 +66,10 @@ export const Camera = forwardRef(function Camera(
|
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
if (
|
|
69
|
+
if (
|
|
70
|
+
resolvedOptions.text.requireConfirmation &&
|
|
71
|
+
textCandidates.length > 0
|
|
72
|
+
) {
|
|
68
73
|
options?.onTextPending?.(textCandidates);
|
|
69
74
|
return;
|
|
70
75
|
}
|
|
@@ -94,19 +99,24 @@ export const Camera = forwardRef(function Camera(
|
|
|
94
99
|
(frame) => {
|
|
95
100
|
'worklet';
|
|
96
101
|
|
|
97
|
-
const now =
|
|
102
|
+
const now =
|
|
103
|
+
typeof frame?.timestamp === 'number' ? frame.timestamp : Date.now();
|
|
98
104
|
|
|
99
105
|
// Max FPS throttle (matches hook behavior)
|
|
100
106
|
const maxFps = resolvedOptions.detection.maxFrameRate;
|
|
101
107
|
if (maxFps > 0) {
|
|
102
108
|
const minInterval = 1000 / maxFps;
|
|
103
|
-
if (
|
|
109
|
+
if (
|
|
110
|
+
lastFrameTimestampRef.current > 0 &&
|
|
111
|
+
now - lastFrameTimestampRef.current < minInterval
|
|
112
|
+
) {
|
|
104
113
|
return;
|
|
105
114
|
}
|
|
106
115
|
lastFrameTimestampRef.current = now;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
const orientationOverride =
|
|
118
|
+
const orientationOverride =
|
|
119
|
+
resolvedOptions.detection.forceOrientation ?? null;
|
|
110
120
|
const regionOverride = resolvedOptions.detection.scanRegion ?? null;
|
|
111
121
|
|
|
112
122
|
// Frame quality gate (luma + sharpness)
|
|
@@ -158,28 +168,34 @@ export const Camera = forwardRef(function Camera(
|
|
|
158
168
|
const pluginArgs =
|
|
159
169
|
orientationOverride || regionOverride
|
|
160
170
|
? {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
...(orientationOverride && { orientation: orientationOverride }),
|
|
172
|
+
...(regionOverride && { scanRegion: regionOverride }),
|
|
173
|
+
}
|
|
164
174
|
: undefined;
|
|
165
175
|
|
|
166
176
|
// Run barcode every frame
|
|
167
177
|
const barcodes = scanBarcodes
|
|
168
178
|
? scanBarcodes(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
: pluginArgs
|
|
173
|
-
)
|
|
179
|
+
frame,
|
|
180
|
+
useBarcodeFallback.current ? { all: true } : pluginArgs
|
|
181
|
+
)
|
|
174
182
|
: [];
|
|
175
183
|
|
|
176
184
|
// Run text every Nth frame per textScanInterval
|
|
177
185
|
frameCounterRef.current += 1;
|
|
178
186
|
const frameIndex = frameCounterRef.current;
|
|
179
187
|
const textScanInterval = resolvedOptions.detection.textScanInterval;
|
|
180
|
-
const shouldRunText =
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
const shouldRunText =
|
|
189
|
+
scanText &&
|
|
190
|
+
(textScanInterval <= 1 || frameIndex % textScanInterval === 0);
|
|
191
|
+
let textBlocks =
|
|
192
|
+
shouldRunText && scanText ? (scanText(frame, pluginArgs) ?? []) : [];
|
|
193
|
+
if (
|
|
194
|
+
scanText &&
|
|
195
|
+
(textBlocks ?? []).length === 0 &&
|
|
196
|
+
(barcodes ?? []).length === 0 &&
|
|
197
|
+
!shouldRunText
|
|
198
|
+
) {
|
|
183
199
|
textBlocks = scanText(frame, pluginArgs) ?? [];
|
|
184
200
|
}
|
|
185
201
|
|
|
@@ -198,7 +214,10 @@ export const Camera = forwardRef(function Camera(
|
|
|
198
214
|
const hasBarcode = (payload.barcodes ?? []).length > 0;
|
|
199
215
|
if (!hasBarcode) {
|
|
200
216
|
noBarcodeFrameCount.current += 1;
|
|
201
|
-
if (
|
|
217
|
+
if (
|
|
218
|
+
noBarcodeFrameCount.current >=
|
|
219
|
+
resolvedOptions.detection.barcodeFallbackAfter
|
|
220
|
+
) {
|
|
202
221
|
useBarcodeFallback.current = true;
|
|
203
222
|
}
|
|
204
223
|
} else {
|
|
@@ -210,7 +229,8 @@ export const Camera = forwardRef(function Camera(
|
|
|
210
229
|
if (firstCandidate) {
|
|
211
230
|
const debounceMs = resolvedOptions.duplicateDebounceMs ?? 1500;
|
|
212
231
|
const isDuplicate =
|
|
213
|
-
firstCandidate.value === lastEmittedVin.current &&
|
|
232
|
+
firstCandidate.value === lastEmittedVin.current &&
|
|
233
|
+
now - lastEmitTimestamp.current < debounceMs;
|
|
214
234
|
const seen = sessionSeen.current.has(firstCandidate.value);
|
|
215
235
|
|
|
216
236
|
if (isDuplicate || seen) {
|
|
@@ -237,9 +257,11 @@ export const Camera = forwardRef(function Camera(
|
|
|
237
257
|
);
|
|
238
258
|
|
|
239
259
|
// Create tap gesture for focus
|
|
240
|
-
const tapGesture = Gesture.Tap().onEnd(
|
|
241
|
-
|
|
242
|
-
|
|
260
|
+
const tapGesture = Gesture.Tap().onEnd(
|
|
261
|
+
({ x, y }: { x: number; y: number }) => {
|
|
262
|
+
runOnJS(focus)({ x, y });
|
|
263
|
+
}
|
|
264
|
+
);
|
|
243
265
|
|
|
244
266
|
return (
|
|
245
267
|
<>
|
|
@@ -252,7 +274,9 @@ export const Camera = forwardRef(function Camera(
|
|
|
252
274
|
device={device}
|
|
253
275
|
enableZoomGesture={true}
|
|
254
276
|
lowLightBoost={options?.cameraSettings?.lowLightBoost ?? true}
|
|
255
|
-
videoStabilizationMode={
|
|
277
|
+
videoStabilizationMode={
|
|
278
|
+
options?.cameraSettings?.videoStabilizationMode ?? 'cinematic'
|
|
279
|
+
}
|
|
256
280
|
fps={Math.min(30, Math.max(24, options?.cameraSettings?.fps ?? 24))}
|
|
257
281
|
{...rest}
|
|
258
282
|
/>
|
package/src/scanBarcodes.ts
CHANGED
|
@@ -116,7 +116,11 @@ const mergeArgs = (
|
|
|
116
116
|
'worklet';
|
|
117
117
|
const merged: BarcodePluginArgs = { ...baseArgs };
|
|
118
118
|
if (overrides) {
|
|
119
|
-
const {
|
|
119
|
+
const {
|
|
120
|
+
orientation: overrideOrientation,
|
|
121
|
+
scanRegion: overrideScanRegion,
|
|
122
|
+
...formatMap
|
|
123
|
+
} = overrides;
|
|
120
124
|
Object.entries(formatMap).forEach(([key, value]) => {
|
|
121
125
|
merged[key as BarcodeFormat] = value as boolean | undefined;
|
|
122
126
|
});
|
package/src/scanText.ts
CHANGED
|
@@ -90,7 +90,11 @@ const mergeTextArgs = (
|
|
|
90
90
|
...base,
|
|
91
91
|
};
|
|
92
92
|
if (overrides) {
|
|
93
|
-
const {
|
|
93
|
+
const {
|
|
94
|
+
orientation: overrideOrientation,
|
|
95
|
+
scanRegion: overrideScanRegion,
|
|
96
|
+
...rest
|
|
97
|
+
} = overrides;
|
|
94
98
|
Object.assign(merged, rest);
|
|
95
99
|
if (overrideScanRegion) {
|
|
96
100
|
merged.scanRegion = overrideScanRegion;
|
package/src/types.ts
CHANGED
|
@@ -102,7 +102,12 @@ export type BoundingBox = {
|
|
|
102
102
|
|
|
103
103
|
export type VinCandidateSource = 'barcode' | 'text';
|
|
104
104
|
|
|
105
|
-
export type VinTextOrigin =
|
|
105
|
+
export type VinTextOrigin =
|
|
106
|
+
| 'rawValue'
|
|
107
|
+
| 'displayValue'
|
|
108
|
+
| 'block'
|
|
109
|
+
| 'line'
|
|
110
|
+
| 'element';
|
|
106
111
|
|
|
107
112
|
export type VinCandidate = {
|
|
108
113
|
value: string;
|
|
@@ -112,9 +117,9 @@ export type VinCandidate = {
|
|
|
112
117
|
origin?: VinTextOrigin;
|
|
113
118
|
boundingBox?: BoundingBox;
|
|
114
119
|
rawPayload?:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
| BarcodeDetection
|
|
121
|
+
| TextDetection
|
|
122
|
+
| { resultText?: string; [key: string]: unknown };
|
|
118
123
|
};
|
|
119
124
|
|
|
120
125
|
export type VinScannerEvent = {
|
package/src/useVinScanner.ts
CHANGED
|
@@ -10,10 +10,7 @@ import {
|
|
|
10
10
|
pickFirstCandidate,
|
|
11
11
|
resolveOptions,
|
|
12
12
|
} from './vinUtils';
|
|
13
|
-
import {
|
|
14
|
-
triggerSuccessHaptic,
|
|
15
|
-
triggerSoftHaptic,
|
|
16
|
-
} from './haptics';
|
|
13
|
+
import { triggerSuccessHaptic, triggerSoftHaptic } from './haptics';
|
|
17
14
|
|
|
18
15
|
export function useVinScanner(options?: VinScannerOptions) {
|
|
19
16
|
const resolvedOptions = useMemo(() => resolveOptions(options), [options]);
|
|
@@ -21,7 +18,9 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
21
18
|
const frameCounterRef = useRef(0);
|
|
22
19
|
const lastEmittedVin = useRef<string | null>(null);
|
|
23
20
|
const lastEmitTimestamp = useRef(0);
|
|
24
|
-
const [pendingTextCandidates, setPendingTextCandidates] = useState<
|
|
21
|
+
const [pendingTextCandidates, setPendingTextCandidates] = useState<
|
|
22
|
+
VinCandidate[]
|
|
23
|
+
>([]);
|
|
25
24
|
const pendingTextTimestampRef = useRef<number | null>(null);
|
|
26
25
|
const pendingTextRef = useRef<VinCandidate[]>([]);
|
|
27
26
|
const pendingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -149,33 +148,33 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
149
148
|
}
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
const barcodeArgs =
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
const barcodeArgs =
|
|
152
|
+
orientationOverride || regionOverride
|
|
153
|
+
? {
|
|
154
|
+
...(orientationOverride && { orientation: orientationOverride }),
|
|
155
|
+
...(regionOverride && { scanRegion: regionOverride }),
|
|
156
|
+
}
|
|
157
|
+
: undefined;
|
|
158
158
|
|
|
159
159
|
// Start performance tracking
|
|
160
160
|
const t0 = Date.now();
|
|
161
161
|
|
|
162
162
|
const barcodes = barcodeScanner
|
|
163
|
-
? barcodeScanner.scanBarcodes(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
: barcodeArgs
|
|
168
|
-
) ?? []
|
|
163
|
+
? (barcodeScanner.scanBarcodes(
|
|
164
|
+
frame,
|
|
165
|
+
useBarcodeFallback.current ? { all: true } : barcodeArgs
|
|
166
|
+
) ?? [])
|
|
169
167
|
: [];
|
|
170
168
|
|
|
171
169
|
const t1 = Date.now();
|
|
172
170
|
|
|
173
|
-
const textArgs =
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
171
|
+
const textArgs =
|
|
172
|
+
orientationOverride || regionOverride
|
|
173
|
+
? {
|
|
174
|
+
...(orientationOverride && { orientation: orientationOverride }),
|
|
175
|
+
...(regionOverride && { scanRegion: regionOverride }),
|
|
176
|
+
}
|
|
177
|
+
: undefined;
|
|
179
178
|
|
|
180
179
|
frameCounterRef.current += 1;
|
|
181
180
|
const frameIndex = frameCounterRef.current;
|
|
@@ -187,7 +186,7 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
187
186
|
|
|
188
187
|
let textBlocks =
|
|
189
188
|
shouldRunText && textScanner
|
|
190
|
-
? textScanner.scanText(frame, textArgs) ?? []
|
|
189
|
+
? (textScanner.scanText(frame, textArgs) ?? [])
|
|
191
190
|
: [];
|
|
192
191
|
|
|
193
192
|
// Two-pass OCR: if we skipped this frame or got nothing and barcodes are empty, run one immediate OCR pass.
|
|
@@ -209,14 +208,19 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
209
208
|
};
|
|
210
209
|
|
|
211
210
|
const candidates = buildVinCandidates(payload, resolvedOptions);
|
|
212
|
-
const filtered = candidates.filter(
|
|
211
|
+
const filtered = candidates.filter(
|
|
212
|
+
(c) => c.confidence >= resolvedOptions.detection.minConfidence
|
|
213
|
+
);
|
|
213
214
|
const barcodeCandidates = filtered.filter((c) => c.source === 'barcode');
|
|
214
215
|
const textCandidates = filtered.filter((c) => c.source === 'text');
|
|
215
216
|
|
|
216
217
|
// Adaptive barcode fallback: if no barcode hits for N frames, scan all formats.
|
|
217
218
|
if (barcodeCandidates.length === 0) {
|
|
218
219
|
noBarcodeFrameCount.current += 1;
|
|
219
|
-
if (
|
|
220
|
+
if (
|
|
221
|
+
noBarcodeFrameCount.current >=
|
|
222
|
+
resolvedOptions.detection.barcodeFallbackAfter
|
|
223
|
+
) {
|
|
220
224
|
useBarcodeFallback.current = true;
|
|
221
225
|
}
|
|
222
226
|
} else {
|
|
@@ -248,7 +252,10 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
248
252
|
|
|
249
253
|
// If a barcode is present, emit immediately (highest confidence path)
|
|
250
254
|
if (barcodeCandidates.length > 0) {
|
|
251
|
-
const barcodeFirst = pickFirstCandidate(
|
|
255
|
+
const barcodeFirst = pickFirstCandidate(
|
|
256
|
+
barcodeCandidates,
|
|
257
|
+
resolvedOptions
|
|
258
|
+
);
|
|
252
259
|
if (barcodeFirst) {
|
|
253
260
|
const debounceMs = resolvedOptions.duplicateDebounceMs ?? 1500;
|
|
254
261
|
const isDuplicate =
|
|
@@ -273,7 +280,10 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
273
280
|
}
|
|
274
281
|
|
|
275
282
|
// For text-only detections, optionally require user confirmation
|
|
276
|
-
if (
|
|
283
|
+
if (
|
|
284
|
+
resolvedOptions.text.requireConfirmation &&
|
|
285
|
+
textCandidates.length > 0
|
|
286
|
+
) {
|
|
277
287
|
emitTextPending(textCandidates, payload.timestamp);
|
|
278
288
|
return;
|
|
279
289
|
}
|