@mleonard9/vin-scanner 1.3.0 → 1.4.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/README.md +129 -12
- package/android/src/main/java/com/visioncamerabarcodescanner/VisionCameraBarcodeScannerModule.kt +26 -17
- package/lib/commonjs/ManualVinInput.js +147 -0
- package/lib/commonjs/ManualVinInput.js.map +1 -0
- package/lib/commonjs/PendingVinBanner.js +120 -0
- package/lib/commonjs/PendingVinBanner.js.map +1 -0
- package/lib/commonjs/TextVinPrompt.js +132 -0
- package/lib/commonjs/TextVinPrompt.js.map +1 -0
- package/lib/commonjs/haptics.js +36 -0
- package/lib/commonjs/haptics.js.map +1 -0
- package/lib/commonjs/index.js +184 -13
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/useVinScanner.js +164 -6
- package/lib/commonjs/useVinScanner.js.map +1 -1
- package/lib/commonjs/vinUtils.js +23 -9
- package/lib/commonjs/vinUtils.js.map +1 -1
- package/lib/module/ManualVinInput.js +139 -0
- package/lib/module/ManualVinInput.js.map +1 -0
- package/lib/module/PendingVinBanner.js +112 -0
- package/lib/module/PendingVinBanner.js.map +1 -0
- package/lib/module/TextVinPrompt.js +124 -0
- package/lib/module/TextVinPrompt.js.map +1 -0
- package/lib/module/haptics.js +27 -0
- package/lib/module/haptics.js.map +1 -0
- package/lib/module/index.js +171 -12
- package/lib/module/index.js.map +1 -1
- package/lib/module/useVinScanner.js +165 -7
- package/lib/module/useVinScanner.js.map +1 -1
- package/lib/module/vinUtils.js +23 -9
- package/lib/module/vinUtils.js.map +1 -1
- package/lib/typescript/src/ManualVinInput.d.ts +11 -0
- package/lib/typescript/src/ManualVinInput.d.ts.map +1 -0
- package/lib/typescript/src/PendingVinBanner.d.ts +17 -0
- package/lib/typescript/src/PendingVinBanner.d.ts.map +1 -0
- package/lib/typescript/src/TextVinPrompt.d.ts +20 -0
- package/lib/typescript/src/TextVinPrompt.d.ts.map +1 -0
- package/lib/typescript/src/haptics.d.ts +4 -0
- package/lib/typescript/src/haptics.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +46 -7
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useVinScanner.d.ts +3 -1
- package/lib/typescript/src/useVinScanner.d.ts.map +1 -1
- package/lib/typescript/src/vinUtils.d.ts +7 -2
- package/lib/typescript/src/vinUtils.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/ManualVinInput.tsx +145 -0
- package/src/PendingVinBanner.tsx +128 -0
- package/src/TextVinPrompt.tsx +139 -0
- package/src/haptics.ts +32 -0
- package/src/index.tsx +195 -22
- package/src/types.ts +46 -7
- package/src/useVinScanner.ts +188 -8
- package/src/vinUtils.ts +34 -17
- package/lib/commonjs/VinScannerOverlay.js +0 -60
- package/lib/commonjs/VinScannerOverlay.js.map +0 -1
- package/lib/module/VinScannerOverlay.js +0 -53
- package/lib/module/VinScannerOverlay.js.map +0 -1
- package/lib/typescript/src/VinScannerOverlay.d.ts +0 -14
- package/lib/typescript/src/VinScannerOverlay.d.ts.map +0 -1
- package/src/VinScannerOverlay.tsx +0 -55
package/src/useVinScanner.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useRef } from 'react';
|
|
1
|
+
import { useMemo, useRef, useState, useCallback } from 'react';
|
|
2
2
|
import { useFrameProcessor } from 'react-native-vision-camera';
|
|
3
3
|
import { useRunOnJS } from 'react-native-worklets-core';
|
|
4
4
|
import type { Frame } from 'react-native-vision-camera';
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
pickFirstCandidate,
|
|
11
11
|
resolveOptions,
|
|
12
12
|
} from './vinUtils';
|
|
13
|
+
import {
|
|
14
|
+
triggerSuccessHaptic,
|
|
15
|
+
triggerSoftHaptic,
|
|
16
|
+
} from './haptics';
|
|
13
17
|
|
|
14
18
|
export function useVinScanner(options?: VinScannerOptions) {
|
|
15
19
|
const resolvedOptions = useMemo(() => resolveOptions(options), [options]);
|
|
@@ -17,6 +21,14 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
17
21
|
const frameCounterRef = useRef(0);
|
|
18
22
|
const lastEmittedVin = useRef<string | null>(null);
|
|
19
23
|
const lastEmitTimestamp = useRef(0);
|
|
24
|
+
const [pendingTextCandidates, setPendingTextCandidates] = useState<VinCandidate[]>([]);
|
|
25
|
+
const pendingTextTimestampRef = useRef<number | null>(null);
|
|
26
|
+
const pendingTextRef = useRef<VinCandidate[]>([]);
|
|
27
|
+
const pendingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
28
|
+
const hapticsEnabled = options?.haptics ?? true;
|
|
29
|
+
const sessionSeen = useRef<Set<string>>(new Set());
|
|
30
|
+
const noBarcodeFrameCount = useRef(0);
|
|
31
|
+
const useBarcodeFallback = useRef(false);
|
|
20
32
|
|
|
21
33
|
const barcodeScanner = useMemo(() => {
|
|
22
34
|
if (!resolvedOptions.barcode.enabled) {
|
|
@@ -44,6 +56,32 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
44
56
|
[options?.onResult]
|
|
45
57
|
);
|
|
46
58
|
|
|
59
|
+
const emitTextPending = useRunOnJS(
|
|
60
|
+
(pending: VinCandidate[], ts: number) => {
|
|
61
|
+
pendingTextTimestampRef.current = ts;
|
|
62
|
+
pendingTextRef.current = pending;
|
|
63
|
+
setPendingTextCandidates(pending);
|
|
64
|
+
if (pendingTimerRef.current) {
|
|
65
|
+
clearTimeout(pendingTimerRef.current);
|
|
66
|
+
}
|
|
67
|
+
const ttl = resolvedOptions.text.pendingTtlMs;
|
|
68
|
+
if (ttl > 0) {
|
|
69
|
+
pendingTimerRef.current = setTimeout(() => {
|
|
70
|
+
pendingTextTimestampRef.current = null;
|
|
71
|
+
pendingTextRef.current = [];
|
|
72
|
+
setPendingTextCandidates([]);
|
|
73
|
+
}, ttl);
|
|
74
|
+
}
|
|
75
|
+
if (typeof options?.onTextPending === 'function') {
|
|
76
|
+
options.onTextPending(pending);
|
|
77
|
+
}
|
|
78
|
+
if (hapticsEnabled) {
|
|
79
|
+
triggerSoftHaptic();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
[options?.onTextPending, resolvedOptions.text.pendingTtlMs, hapticsEnabled]
|
|
83
|
+
);
|
|
84
|
+
|
|
47
85
|
const frameProcessor = useFrameProcessor(
|
|
48
86
|
(frame: Frame) => {
|
|
49
87
|
'worklet';
|
|
@@ -65,6 +103,52 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
65
103
|
resolvedOptions.detection.forceOrientation ?? null;
|
|
66
104
|
const regionOverride = resolvedOptions.detection.scanRegion ?? null;
|
|
67
105
|
|
|
106
|
+
// Frame quality gate (luma + sharpness)
|
|
107
|
+
const minLuma = resolvedOptions.detection.minLuma;
|
|
108
|
+
const minSharpness = resolvedOptions.detection.minSharpness;
|
|
109
|
+
if (minLuma > 0 || minSharpness > 0) {
|
|
110
|
+
const planes = (frame as any)?.planes;
|
|
111
|
+
if (planes?.length > 0) {
|
|
112
|
+
const yPlane = planes[0];
|
|
113
|
+
const bytes: Uint8Array | undefined = yPlane?.bytes;
|
|
114
|
+
const width: number | undefined = yPlane?.width;
|
|
115
|
+
const height: number | undefined = yPlane?.height;
|
|
116
|
+
const stride: number | undefined = yPlane?.bytesPerRow;
|
|
117
|
+
if (bytes && width && height && stride) {
|
|
118
|
+
let lumaSum = 0;
|
|
119
|
+
const sampleStep = 8;
|
|
120
|
+
const sampleCount = Math.floor(bytes.length / sampleStep);
|
|
121
|
+
for (let i = 0; i < bytes.length; i += sampleStep) {
|
|
122
|
+
lumaSum += bytes[i]!;
|
|
123
|
+
}
|
|
124
|
+
const meanLuma = lumaSum / Math.max(1, sampleCount);
|
|
125
|
+
if (minLuma > 0 && meanLuma < minLuma) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (minSharpness > 0) {
|
|
130
|
+
const step = Math.max(2, Math.floor(width / 96));
|
|
131
|
+
let sharpAccum = 0;
|
|
132
|
+
let sharpCount = 0;
|
|
133
|
+
for (let y = step; y < height - step; y += step) {
|
|
134
|
+
const row = y * stride;
|
|
135
|
+
for (let x = step; x < width - step; x += step) {
|
|
136
|
+
const idx = row + x;
|
|
137
|
+
const gx = bytes[idx + 1]! - bytes[idx - 1]!;
|
|
138
|
+
const gy = bytes[idx + stride]! - bytes[idx - stride]!;
|
|
139
|
+
sharpAccum += Math.abs(gx) + Math.abs(gy);
|
|
140
|
+
sharpCount += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const sharpness = sharpAccum / Math.max(1, sharpCount);
|
|
144
|
+
if (sharpness < minSharpness) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
68
152
|
const barcodeArgs = orientationOverride || regionOverride
|
|
69
153
|
? {
|
|
70
154
|
...(orientationOverride && { orientation: orientationOverride }),
|
|
@@ -76,7 +160,12 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
76
160
|
const t0 = Date.now();
|
|
77
161
|
|
|
78
162
|
const barcodes = barcodeScanner
|
|
79
|
-
? barcodeScanner.scanBarcodes(
|
|
163
|
+
? barcodeScanner.scanBarcodes(
|
|
164
|
+
frame,
|
|
165
|
+
useBarcodeFallback.current
|
|
166
|
+
? { all: true }
|
|
167
|
+
: barcodeArgs
|
|
168
|
+
) ?? []
|
|
80
169
|
: [];
|
|
81
170
|
|
|
82
171
|
const t1 = Date.now();
|
|
@@ -96,11 +185,21 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
96
185
|
textScanner &&
|
|
97
186
|
(textScanInterval <= 1 || frameIndex % textScanInterval === 0);
|
|
98
187
|
|
|
99
|
-
|
|
188
|
+
let textBlocks =
|
|
100
189
|
shouldRunText && textScanner
|
|
101
|
-
? textScanner.scanText(frame, textArgs)
|
|
190
|
+
? textScanner.scanText(frame, textArgs) ?? []
|
|
102
191
|
: [];
|
|
103
192
|
|
|
193
|
+
// Two-pass OCR: if we skipped this frame or got nothing and barcodes are empty, run one immediate OCR pass.
|
|
194
|
+
if (
|
|
195
|
+
textScanner &&
|
|
196
|
+
textBlocks.length === 0 &&
|
|
197
|
+
barcodes.length === 0 &&
|
|
198
|
+
!shouldRunText
|
|
199
|
+
) {
|
|
200
|
+
textBlocks = textScanner.scanText(frame, textArgs) ?? [];
|
|
201
|
+
}
|
|
202
|
+
|
|
104
203
|
const t2 = Date.now();
|
|
105
204
|
|
|
106
205
|
const payload = {
|
|
@@ -110,6 +209,20 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
110
209
|
};
|
|
111
210
|
|
|
112
211
|
const candidates = buildVinCandidates(payload, resolvedOptions);
|
|
212
|
+
const filtered = candidates.filter((c) => c.confidence >= resolvedOptions.detection.minConfidence);
|
|
213
|
+
const barcodeCandidates = filtered.filter((c) => c.source === 'barcode');
|
|
214
|
+
const textCandidates = filtered.filter((c) => c.source === 'text');
|
|
215
|
+
|
|
216
|
+
// Adaptive barcode fallback: if no barcode hits for N frames, scan all formats.
|
|
217
|
+
if (barcodeCandidates.length === 0) {
|
|
218
|
+
noBarcodeFrameCount.current += 1;
|
|
219
|
+
if (noBarcodeFrameCount.current >= resolvedOptions.detection.barcodeFallbackAfter) {
|
|
220
|
+
useBarcodeFallback.current = true;
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
noBarcodeFrameCount.current = 0;
|
|
224
|
+
useBarcodeFallback.current = false;
|
|
225
|
+
}
|
|
113
226
|
const firstCandidate = pickFirstCandidate(candidates, resolvedOptions);
|
|
114
227
|
|
|
115
228
|
const t3 = Date.now();
|
|
@@ -133,22 +246,89 @@ export function useVinScanner(options?: VinScannerOptions) {
|
|
|
133
246
|
},
|
|
134
247
|
};
|
|
135
248
|
|
|
136
|
-
//
|
|
249
|
+
// If a barcode is present, emit immediately (highest confidence path)
|
|
250
|
+
if (barcodeCandidates.length > 0) {
|
|
251
|
+
const barcodeFirst = pickFirstCandidate(barcodeCandidates, resolvedOptions);
|
|
252
|
+
if (barcodeFirst) {
|
|
253
|
+
const debounceMs = resolvedOptions.duplicateDebounceMs ?? 1500;
|
|
254
|
+
const isDuplicate =
|
|
255
|
+
barcodeFirst.value === lastEmittedVin.current &&
|
|
256
|
+
now - lastEmitTimestamp.current < debounceMs;
|
|
257
|
+
|
|
258
|
+
if (!isDuplicate) {
|
|
259
|
+
lastEmittedVin.current = barcodeFirst.value;
|
|
260
|
+
lastEmitTimestamp.current = now;
|
|
261
|
+
sessionSeen.current.add(barcodeFirst.value);
|
|
262
|
+
emitResult(barcodeCandidates, {
|
|
263
|
+
...event,
|
|
264
|
+
candidates: barcodeCandidates,
|
|
265
|
+
firstCandidate: barcodeFirst,
|
|
266
|
+
});
|
|
267
|
+
if (hapticsEnabled) {
|
|
268
|
+
triggerSuccessHaptic();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// For text-only detections, optionally require user confirmation
|
|
276
|
+
if (resolvedOptions.text.requireConfirmation && textCandidates.length > 0) {
|
|
277
|
+
emitTextPending(textCandidates, payload.timestamp);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Default behavior: emit first candidate (text)
|
|
137
282
|
if (firstCandidate) {
|
|
138
283
|
const debounceMs = resolvedOptions.duplicateDebounceMs ?? 1500;
|
|
139
284
|
const isDuplicate =
|
|
140
285
|
firstCandidate.value === lastEmittedVin.current &&
|
|
141
286
|
now - lastEmitTimestamp.current < debounceMs;
|
|
287
|
+
const seen = sessionSeen.current.has(firstCandidate.value);
|
|
142
288
|
|
|
143
|
-
if (!isDuplicate) {
|
|
289
|
+
if (!isDuplicate && !seen) {
|
|
144
290
|
lastEmittedVin.current = firstCandidate.value;
|
|
145
291
|
lastEmitTimestamp.current = now;
|
|
292
|
+
sessionSeen.current.add(firstCandidate.value);
|
|
146
293
|
emitResult(candidates, event);
|
|
147
294
|
}
|
|
148
295
|
}
|
|
149
296
|
},
|
|
150
|
-
[barcodeScanner, textScanner, emitResult, resolvedOptions]
|
|
297
|
+
[barcodeScanner, textScanner, emitResult, emitTextPending, resolvedOptions]
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const confirmTextCandidate = useCallback(
|
|
301
|
+
(vinValue?: string) => {
|
|
302
|
+
const pending = pendingTextRef.current;
|
|
303
|
+
if (!pending.length) return;
|
|
304
|
+
const selected =
|
|
305
|
+
(vinValue && pending.find((c) => c.value === vinValue)) || pending[0];
|
|
306
|
+
|
|
307
|
+
const ts = pendingTextTimestampRef.current ?? Date.now();
|
|
308
|
+
|
|
309
|
+
// Clear pending state
|
|
310
|
+
if (pendingTimerRef.current) {
|
|
311
|
+
clearTimeout(pendingTimerRef.current);
|
|
312
|
+
pendingTimerRef.current = null;
|
|
313
|
+
}
|
|
314
|
+
pendingTextTimestampRef.current = null;
|
|
315
|
+
pendingTextRef.current = [];
|
|
316
|
+
setPendingTextCandidates([]);
|
|
317
|
+
|
|
318
|
+
const event: VinScannerEvent = {
|
|
319
|
+
timestamp: ts,
|
|
320
|
+
duration: 0,
|
|
321
|
+
candidates: pending,
|
|
322
|
+
firstCandidate: selected,
|
|
323
|
+
raw: { barcodes: [], textBlocks: [] },
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
emitResult(pending, event);
|
|
327
|
+
lastEmittedVin.current = selected?.value ?? null;
|
|
328
|
+
lastEmitTimestamp.current = Date.now();
|
|
329
|
+
},
|
|
330
|
+
[emitResult]
|
|
151
331
|
);
|
|
152
332
|
|
|
153
|
-
return { frameProcessor };
|
|
333
|
+
return { frameProcessor, pendingTextCandidates, confirmTextCandidate };
|
|
154
334
|
}
|
package/src/vinUtils.ts
CHANGED
|
@@ -40,13 +40,18 @@ export type ResolvedVinScannerOptions = {
|
|
|
40
40
|
enabled: boolean;
|
|
41
41
|
language: TextRecognitionLanguage;
|
|
42
42
|
validationPattern?: string;
|
|
43
|
+
requireConfirmation: boolean;
|
|
44
|
+
pendingTtlMs: number;
|
|
43
45
|
};
|
|
44
|
-
detection: Required<DetectionOptions> & {
|
|
46
|
+
detection: Omit<Required<DetectionOptions>, 'enableFrameQualityCheck'> & {
|
|
45
47
|
textScanInterval: number;
|
|
46
48
|
maxFrameRate: number;
|
|
47
49
|
forceOrientation: FrameOrientation | null;
|
|
48
50
|
scanRegion: ScanRegion | null;
|
|
49
|
-
|
|
51
|
+
minLuma: number;
|
|
52
|
+
minSharpness: number;
|
|
53
|
+
minConfidence: number;
|
|
54
|
+
barcodeFallbackAfter: number;
|
|
50
55
|
};
|
|
51
56
|
showOverlay: boolean;
|
|
52
57
|
overlayColors: OverlayColors;
|
|
@@ -56,18 +61,23 @@ export type ResolvedVinScannerOptions = {
|
|
|
56
61
|
const DEFAULT_RESOLVED_OPTIONS: ResolvedVinScannerOptions = {
|
|
57
62
|
barcode: {
|
|
58
63
|
enabled: true,
|
|
59
|
-
formats: ['
|
|
64
|
+
formats: ['code-39', 'code-128', 'pdf-417'],
|
|
60
65
|
},
|
|
61
66
|
text: {
|
|
62
67
|
enabled: true,
|
|
63
68
|
language: 'latin',
|
|
69
|
+
requireConfirmation: false,
|
|
70
|
+
pendingTtlMs: 5000,
|
|
64
71
|
},
|
|
65
72
|
detection: {
|
|
66
73
|
textScanInterval: 3,
|
|
67
74
|
maxFrameRate: 30,
|
|
68
75
|
forceOrientation: null,
|
|
69
76
|
scanRegion: { x: 0.15, y: 0.15, width: 0.7, height: 0.7 },
|
|
70
|
-
|
|
77
|
+
minLuma: 30,
|
|
78
|
+
minSharpness: 12,
|
|
79
|
+
minConfidence: 0.6,
|
|
80
|
+
barcodeFallbackAfter: 45,
|
|
71
81
|
},
|
|
72
82
|
showOverlay: false,
|
|
73
83
|
overlayColors: {
|
|
@@ -101,6 +111,11 @@ export const resolveOptions = (
|
|
|
101
111
|
enabled: options?.text?.enabled ?? DEFAULT_RESOLVED_OPTIONS.text.enabled,
|
|
102
112
|
language:
|
|
103
113
|
options?.text?.language ?? DEFAULT_RESOLVED_OPTIONS.text.language,
|
|
114
|
+
requireConfirmation:
|
|
115
|
+
options?.text?.requireConfirmation ??
|
|
116
|
+
DEFAULT_RESOLVED_OPTIONS.text.requireConfirmation,
|
|
117
|
+
pendingTtlMs:
|
|
118
|
+
options?.text?.pendingTtlMs ?? DEFAULT_RESOLVED_OPTIONS.text.pendingTtlMs,
|
|
104
119
|
},
|
|
105
120
|
detection: {
|
|
106
121
|
textScanInterval,
|
|
@@ -111,8 +126,14 @@ export const resolveOptions = (
|
|
|
111
126
|
options?.detection?.forceOrientation ??
|
|
112
127
|
DEFAULT_RESOLVED_OPTIONS.detection.forceOrientation,
|
|
113
128
|
scanRegion: options?.detection?.scanRegion ?? DEFAULT_RESOLVED_OPTIONS.detection.scanRegion,
|
|
114
|
-
|
|
115
|
-
options?.detection?.
|
|
129
|
+
minLuma:
|
|
130
|
+
options?.detection?.minLuma ?? DEFAULT_RESOLVED_OPTIONS.detection.minLuma,
|
|
131
|
+
minSharpness:
|
|
132
|
+
options?.detection?.minSharpness ?? DEFAULT_RESOLVED_OPTIONS.detection.minSharpness,
|
|
133
|
+
minConfidence:
|
|
134
|
+
options?.detection?.minConfidence ?? DEFAULT_RESOLVED_OPTIONS.detection.minConfidence,
|
|
135
|
+
barcodeFallbackAfter:
|
|
136
|
+
options?.detection?.barcodeFallbackAfter ?? DEFAULT_RESOLVED_OPTIONS.detection.barcodeFallbackAfter,
|
|
116
137
|
},
|
|
117
138
|
showOverlay: options?.showOverlay ?? false,
|
|
118
139
|
overlayColors: {
|
|
@@ -431,17 +452,13 @@ const isValidWmi = (value: string): boolean => {
|
|
|
431
452
|
return false;
|
|
432
453
|
}
|
|
433
454
|
const firstChar = value[0];
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
firstChar === '5' ||
|
|
442
|
-
firstChar === 'J' ||
|
|
443
|
-
firstChar === 'W'
|
|
444
|
-
);
|
|
455
|
+
if (!firstChar) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Allow any WMI that starts with a VIN-valid character (A–Z minus I/O/Q, or 1–9).
|
|
460
|
+
// This keeps validation global while still rejecting impossible leading chars.
|
|
461
|
+
return /^[A-HJ-NPR-Z1-9]$/.test(firstChar);
|
|
445
462
|
};
|
|
446
463
|
|
|
447
464
|
/**
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.VinScannerOverlay = VinScannerOverlay;
|
|
7
|
-
var _react = _interopRequireDefault(require("react"));
|
|
8
|
-
var _reactNative = require("react-native");
|
|
9
|
-
var _reactNativeSkia = require("@shopify/react-native-skia");
|
|
10
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
-
const DEFAULT_COLORS = {
|
|
12
|
-
high: '#00FF00',
|
|
13
|
-
medium: '#FFFF00',
|
|
14
|
-
low: '#FF0000'
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* AR overlay component that renders bounding boxes around detected VINs.
|
|
19
|
-
* Color-coded by confidence score: green (>0.8), yellow (0.5-0.8), red (<0.5).
|
|
20
|
-
*
|
|
21
|
-
* Requires @shopify/react-native-skia peer dependency.
|
|
22
|
-
*/
|
|
23
|
-
function VinScannerOverlay({
|
|
24
|
-
candidates,
|
|
25
|
-
colors
|
|
26
|
-
}) {
|
|
27
|
-
const resolvedColors = {
|
|
28
|
-
...DEFAULT_COLORS,
|
|
29
|
-
...colors
|
|
30
|
-
};
|
|
31
|
-
const getColor = confidence => {
|
|
32
|
-
if (confidence > 0.8) return resolvedColors.high;
|
|
33
|
-
if (confidence >= 0.5) return resolvedColors.medium;
|
|
34
|
-
return resolvedColors.low;
|
|
35
|
-
};
|
|
36
|
-
return /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Canvas, {
|
|
37
|
-
style: _reactNative.StyleSheet.absoluteFill,
|
|
38
|
-
pointerEvents: "none"
|
|
39
|
-
}, candidates.map((candidate, index) => {
|
|
40
|
-
if (!candidate.boundingBox) return null;
|
|
41
|
-
const {
|
|
42
|
-
left,
|
|
43
|
-
top,
|
|
44
|
-
width,
|
|
45
|
-
height
|
|
46
|
-
} = candidate.boundingBox;
|
|
47
|
-
const color = getColor(candidate.confidence);
|
|
48
|
-
return /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Rect, {
|
|
49
|
-
key: `${candidate.value}-${index}`,
|
|
50
|
-
x: left,
|
|
51
|
-
y: top,
|
|
52
|
-
width: width || 0,
|
|
53
|
-
height: height || 0,
|
|
54
|
-
color: color,
|
|
55
|
-
style: "stroke",
|
|
56
|
-
strokeWidth: 3
|
|
57
|
-
});
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
//# sourceMappingURL=VinScannerOverlay.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","_reactNativeSkia","e","__esModule","default","DEFAULT_COLORS","high","medium","low","VinScannerOverlay","candidates","colors","resolvedColors","getColor","confidence","createElement","Canvas","style","StyleSheet","absoluteFill","pointerEvents","map","candidate","index","boundingBox","left","top","width","height","color","Rect","key","value","x","y","strokeWidth"],"sourceRoot":"../../src","sources":["VinScannerOverlay.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,gBAAA,GAAAF,OAAA;AAA0D,SAAAD,uBAAAI,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAQ1D,MAAMG,cAA6B,GAAG;EAClCC,IAAI,EAAE,SAAS;EACfC,MAAM,EAAE,SAAS;EACjBC,GAAG,EAAE;AACT,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,iBAAiBA,CAAC;EAAEC,UAAU;EAAEC;AAA+B,CAAC,EAAE;EAC9E,MAAMC,cAAc,GAAG;IAAE,GAAGP,cAAc;IAAE,GAAGM;EAAO,CAAC;EAEvD,MAAME,QAAQ,GAAIC,UAAkB,IAAa;IAC7C,IAAIA,UAAU,GAAG,GAAG,EAAE,OAAOF,cAAc,CAACN,IAAI;IAChD,IAAIQ,UAAU,IAAI,GAAG,EAAE,OAAOF,cAAc,CAACL,MAAM;IACnD,OAAOK,cAAc,CAACJ,GAAG;EAC7B,CAAC;EAED,oBACIX,MAAA,CAAAO,OAAA,CAAAW,aAAA,CAACd,gBAAA,CAAAe,MAAM;IAACC,KAAK,EAAEC,uBAAU,CAACC,YAAa;IAACC,aAAa,EAAC;EAAM,GACvDV,UAAU,CAACW,GAAG,CAAC,CAACC,SAAS,EAAEC,KAAK,KAAK;IAClC,IAAI,CAACD,SAAS,CAACE,WAAW,EAAE,OAAO,IAAI;IAEvC,MAAM;MAAEC,IAAI;MAAEC,GAAG;MAAEC,KAAK;MAAEC;IAAO,CAAC,GAAGN,SAAS,CAACE,WAAW;IAC1D,MAAMK,KAAK,GAAGhB,QAAQ,CAACS,SAAS,CAACR,UAAU,CAAC;IAE5C,oBACIjB,MAAA,CAAAO,OAAA,CAAAW,aAAA,CAACd,gBAAA,CAAA6B,IAAI;MACDC,GAAG,EAAE,GAAGT,SAAS,CAACU,KAAK,IAAIT,KAAK,EAAG;MACnCU,CAAC,EAAER,IAAK;MACRS,CAAC,EAAER,GAAI;MACPC,KAAK,EAAEA,KAAK,IAAI,CAAE;MAClBC,MAAM,EAAEA,MAAM,IAAI,CAAE;MACpBC,KAAK,EAAEA,KAAM;MACbZ,KAAK,EAAC,QAAQ;MACdkB,WAAW,EAAE;IAAE,CAClB,CAAC;EAEV,CAAC,CACG,CAAC;AAEjB","ignoreList":[]}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { StyleSheet } from 'react-native';
|
|
3
|
-
import { Canvas, Rect } from '@shopify/react-native-skia';
|
|
4
|
-
const DEFAULT_COLORS = {
|
|
5
|
-
high: '#00FF00',
|
|
6
|
-
medium: '#FFFF00',
|
|
7
|
-
low: '#FF0000'
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* AR overlay component that renders bounding boxes around detected VINs.
|
|
12
|
-
* Color-coded by confidence score: green (>0.8), yellow (0.5-0.8), red (<0.5).
|
|
13
|
-
*
|
|
14
|
-
* Requires @shopify/react-native-skia peer dependency.
|
|
15
|
-
*/
|
|
16
|
-
export function VinScannerOverlay({
|
|
17
|
-
candidates,
|
|
18
|
-
colors
|
|
19
|
-
}) {
|
|
20
|
-
const resolvedColors = {
|
|
21
|
-
...DEFAULT_COLORS,
|
|
22
|
-
...colors
|
|
23
|
-
};
|
|
24
|
-
const getColor = confidence => {
|
|
25
|
-
if (confidence > 0.8) return resolvedColors.high;
|
|
26
|
-
if (confidence >= 0.5) return resolvedColors.medium;
|
|
27
|
-
return resolvedColors.low;
|
|
28
|
-
};
|
|
29
|
-
return /*#__PURE__*/React.createElement(Canvas, {
|
|
30
|
-
style: StyleSheet.absoluteFill,
|
|
31
|
-
pointerEvents: "none"
|
|
32
|
-
}, candidates.map((candidate, index) => {
|
|
33
|
-
if (!candidate.boundingBox) return null;
|
|
34
|
-
const {
|
|
35
|
-
left,
|
|
36
|
-
top,
|
|
37
|
-
width,
|
|
38
|
-
height
|
|
39
|
-
} = candidate.boundingBox;
|
|
40
|
-
const color = getColor(candidate.confidence);
|
|
41
|
-
return /*#__PURE__*/React.createElement(Rect, {
|
|
42
|
-
key: `${candidate.value}-${index}`,
|
|
43
|
-
x: left,
|
|
44
|
-
y: top,
|
|
45
|
-
width: width || 0,
|
|
46
|
-
height: height || 0,
|
|
47
|
-
color: color,
|
|
48
|
-
style: "stroke",
|
|
49
|
-
strokeWidth: 3
|
|
50
|
-
});
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
53
|
-
//# sourceMappingURL=VinScannerOverlay.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["React","StyleSheet","Canvas","Rect","DEFAULT_COLORS","high","medium","low","VinScannerOverlay","candidates","colors","resolvedColors","getColor","confidence","createElement","style","absoluteFill","pointerEvents","map","candidate","index","boundingBox","left","top","width","height","color","key","value","x","y","strokeWidth"],"sourceRoot":"../../src","sources":["VinScannerOverlay.tsx"],"mappings":"AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,UAAU,QAAQ,cAAc;AACzC,SAASC,MAAM,EAAEC,IAAI,QAAQ,4BAA4B;AAQzD,MAAMC,cAA6B,GAAG;EAClCC,IAAI,EAAE,SAAS;EACfC,MAAM,EAAE,SAAS;EACjBC,GAAG,EAAE;AACT,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAAC;EAAEC,UAAU;EAAEC;AAA+B,CAAC,EAAE;EAC9E,MAAMC,cAAc,GAAG;IAAE,GAAGP,cAAc;IAAE,GAAGM;EAAO,CAAC;EAEvD,MAAME,QAAQ,GAAIC,UAAkB,IAAa;IAC7C,IAAIA,UAAU,GAAG,GAAG,EAAE,OAAOF,cAAc,CAACN,IAAI;IAChD,IAAIQ,UAAU,IAAI,GAAG,EAAE,OAAOF,cAAc,CAACL,MAAM;IACnD,OAAOK,cAAc,CAACJ,GAAG;EAC7B,CAAC;EAED,oBACIP,KAAA,CAAAc,aAAA,CAACZ,MAAM;IAACa,KAAK,EAAEd,UAAU,CAACe,YAAa;IAACC,aAAa,EAAC;EAAM,GACvDR,UAAU,CAACS,GAAG,CAAC,CAACC,SAAS,EAAEC,KAAK,KAAK;IAClC,IAAI,CAACD,SAAS,CAACE,WAAW,EAAE,OAAO,IAAI;IAEvC,MAAM;MAAEC,IAAI;MAAEC,GAAG;MAAEC,KAAK;MAAEC;IAAO,CAAC,GAAGN,SAAS,CAACE,WAAW;IAC1D,MAAMK,KAAK,GAAGd,QAAQ,CAACO,SAAS,CAACN,UAAU,CAAC;IAE5C,oBACIb,KAAA,CAAAc,aAAA,CAACX,IAAI;MACDwB,GAAG,EAAE,GAAGR,SAAS,CAACS,KAAK,IAAIR,KAAK,EAAG;MACnCS,CAAC,EAAEP,IAAK;MACRQ,CAAC,EAAEP,GAAI;MACPC,KAAK,EAAEA,KAAK,IAAI,CAAE;MAClBC,MAAM,EAAEA,MAAM,IAAI,CAAE;MACpBC,KAAK,EAAEA,KAAM;MACbX,KAAK,EAAC,QAAQ;MACdgB,WAAW,EAAE;IAAE,CAClB,CAAC;EAEV,CAAC,CACG,CAAC;AAEjB","ignoreList":[]}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { VinCandidate, OverlayColors } from './types';
|
|
3
|
-
export type VinScannerOverlayProps = {
|
|
4
|
-
candidates: VinCandidate[];
|
|
5
|
-
colors?: Partial<OverlayColors>;
|
|
6
|
-
};
|
|
7
|
-
/**
|
|
8
|
-
* AR overlay component that renders bounding boxes around detected VINs.
|
|
9
|
-
* Color-coded by confidence score: green (>0.8), yellow (0.5-0.8), red (<0.5).
|
|
10
|
-
*
|
|
11
|
-
* Requires @shopify/react-native-skia peer dependency.
|
|
12
|
-
*/
|
|
13
|
-
export declare function VinScannerOverlay({ candidates, colors }: VinScannerOverlayProps): React.JSX.Element;
|
|
14
|
-
//# sourceMappingURL=VinScannerOverlay.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"VinScannerOverlay.d.ts","sourceRoot":"","sources":["../../../src/VinScannerOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE3D,MAAM,MAAM,sBAAsB,GAAG;IACjC,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CACnC,CAAC;AAQF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,sBAAsB,qBAgC/E"}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { StyleSheet } from 'react-native';
|
|
3
|
-
import { Canvas, Rect } from '@shopify/react-native-skia';
|
|
4
|
-
import type { VinCandidate, OverlayColors } from './types';
|
|
5
|
-
|
|
6
|
-
export type VinScannerOverlayProps = {
|
|
7
|
-
candidates: VinCandidate[];
|
|
8
|
-
colors?: Partial<OverlayColors>;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const DEFAULT_COLORS: OverlayColors = {
|
|
12
|
-
high: '#00FF00',
|
|
13
|
-
medium: '#FFFF00',
|
|
14
|
-
low: '#FF0000',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* AR overlay component that renders bounding boxes around detected VINs.
|
|
19
|
-
* Color-coded by confidence score: green (>0.8), yellow (0.5-0.8), red (<0.5).
|
|
20
|
-
*
|
|
21
|
-
* Requires @shopify/react-native-skia peer dependency.
|
|
22
|
-
*/
|
|
23
|
-
export function VinScannerOverlay({ candidates, colors }: VinScannerOverlayProps) {
|
|
24
|
-
const resolvedColors = { ...DEFAULT_COLORS, ...colors };
|
|
25
|
-
|
|
26
|
-
const getColor = (confidence: number): string => {
|
|
27
|
-
if (confidence > 0.8) return resolvedColors.high;
|
|
28
|
-
if (confidence >= 0.5) return resolvedColors.medium;
|
|
29
|
-
return resolvedColors.low;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<Canvas style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
34
|
-
{candidates.map((candidate, index) => {
|
|
35
|
-
if (!candidate.boundingBox) return null;
|
|
36
|
-
|
|
37
|
-
const { left, top, width, height } = candidate.boundingBox;
|
|
38
|
-
const color = getColor(candidate.confidence);
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<Rect
|
|
42
|
-
key={`${candidate.value}-${index}`}
|
|
43
|
-
x={left}
|
|
44
|
-
y={top}
|
|
45
|
-
width={width || 0}
|
|
46
|
-
height={height || 0}
|
|
47
|
-
color={color}
|
|
48
|
-
style="stroke"
|
|
49
|
-
strokeWidth={3}
|
|
50
|
-
/>
|
|
51
|
-
);
|
|
52
|
-
})}
|
|
53
|
-
</Canvas>
|
|
54
|
-
);
|
|
55
|
-
}
|