react-native-rectangle-doc-scanner 0.39.0 → 0.40.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.
@@ -42,6 +42,7 @@ const react_native_worklets_core_1 = require("react-native-worklets-core");
42
42
  const react_native_fast_opencv_1 = require("react-native-fast-opencv");
43
43
  const overlay_1 = require("./utils/overlay");
44
44
  const stability_1 = require("./utils/stability");
45
+ const quad_1 = require("./utils/quad");
45
46
  const isConvexQuadrilateral = (points) => {
46
47
  'worklet';
47
48
  try {
@@ -93,50 +94,52 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
93
94
  }, [requestPermission]);
94
95
  const lastQuadRef = (0, react_1.useRef)(null);
95
96
  const smoothingBufferRef = (0, react_1.useRef)([]);
97
+ const anchorQuadRef = (0, react_1.useRef)(null);
98
+ const missingFrameCountRef = (0, react_1.useRef)(0);
99
+ const MAX_HISTORY = 5;
100
+ const SNAP_DISTANCE = 5; // pixels; keep corners locked when movement is tiny
101
+ const BLEND_DISTANCE = 20; // pixels; softly ease between similar shapes
102
+ const BLEND_ALPHA = 0.35;
96
103
  const updateQuad = (0, react_native_worklets_core_1.useRunOnJS)((value) => {
97
104
  if (__DEV__) {
98
105
  console.log('[DocScanner] quad', value);
99
106
  }
100
- // Add to smoothing buffer
101
- smoothingBufferRef.current.push(value);
102
- // Keep only last 3 frames for smoothing
103
- if (smoothingBufferRef.current.length > 3) {
104
- smoothingBufferRef.current.shift();
105
- }
106
- // If we have a valid quad, smooth it
107
- if (value && value.length === 4) {
108
- // Average with previous frames if available
109
- if (smoothingBufferRef.current.length >= 2) {
110
- const validQuads = smoothingBufferRef.current.filter(q => q !== null && q.length === 4);
111
- if (validQuads.length >= 2) {
112
- // Average the positions
113
- const smoothed = value.map((_, idx) => {
114
- let sumX = 0;
115
- let sumY = 0;
116
- validQuads.forEach(quad => {
117
- sumX += quad[idx].x;
118
- sumY += quad[idx].y;
119
- });
120
- return {
121
- x: sumX / validQuads.length,
122
- y: sumY / validQuads.length,
123
- };
124
- });
125
- lastQuadRef.current = smoothed;
126
- setQuad(smoothed);
127
- return;
128
- }
107
+ if (!(0, quad_1.isValidQuad)(value)) {
108
+ missingFrameCountRef.current += 1;
109
+ if (lastQuadRef.current && missingFrameCountRef.current <= 2) {
110
+ setQuad(lastQuadRef.current);
111
+ return;
129
112
  }
130
- lastQuadRef.current = value;
131
- setQuad(value);
113
+ smoothingBufferRef.current = [];
114
+ anchorQuadRef.current = null;
115
+ lastQuadRef.current = null;
116
+ setQuad(null);
117
+ return;
132
118
  }
133
- else if (lastQuadRef.current) {
134
- // Keep showing last quad for 1 frame to reduce flickering
135
- setQuad(lastQuadRef.current);
119
+ missingFrameCountRef.current = 0;
120
+ const ordered = (0, quad_1.orderQuadPoints)(value);
121
+ const sanitized = (0, quad_1.sanitizeQuad)(ordered);
122
+ smoothingBufferRef.current.push(sanitized);
123
+ if (smoothingBufferRef.current.length > MAX_HISTORY) {
124
+ smoothingBufferRef.current.shift();
136
125
  }
137
- else {
138
- setQuad(null);
126
+ const history = smoothingBufferRef.current;
127
+ const hasHistory = history.length >= 2;
128
+ let candidate = hasHistory ? (0, quad_1.averageQuad)(history) : sanitized;
129
+ const anchor = anchorQuadRef.current;
130
+ if (anchor && (0, quad_1.isValidQuad)(anchor)) {
131
+ const delta = (0, quad_1.quadDistance)(candidate, anchor);
132
+ if (delta <= SNAP_DISTANCE) {
133
+ candidate = anchor;
134
+ }
135
+ else if (delta <= BLEND_DISTANCE) {
136
+ candidate = (0, quad_1.blendQuads)(anchor, candidate, BLEND_ALPHA);
137
+ }
139
138
  }
139
+ candidate = (0, quad_1.orderQuadPoints)(candidate);
140
+ anchorQuadRef.current = candidate;
141
+ lastQuadRef.current = candidate;
142
+ setQuad(candidate);
140
143
  }, []);
141
144
  const reportError = (0, react_native_worklets_core_1.useRunOnJS)((step, error) => {
142
145
  const message = error instanceof Error ? error.message : `${error}`;
@@ -0,0 +1,8 @@
1
+ import type { Point } from '../types';
2
+ export declare const isValidPoint: (point: Point | null | undefined) => point is Point;
3
+ export declare const isValidQuad: (quad: Point[] | null | undefined) => quad is Point[];
4
+ export declare const orderQuadPoints: (quad: Point[]) => Point[];
5
+ export declare const quadDistance: (a: Point[], b: Point[]) => number;
6
+ export declare const averageQuad: (quads: Point[][]) => Point[];
7
+ export declare const blendQuads: (base: Point[], target: Point[], alpha: number) => Point[];
8
+ export declare const sanitizeQuad: (quad: Point[]) => Point[];
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeQuad = exports.blendQuads = exports.averageQuad = exports.quadDistance = exports.orderQuadPoints = exports.isValidQuad = exports.isValidPoint = void 0;
4
+ const POINT_EPSILON = 1e-3;
5
+ const isFiniteNumber = (value) => Number.isFinite(value) && !Number.isNaN(value);
6
+ const isValidPoint = (point) => {
7
+ if (!point) {
8
+ return false;
9
+ }
10
+ if (!isFiniteNumber(point.x) || !isFiniteNumber(point.y)) {
11
+ return false;
12
+ }
13
+ if (Math.abs(point.x) > 1_000_000 || Math.abs(point.y) > 1_000_000) {
14
+ return false;
15
+ }
16
+ return true;
17
+ };
18
+ exports.isValidPoint = isValidPoint;
19
+ const isValidQuad = (quad) => {
20
+ return Array.isArray(quad) && quad.length === 4 && quad.every(exports.isValidPoint);
21
+ };
22
+ exports.isValidQuad = isValidQuad;
23
+ const clonePoint = (point) => ({ x: point.x, y: point.y });
24
+ const orderQuadPoints = (quad) => {
25
+ const centroid = quad.reduce((acc, point) => ({ x: acc.x + point.x / quad.length, y: acc.y + point.y / quad.length }), { x: 0, y: 0 });
26
+ const sorted = quad
27
+ .slice()
28
+ .map(clonePoint)
29
+ .sort((a, b) => {
30
+ const angleA = Math.atan2(a.y - centroid.y, a.x - centroid.x);
31
+ const angleB = Math.atan2(b.y - centroid.y, b.x - centroid.x);
32
+ return angleA - angleB;
33
+ });
34
+ const topLeftIndex = sorted.reduce((selectedIndex, point, index) => {
35
+ const currentScore = point.x + point.y;
36
+ const selectedPoint = sorted[selectedIndex];
37
+ const selectedScore = selectedPoint.x + selectedPoint.y;
38
+ if (currentScore < selectedScore - POINT_EPSILON) {
39
+ return index;
40
+ }
41
+ return selectedIndex;
42
+ }, 0);
43
+ return [...sorted.slice(topLeftIndex), ...sorted.slice(0, topLeftIndex)];
44
+ };
45
+ exports.orderQuadPoints = orderQuadPoints;
46
+ const quadDistance = (a, b) => {
47
+ if (!(0, exports.isValidQuad)(a) || !(0, exports.isValidQuad)(b)) {
48
+ return Number.POSITIVE_INFINITY;
49
+ }
50
+ let total = 0;
51
+ for (let i = 0; i < 4; i += 1) {
52
+ total += Math.hypot(a[i].x - b[i].x, a[i].y - b[i].y);
53
+ }
54
+ return total / 4;
55
+ };
56
+ exports.quadDistance = quadDistance;
57
+ const averageQuad = (quads) => {
58
+ if (!Array.isArray(quads) || quads.length === 0) {
59
+ throw new Error('Cannot average empty quad array');
60
+ }
61
+ const accum = quads[0].map(() => ({ x: 0, y: 0 }));
62
+ quads.forEach((quad) => {
63
+ quad.forEach((point, index) => {
64
+ accum[index].x += point.x;
65
+ accum[index].y += point.y;
66
+ });
67
+ });
68
+ return accum.map((point) => ({ x: point.x / quads.length, y: point.y / quads.length }));
69
+ };
70
+ exports.averageQuad = averageQuad;
71
+ const blendQuads = (base, target, alpha) => {
72
+ if (alpha <= 0) {
73
+ return base.map(clonePoint);
74
+ }
75
+ if (alpha >= 1) {
76
+ return target.map(clonePoint);
77
+ }
78
+ return base.map((point, index) => ({
79
+ x: point.x * (1 - alpha) + target[index].x * alpha,
80
+ y: point.y * (1 - alpha) + target[index].y * alpha,
81
+ }));
82
+ };
83
+ exports.blendQuads = blendQuads;
84
+ const sanitizeQuad = (quad) => {
85
+ if (!(0, exports.isValidQuad)(quad)) {
86
+ throw new Error('Cannot sanitise invalid quad');
87
+ }
88
+ return quad.map((point) => ({
89
+ x: Number.isFinite(point.x) ? point.x : 0,
90
+ y: Number.isFinite(point.y) ? point.y : 0,
91
+ }));
92
+ };
93
+ exports.sanitizeQuad = sanitizeQuad;
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkStability = checkStability;
4
+ const quad_1 = require("./quad");
4
5
  let last = null;
5
6
  let stable = 0;
7
+ const STABILITY_DISTANCE = 8;
6
8
  function checkStability(current) {
7
- if (!current) {
9
+ if (!(0, quad_1.isValidQuad)(current)) {
8
10
  stable = 0;
9
11
  last = null;
10
12
  return 0;
@@ -14,8 +16,8 @@ function checkStability(current) {
14
16
  stable = 1;
15
17
  return stable;
16
18
  }
17
- const diff = Math.hypot(avg(current.map((p) => p.x)) - avg(last.map((p) => p.x)), avg(current.map((p) => p.y)) - avg(last.map((p) => p.y)));
18
- if (diff < 10) {
19
+ const diff = (0, quad_1.quadDistance)(current, last);
20
+ if (diff < STABILITY_DISTANCE) {
19
21
  stable++;
20
22
  }
21
23
  else {
@@ -24,4 +26,3 @@ function checkStability(current) {
24
26
  last = current;
25
27
  return stable;
26
28
  }
27
- const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {
@@ -14,6 +14,7 @@ import {
14
14
  } from 'react-native-fast-opencv';
15
15
  import { Overlay } from './utils/overlay';
16
16
  import { checkStability } from './utils/stability';
17
+ import { averageQuad, blendQuads, isValidQuad, orderQuadPoints, quadDistance, sanitizeQuad } from './utils/quad';
17
18
  import type { Point } from './types';
18
19
 
19
20
  const isConvexQuadrilateral = (points: Point[]) => {
@@ -97,56 +98,64 @@ export const DocScanner: React.FC<Props> = ({
97
98
  }, [requestPermission]);
98
99
 
99
100
  const lastQuadRef = useRef<Point[] | null>(null);
100
- const smoothingBufferRef = useRef<(Point[] | null)[]>([]);
101
+ const smoothingBufferRef = useRef<Point[][]>([]);
102
+ const anchorQuadRef = useRef<Point[] | null>(null);
103
+ const missingFrameCountRef = useRef(0);
104
+
105
+ const MAX_HISTORY = 5;
106
+ const SNAP_DISTANCE = 5; // pixels; keep corners locked when movement is tiny
107
+ const BLEND_DISTANCE = 20; // pixels; softly ease between similar shapes
108
+ const BLEND_ALPHA = 0.35;
101
109
 
102
110
  const updateQuad = useRunOnJS((value: Point[] | null) => {
103
111
  if (__DEV__) {
104
112
  console.log('[DocScanner] quad', value);
105
113
  }
106
114
 
107
- // Add to smoothing buffer
108
- smoothingBufferRef.current.push(value);
115
+ if (!isValidQuad(value)) {
116
+ missingFrameCountRef.current += 1;
117
+
118
+ if (lastQuadRef.current && missingFrameCountRef.current <= 2) {
119
+ setQuad(lastQuadRef.current);
120
+ return;
121
+ }
122
+
123
+ smoothingBufferRef.current = [];
124
+ anchorQuadRef.current = null;
125
+ lastQuadRef.current = null;
126
+ setQuad(null);
127
+ return;
128
+ }
129
+
130
+ missingFrameCountRef.current = 0;
109
131
 
110
- // Keep only last 3 frames for smoothing
111
- if (smoothingBufferRef.current.length > 3) {
132
+ const ordered = orderQuadPoints(value);
133
+ const sanitized = sanitizeQuad(ordered);
134
+
135
+ smoothingBufferRef.current.push(sanitized);
136
+ if (smoothingBufferRef.current.length > MAX_HISTORY) {
112
137
  smoothingBufferRef.current.shift();
113
138
  }
114
139
 
115
- // If we have a valid quad, smooth it
116
- if (value && value.length === 4) {
117
- // Average with previous frames if available
118
- if (smoothingBufferRef.current.length >= 2) {
119
- const validQuads = smoothingBufferRef.current.filter(q => q !== null && q.length === 4) as Point[][];
120
-
121
- if (validQuads.length >= 2) {
122
- // Average the positions
123
- const smoothed: Point[] = value.map((_, idx) => {
124
- let sumX = 0;
125
- let sumY = 0;
126
- validQuads.forEach(quad => {
127
- sumX += quad[idx].x;
128
- sumY += quad[idx].y;
129
- });
130
- return {
131
- x: sumX / validQuads.length,
132
- y: sumY / validQuads.length,
133
- };
134
- });
135
-
136
- lastQuadRef.current = smoothed;
137
- setQuad(smoothed);
138
- return;
139
- }
140
- }
140
+ const history = smoothingBufferRef.current;
141
+ const hasHistory = history.length >= 2;
142
+ let candidate = hasHistory ? averageQuad(history) : sanitized;
141
143
 
142
- lastQuadRef.current = value;
143
- setQuad(value);
144
- } else if (lastQuadRef.current) {
145
- // Keep showing last quad for 1 frame to reduce flickering
146
- setQuad(lastQuadRef.current);
147
- } else {
148
- setQuad(null);
144
+ const anchor = anchorQuadRef.current;
145
+ if (anchor && isValidQuad(anchor)) {
146
+ const delta = quadDistance(candidate, anchor);
147
+
148
+ if (delta <= SNAP_DISTANCE) {
149
+ candidate = anchor;
150
+ } else if (delta <= BLEND_DISTANCE) {
151
+ candidate = blendQuads(anchor, candidate, BLEND_ALPHA);
152
+ }
149
153
  }
154
+
155
+ candidate = orderQuadPoints(candidate);
156
+ anchorQuadRef.current = candidate;
157
+ lastQuadRef.current = candidate;
158
+ setQuad(candidate);
150
159
  }, []);
151
160
 
152
161
  const reportError = useRunOnJS((step: string, error: unknown) => {
@@ -0,0 +1,111 @@
1
+ import type { Point } from '../types';
2
+
3
+ const POINT_EPSILON = 1e-3;
4
+
5
+ const isFiniteNumber = (value: number): boolean => Number.isFinite(value) && !Number.isNaN(value);
6
+
7
+ export const isValidPoint = (point: Point | null | undefined): point is Point => {
8
+ if (!point) {
9
+ return false;
10
+ }
11
+
12
+ if (!isFiniteNumber(point.x) || !isFiniteNumber(point.y)) {
13
+ return false;
14
+ }
15
+
16
+ if (Math.abs(point.x) > 1_000_000 || Math.abs(point.y) > 1_000_000) {
17
+ return false;
18
+ }
19
+
20
+ return true;
21
+ };
22
+
23
+ export const isValidQuad = (quad: Point[] | null | undefined): quad is Point[] => {
24
+ return Array.isArray(quad) && quad.length === 4 && quad.every(isValidPoint);
25
+ };
26
+
27
+ const clonePoint = (point: Point): Point => ({ x: point.x, y: point.y });
28
+
29
+ export const orderQuadPoints = (quad: Point[]): Point[] => {
30
+ const centroid = quad.reduce(
31
+ (acc, point) => ({ x: acc.x + point.x / quad.length, y: acc.y + point.y / quad.length }),
32
+ { x: 0, y: 0 },
33
+ );
34
+
35
+ const sorted = quad
36
+ .slice()
37
+ .map(clonePoint)
38
+ .sort((a, b) => {
39
+ const angleA = Math.atan2(a.y - centroid.y, a.x - centroid.x);
40
+ const angleB = Math.atan2(b.y - centroid.y, b.x - centroid.x);
41
+ return angleA - angleB;
42
+ });
43
+
44
+ const topLeftIndex = sorted.reduce((selectedIndex, point, index) => {
45
+ const currentScore = point.x + point.y;
46
+ const selectedPoint = sorted[selectedIndex];
47
+ const selectedScore = selectedPoint.x + selectedPoint.y;
48
+ if (currentScore < selectedScore - POINT_EPSILON) {
49
+ return index;
50
+ }
51
+ return selectedIndex;
52
+ }, 0);
53
+
54
+ return [...sorted.slice(topLeftIndex), ...sorted.slice(0, topLeftIndex)];
55
+ };
56
+
57
+ export const quadDistance = (a: Point[], b: Point[]): number => {
58
+ if (!isValidQuad(a) || !isValidQuad(b)) {
59
+ return Number.POSITIVE_INFINITY;
60
+ }
61
+
62
+ let total = 0;
63
+ for (let i = 0; i < 4; i += 1) {
64
+ total += Math.hypot(a[i].x - b[i].x, a[i].y - b[i].y);
65
+ }
66
+
67
+ return total / 4;
68
+ };
69
+
70
+ export const averageQuad = (quads: Point[][]): Point[] => {
71
+ if (!Array.isArray(quads) || quads.length === 0) {
72
+ throw new Error('Cannot average empty quad array');
73
+ }
74
+
75
+ const accum: Point[] = quads[0].map(() => ({ x: 0, y: 0 }));
76
+
77
+ quads.forEach((quad) => {
78
+ quad.forEach((point, index) => {
79
+ accum[index].x += point.x;
80
+ accum[index].y += point.y;
81
+ });
82
+ });
83
+
84
+ return accum.map((point) => ({ x: point.x / quads.length, y: point.y / quads.length }));
85
+ };
86
+
87
+ export const blendQuads = (base: Point[], target: Point[], alpha: number): Point[] => {
88
+ if (alpha <= 0) {
89
+ return base.map(clonePoint);
90
+ }
91
+
92
+ if (alpha >= 1) {
93
+ return target.map(clonePoint);
94
+ }
95
+
96
+ return base.map((point, index) => ({
97
+ x: point.x * (1 - alpha) + target[index].x * alpha,
98
+ y: point.y * (1 - alpha) + target[index].y * alpha,
99
+ }));
100
+ };
101
+
102
+ export const sanitizeQuad = (quad: Point[]): Point[] => {
103
+ if (!isValidQuad(quad)) {
104
+ throw new Error('Cannot sanitise invalid quad');
105
+ }
106
+
107
+ return quad.map((point) => ({
108
+ x: Number.isFinite(point.x) ? point.x : 0,
109
+ y: Number.isFinite(point.y) ? point.y : 0,
110
+ }));
111
+ };
@@ -1,10 +1,13 @@
1
1
  import type { Point } from '../types';
2
+ import { isValidQuad, quadDistance } from './quad';
2
3
 
3
4
  let last: Point[] | null = null;
4
5
  let stable = 0;
5
6
 
7
+ const STABILITY_DISTANCE = 8;
8
+
6
9
  export function checkStability(current: Point[] | null): number {
7
- if (!current) {
10
+ if (!isValidQuad(current)) {
8
11
  stable = 0;
9
12
  last = null;
10
13
  return 0;
@@ -16,12 +19,9 @@ export function checkStability(current: Point[] | null): number {
16
19
  return stable;
17
20
  }
18
21
 
19
- const diff = Math.hypot(
20
- avg(current.map((p) => p.x)) - avg(last.map((p) => p.x)),
21
- avg(current.map((p) => p.y)) - avg(last.map((p) => p.y))
22
- );
22
+ const diff = quadDistance(current, last);
23
23
 
24
- if (diff < 10) {
24
+ if (diff < STABILITY_DISTANCE) {
25
25
  stable++;
26
26
  } else {
27
27
  stable = 0;
@@ -30,5 +30,3 @@ export function checkStability(current: Point[] | null): number {
30
30
  last = current;
31
31
  return stable;
32
32
  }
33
-
34
- const avg = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length;