react-native-rectangle-doc-scanner 0.40.0 → 0.41.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.
@@ -96,10 +96,18 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
96
96
  const smoothingBufferRef = (0, react_1.useRef)([]);
97
97
  const anchorQuadRef = (0, react_1.useRef)(null);
98
98
  const missingFrameCountRef = (0, react_1.useRef)(0);
99
+ const lastMeasurementRef = (0, react_1.useRef)(null);
100
+ const frameSizeRef = (0, react_1.useRef)(null);
99
101
  const MAX_HISTORY = 5;
100
102
  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;
103
+ const SNAP_CENTER_DISTANCE = 12;
104
+ const BLEND_DISTANCE = 65; // pixels; softly ease between similar shapes
105
+ const MAX_CENTER_DELTA = 85;
106
+ const MAX_AREA_SHIFT = 0.45;
107
+ const HISTORY_RESET_DISTANCE = 70;
108
+ const MIN_AREA_RATIO = 0.0035;
109
+ const MAX_AREA_RATIO = 0.9;
110
+ const MIN_EDGE_RATIO = 0.025;
103
111
  const updateQuad = (0, react_native_worklets_core_1.useRunOnJS)((value) => {
104
112
  if (__DEV__) {
105
113
  console.log('[DocScanner] quad', value);
@@ -119,21 +127,63 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
119
127
  missingFrameCountRef.current = 0;
120
128
  const ordered = (0, quad_1.orderQuadPoints)(value);
121
129
  const sanitized = (0, quad_1.sanitizeQuad)(ordered);
130
+ const frameSize = frameSizeRef.current;
131
+ const frameArea = frameSize ? frameSize.width * frameSize.height : null;
132
+ const area = (0, quad_1.quadArea)(sanitized);
133
+ const edges = (0, quad_1.quadEdgeLengths)(sanitized);
134
+ const minEdge = Math.min(...edges);
135
+ const maxEdge = Math.max(...edges);
136
+ const aspectRatio = maxEdge > 0 ? maxEdge / Math.max(minEdge, 1) : 0;
137
+ const minEdgeThreshold = frameSize
138
+ ? Math.max(14, Math.min(frameSize.width, frameSize.height) * MIN_EDGE_RATIO)
139
+ : 14;
140
+ const areaTooSmall = frameArea ? area < frameArea * MIN_AREA_RATIO : area === 0;
141
+ const areaTooLarge = frameArea ? area > frameArea * MAX_AREA_RATIO : false;
142
+ const edgesTooShort = minEdge < minEdgeThreshold;
143
+ const aspectTooExtreme = aspectRatio > 7;
144
+ if (areaTooSmall || areaTooLarge || edgesTooShort || aspectTooExtreme) {
145
+ missingFrameCountRef.current += 1;
146
+ if (lastQuadRef.current && missingFrameCountRef.current <= 2) {
147
+ setQuad(lastQuadRef.current);
148
+ return;
149
+ }
150
+ smoothingBufferRef.current = [];
151
+ anchorQuadRef.current = null;
152
+ lastQuadRef.current = null;
153
+ setQuad(null);
154
+ return;
155
+ }
156
+ const lastMeasurement = lastMeasurementRef.current;
157
+ if (lastMeasurement && (0, quad_1.quadDistance)(lastMeasurement, sanitized) > HISTORY_RESET_DISTANCE) {
158
+ smoothingBufferRef.current = [];
159
+ }
160
+ lastMeasurementRef.current = sanitized;
122
161
  smoothingBufferRef.current.push(sanitized);
123
162
  if (smoothingBufferRef.current.length > MAX_HISTORY) {
124
163
  smoothingBufferRef.current.shift();
125
164
  }
126
165
  const history = smoothingBufferRef.current;
127
166
  const hasHistory = history.length >= 2;
128
- let candidate = hasHistory ? (0, quad_1.averageQuad)(history) : sanitized;
167
+ let candidate = hasHistory ? (0, quad_1.weightedAverageQuad)(history) : sanitized;
129
168
  const anchor = anchorQuadRef.current;
130
169
  if (anchor && (0, quad_1.isValidQuad)(anchor)) {
131
170
  const delta = (0, quad_1.quadDistance)(candidate, anchor);
132
- if (delta <= SNAP_DISTANCE) {
171
+ const anchorCenter = (0, quad_1.quadCenter)(anchor);
172
+ const candidateCenter = (0, quad_1.quadCenter)(candidate);
173
+ const anchorArea = (0, quad_1.quadArea)(anchor);
174
+ const candidateArea = (0, quad_1.quadArea)(candidate);
175
+ const centerDelta = Math.hypot(candidateCenter.x - anchorCenter.x, candidateCenter.y - anchorCenter.y);
176
+ const areaShift = anchorArea > 0 ? Math.abs(anchorArea - candidateArea) / anchorArea : 0;
177
+ if (delta <= SNAP_DISTANCE && centerDelta <= SNAP_CENTER_DISTANCE && areaShift <= 0.08) {
133
178
  candidate = anchor;
134
179
  }
135
- else if (delta <= BLEND_DISTANCE) {
136
- candidate = (0, quad_1.blendQuads)(anchor, candidate, BLEND_ALPHA);
180
+ else if (delta <= BLEND_DISTANCE && centerDelta <= MAX_CENTER_DELTA && areaShift <= MAX_AREA_SHIFT) {
181
+ const normalizedDelta = Math.min(1, delta / BLEND_DISTANCE);
182
+ const adaptiveAlpha = 0.25 + normalizedDelta * 0.45; // 0.25..0.7 range
183
+ candidate = (0, quad_1.blendQuads)(anchor, candidate, adaptiveAlpha);
184
+ }
185
+ else {
186
+ smoothingBufferRef.current = [sanitized];
137
187
  }
138
188
  }
139
189
  candidate = (0, quad_1.orderQuadPoints)(candidate);
@@ -150,6 +200,7 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
150
200
  }, []);
151
201
  const [frameSize, setFrameSize] = (0, react_1.useState)(null);
152
202
  const updateFrameSize = (0, react_native_worklets_core_1.useRunOnJS)((width, height) => {
203
+ frameSizeRef.current = { width, height };
153
204
  setFrameSize({ width, height });
154
205
  }, []);
155
206
  const frameProcessor = (0, react_native_vision_camera_1.useFrameProcessor)((frame) => {
@@ -6,3 +6,7 @@ export declare const quadDistance: (a: Point[], b: Point[]) => number;
6
6
  export declare const averageQuad: (quads: Point[][]) => Point[];
7
7
  export declare const blendQuads: (base: Point[], target: Point[], alpha: number) => Point[];
8
8
  export declare const sanitizeQuad: (quad: Point[]) => Point[];
9
+ export declare const quadArea: (quad: Point[]) => number;
10
+ export declare const quadCenter: (quad: Point[]) => Point;
11
+ export declare const quadEdgeLengths: (quad: Point[]) => number[];
12
+ export declare const weightedAverageQuad: (quads: Point[][]) => Point[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sanitizeQuad = exports.blendQuads = exports.averageQuad = exports.quadDistance = exports.orderQuadPoints = exports.isValidQuad = exports.isValidPoint = void 0;
3
+ exports.weightedAverageQuad = exports.quadEdgeLengths = exports.quadCenter = exports.quadArea = exports.sanitizeQuad = exports.blendQuads = exports.averageQuad = exports.quadDistance = exports.orderQuadPoints = exports.isValidQuad = exports.isValidPoint = void 0;
4
4
  const POINT_EPSILON = 1e-3;
5
5
  const isFiniteNumber = (value) => Number.isFinite(value) && !Number.isNaN(value);
6
6
  const isValidPoint = (point) => {
@@ -91,3 +91,59 @@ const sanitizeQuad = (quad) => {
91
91
  }));
92
92
  };
93
93
  exports.sanitizeQuad = sanitizeQuad;
94
+ const quadArea = (quad) => {
95
+ if (!(0, exports.isValidQuad)(quad)) {
96
+ return 0;
97
+ }
98
+ let area = 0;
99
+ for (let i = 0; i < 4; i += 1) {
100
+ const current = quad[i];
101
+ const next = quad[(i + 1) % 4];
102
+ area += current.x * next.y - next.x * current.y;
103
+ }
104
+ return Math.abs(area) / 2;
105
+ };
106
+ exports.quadArea = quadArea;
107
+ const quadCenter = (quad) => {
108
+ if (!(0, exports.isValidQuad)(quad)) {
109
+ return { x: 0, y: 0 };
110
+ }
111
+ const sum = quad.reduce((acc, point) => ({ x: acc.x + point.x, y: acc.y + point.y }), { x: 0, y: 0 });
112
+ return {
113
+ x: sum.x / quad.length,
114
+ y: sum.y / quad.length,
115
+ };
116
+ };
117
+ exports.quadCenter = quadCenter;
118
+ const quadEdgeLengths = (quad) => {
119
+ if (!(0, exports.isValidQuad)(quad)) {
120
+ return [0, 0, 0, 0];
121
+ }
122
+ const lengths = [];
123
+ for (let i = 0; i < 4; i += 1) {
124
+ const current = quad[i];
125
+ const next = quad[(i + 1) % 4];
126
+ lengths.push(Math.hypot(next.x - current.x, next.y - current.y));
127
+ }
128
+ return lengths;
129
+ };
130
+ exports.quadEdgeLengths = quadEdgeLengths;
131
+ const weightedAverageQuad = (quads) => {
132
+ if (!Array.isArray(quads) || quads.length === 0) {
133
+ throw new Error('Cannot average empty quad array');
134
+ }
135
+ const weights = quads.map((_, index) => index + 1);
136
+ const totalWeight = weights.reduce((acc, weight) => acc + weight, 0);
137
+ const accum = quads[0].map(() => ({ x: 0, y: 0 }));
138
+ quads.forEach((quad, quadIndex) => {
139
+ quad.forEach((point, pointIndex) => {
140
+ accum[pointIndex].x += point.x * weights[quadIndex];
141
+ accum[pointIndex].y += point.y * weights[quadIndex];
142
+ });
143
+ });
144
+ return accum.map((point) => ({
145
+ x: point.x / totalWeight,
146
+ y: point.y / totalWeight,
147
+ }));
148
+ };
149
+ exports.weightedAverageQuad = weightedAverageQuad;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {
@@ -14,7 +14,17 @@ 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
+ import {
18
+ blendQuads,
19
+ isValidQuad,
20
+ orderQuadPoints,
21
+ quadArea,
22
+ quadCenter,
23
+ quadDistance,
24
+ quadEdgeLengths,
25
+ sanitizeQuad,
26
+ weightedAverageQuad,
27
+ } from './utils/quad';
18
28
  import type { Point } from './types';
19
29
 
20
30
  const isConvexQuadrilateral = (points: Point[]) => {
@@ -101,11 +111,19 @@ export const DocScanner: React.FC<Props> = ({
101
111
  const smoothingBufferRef = useRef<Point[][]>([]);
102
112
  const anchorQuadRef = useRef<Point[] | null>(null);
103
113
  const missingFrameCountRef = useRef(0);
114
+ const lastMeasurementRef = useRef<Point[] | null>(null);
115
+ const frameSizeRef = useRef<{ width: number; height: number } | null>(null);
104
116
 
105
117
  const MAX_HISTORY = 5;
106
118
  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;
119
+ const SNAP_CENTER_DISTANCE = 12;
120
+ const BLEND_DISTANCE = 65; // pixels; softly ease between similar shapes
121
+ const MAX_CENTER_DELTA = 85;
122
+ const MAX_AREA_SHIFT = 0.45;
123
+ const HISTORY_RESET_DISTANCE = 70;
124
+ const MIN_AREA_RATIO = 0.0035;
125
+ const MAX_AREA_RATIO = 0.9;
126
+ const MIN_EDGE_RATIO = 0.025;
109
127
 
110
128
  const updateQuad = useRunOnJS((value: Point[] | null) => {
111
129
  if (__DEV__) {
@@ -132,6 +150,44 @@ export const DocScanner: React.FC<Props> = ({
132
150
  const ordered = orderQuadPoints(value);
133
151
  const sanitized = sanitizeQuad(ordered);
134
152
 
153
+ const frameSize = frameSizeRef.current;
154
+ const frameArea = frameSize ? frameSize.width * frameSize.height : null;
155
+ const area = quadArea(sanitized);
156
+ const edges = quadEdgeLengths(sanitized);
157
+ const minEdge = Math.min(...edges);
158
+ const maxEdge = Math.max(...edges);
159
+ const aspectRatio = maxEdge > 0 ? maxEdge / Math.max(minEdge, 1) : 0;
160
+
161
+ const minEdgeThreshold = frameSize
162
+ ? Math.max(14, Math.min(frameSize.width, frameSize.height) * MIN_EDGE_RATIO)
163
+ : 14;
164
+
165
+ const areaTooSmall = frameArea ? area < frameArea * MIN_AREA_RATIO : area === 0;
166
+ const areaTooLarge = frameArea ? area > frameArea * MAX_AREA_RATIO : false;
167
+ const edgesTooShort = minEdge < minEdgeThreshold;
168
+ const aspectTooExtreme = aspectRatio > 7;
169
+
170
+ if (areaTooSmall || areaTooLarge || edgesTooShort || aspectTooExtreme) {
171
+ missingFrameCountRef.current += 1;
172
+
173
+ if (lastQuadRef.current && missingFrameCountRef.current <= 2) {
174
+ setQuad(lastQuadRef.current);
175
+ return;
176
+ }
177
+
178
+ smoothingBufferRef.current = [];
179
+ anchorQuadRef.current = null;
180
+ lastQuadRef.current = null;
181
+ setQuad(null);
182
+ return;
183
+ }
184
+
185
+ const lastMeasurement = lastMeasurementRef.current;
186
+ if (lastMeasurement && quadDistance(lastMeasurement, sanitized) > HISTORY_RESET_DISTANCE) {
187
+ smoothingBufferRef.current = [];
188
+ }
189
+ lastMeasurementRef.current = sanitized;
190
+
135
191
  smoothingBufferRef.current.push(sanitized);
136
192
  if (smoothingBufferRef.current.length > MAX_HISTORY) {
137
193
  smoothingBufferRef.current.shift();
@@ -139,16 +195,26 @@ export const DocScanner: React.FC<Props> = ({
139
195
 
140
196
  const history = smoothingBufferRef.current;
141
197
  const hasHistory = history.length >= 2;
142
- let candidate = hasHistory ? averageQuad(history) : sanitized;
198
+ let candidate = hasHistory ? weightedAverageQuad(history) : sanitized;
143
199
 
144
200
  const anchor = anchorQuadRef.current;
145
201
  if (anchor && isValidQuad(anchor)) {
146
202
  const delta = quadDistance(candidate, anchor);
147
-
148
- if (delta <= SNAP_DISTANCE) {
203
+ const anchorCenter = quadCenter(anchor);
204
+ const candidateCenter = quadCenter(candidate);
205
+ const anchorArea = quadArea(anchor);
206
+ const candidateArea = quadArea(candidate);
207
+ const centerDelta = Math.hypot(candidateCenter.x - anchorCenter.x, candidateCenter.y - anchorCenter.y);
208
+ const areaShift = anchorArea > 0 ? Math.abs(anchorArea - candidateArea) / anchorArea : 0;
209
+
210
+ if (delta <= SNAP_DISTANCE && centerDelta <= SNAP_CENTER_DISTANCE && areaShift <= 0.08) {
149
211
  candidate = anchor;
150
- } else if (delta <= BLEND_DISTANCE) {
151
- candidate = blendQuads(anchor, candidate, BLEND_ALPHA);
212
+ } else if (delta <= BLEND_DISTANCE && centerDelta <= MAX_CENTER_DELTA && areaShift <= MAX_AREA_SHIFT) {
213
+ const normalizedDelta = Math.min(1, delta / BLEND_DISTANCE);
214
+ const adaptiveAlpha = 0.25 + normalizedDelta * 0.45; // 0.25..0.7 range
215
+ candidate = blendQuads(anchor, candidate, adaptiveAlpha);
216
+ } else {
217
+ smoothingBufferRef.current = [sanitized];
152
218
  }
153
219
  }
154
220
 
@@ -169,6 +235,7 @@ export const DocScanner: React.FC<Props> = ({
169
235
 
170
236
  const [frameSize, setFrameSize] = useState<{ width: number; height: number } | null>(null);
171
237
  const updateFrameSize = useRunOnJS((width: number, height: number) => {
238
+ frameSizeRef.current = { width, height };
172
239
  setFrameSize({ width, height });
173
240
  }, []);
174
241
 
package/src/utils/quad.ts CHANGED
@@ -109,3 +109,73 @@ export const sanitizeQuad = (quad: Point[]): Point[] => {
109
109
  y: Number.isFinite(point.y) ? point.y : 0,
110
110
  }));
111
111
  };
112
+
113
+ export const quadArea = (quad: Point[]): number => {
114
+ if (!isValidQuad(quad)) {
115
+ return 0;
116
+ }
117
+
118
+ let area = 0;
119
+ for (let i = 0; i < 4; i += 1) {
120
+ const current = quad[i];
121
+ const next = quad[(i + 1) % 4];
122
+ area += current.x * next.y - next.x * current.y;
123
+ }
124
+
125
+ return Math.abs(area) / 2;
126
+ };
127
+
128
+ export const quadCenter = (quad: Point[]): Point => {
129
+ if (!isValidQuad(quad)) {
130
+ return { x: 0, y: 0 };
131
+ }
132
+
133
+ const sum = quad.reduce(
134
+ (acc, point) => ({ x: acc.x + point.x, y: acc.y + point.y }),
135
+ { x: 0, y: 0 },
136
+ );
137
+
138
+ return {
139
+ x: sum.x / quad.length,
140
+ y: sum.y / quad.length,
141
+ };
142
+ };
143
+
144
+ export const quadEdgeLengths = (quad: Point[]): number[] => {
145
+ if (!isValidQuad(quad)) {
146
+ return [0, 0, 0, 0];
147
+ }
148
+
149
+ const lengths: number[] = [];
150
+
151
+ for (let i = 0; i < 4; i += 1) {
152
+ const current = quad[i];
153
+ const next = quad[(i + 1) % 4];
154
+ lengths.push(Math.hypot(next.x - current.x, next.y - current.y));
155
+ }
156
+
157
+ return lengths;
158
+ };
159
+
160
+ export const weightedAverageQuad = (quads: Point[][]): Point[] => {
161
+ if (!Array.isArray(quads) || quads.length === 0) {
162
+ throw new Error('Cannot average empty quad array');
163
+ }
164
+
165
+ const weights = quads.map((_, index) => index + 1);
166
+ const totalWeight = weights.reduce((acc, weight) => acc + weight, 0);
167
+
168
+ const accum: Point[] = quads[0].map(() => ({ x: 0, y: 0 }));
169
+
170
+ quads.forEach((quad, quadIndex) => {
171
+ quad.forEach((point, pointIndex) => {
172
+ accum[pointIndex].x += point.x * weights[quadIndex];
173
+ accum[pointIndex].y += point.y * weights[quadIndex];
174
+ });
175
+ });
176
+
177
+ return accum.map((point) => ({
178
+ x: point.x / totalWeight,
179
+ y: point.y / totalWeight,
180
+ }));
181
+ };