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.
- package/dist/DocScanner.js +39 -36
- package/dist/utils/quad.d.ts +8 -0
- package/dist/utils/quad.js +93 -0
- package/dist/utils/stability.js +5 -4
- package/package.json +1 -1
- package/src/DocScanner.tsx +47 -38
- package/src/utils/quad.ts +111 -0
- package/src/utils/stability.ts +6 -8
package/dist/DocScanner.js
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
131
|
-
|
|
113
|
+
smoothingBufferRef.current = [];
|
|
114
|
+
anchorQuadRef.current = null;
|
|
115
|
+
lastQuadRef.current = null;
|
|
116
|
+
setQuad(null);
|
|
117
|
+
return;
|
|
132
118
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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;
|
package/dist/utils/stability.js
CHANGED
|
@@ -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 =
|
|
18
|
-
if (diff <
|
|
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
package/src/DocScanner.tsx
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
};
|
package/src/utils/stability.ts
CHANGED
|
@@ -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 =
|
|
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 <
|
|
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;
|