react-native-rectangle-doc-scanner 0.40.0 → 0.42.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 +70 -6
- package/dist/utils/quad.d.ts +4 -0
- package/dist/utils/quad.js +57 -1
- package/package.json +1 -1
- package/src/DocScanner.tsx +90 -7
- package/src/utils/quad.ts +70 -0
package/dist/DocScanner.js
CHANGED
|
@@ -96,10 +96,19 @@ 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
|
|
102
|
-
const
|
|
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 REJECT_CENTER_DELTA = 145;
|
|
107
|
+
const MAX_AREA_SHIFT = 0.45;
|
|
108
|
+
const HISTORY_RESET_DISTANCE = 70;
|
|
109
|
+
const MIN_AREA_RATIO = 0.0035;
|
|
110
|
+
const MAX_AREA_RATIO = 0.9;
|
|
111
|
+
const MIN_EDGE_RATIO = 0.025;
|
|
103
112
|
const updateQuad = (0, react_native_worklets_core_1.useRunOnJS)((value) => {
|
|
104
113
|
if (__DEV__) {
|
|
105
114
|
console.log('[DocScanner] quad', value);
|
|
@@ -119,21 +128,75 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
|
|
|
119
128
|
missingFrameCountRef.current = 0;
|
|
120
129
|
const ordered = (0, quad_1.orderQuadPoints)(value);
|
|
121
130
|
const sanitized = (0, quad_1.sanitizeQuad)(ordered);
|
|
131
|
+
const frameSize = frameSizeRef.current;
|
|
132
|
+
const frameArea = frameSize ? frameSize.width * frameSize.height : null;
|
|
133
|
+
const area = (0, quad_1.quadArea)(sanitized);
|
|
134
|
+
const edges = (0, quad_1.quadEdgeLengths)(sanitized);
|
|
135
|
+
const minEdge = Math.min(...edges);
|
|
136
|
+
const maxEdge = Math.max(...edges);
|
|
137
|
+
const aspectRatio = maxEdge > 0 ? maxEdge / Math.max(minEdge, 1) : 0;
|
|
138
|
+
const minEdgeThreshold = frameSize
|
|
139
|
+
? Math.max(14, Math.min(frameSize.width, frameSize.height) * MIN_EDGE_RATIO)
|
|
140
|
+
: 14;
|
|
141
|
+
const areaTooSmall = frameArea ? area < frameArea * MIN_AREA_RATIO : area === 0;
|
|
142
|
+
const areaTooLarge = frameArea ? area > frameArea * MAX_AREA_RATIO : false;
|
|
143
|
+
const edgesTooShort = minEdge < minEdgeThreshold;
|
|
144
|
+
const aspectTooExtreme = aspectRatio > 7;
|
|
145
|
+
if (areaTooSmall || areaTooLarge || edgesTooShort || aspectTooExtreme) {
|
|
146
|
+
missingFrameCountRef.current += 1;
|
|
147
|
+
if (lastQuadRef.current && missingFrameCountRef.current <= 2) {
|
|
148
|
+
setQuad(lastQuadRef.current);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
smoothingBufferRef.current = [];
|
|
152
|
+
anchorQuadRef.current = null;
|
|
153
|
+
lastQuadRef.current = null;
|
|
154
|
+
setQuad(null);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const lastMeasurement = lastMeasurementRef.current;
|
|
158
|
+
if (lastMeasurement && (0, quad_1.quadDistance)(lastMeasurement, sanitized) > HISTORY_RESET_DISTANCE) {
|
|
159
|
+
smoothingBufferRef.current = [];
|
|
160
|
+
}
|
|
161
|
+
lastMeasurementRef.current = sanitized;
|
|
122
162
|
smoothingBufferRef.current.push(sanitized);
|
|
123
163
|
if (smoothingBufferRef.current.length > MAX_HISTORY) {
|
|
124
164
|
smoothingBufferRef.current.shift();
|
|
125
165
|
}
|
|
126
166
|
const history = smoothingBufferRef.current;
|
|
127
167
|
const hasHistory = history.length >= 2;
|
|
128
|
-
let candidate = hasHistory ? (0, quad_1.
|
|
168
|
+
let candidate = hasHistory ? (0, quad_1.weightedAverageQuad)(history) : sanitized;
|
|
129
169
|
const anchor = anchorQuadRef.current;
|
|
130
170
|
if (anchor && (0, quad_1.isValidQuad)(anchor)) {
|
|
131
171
|
const delta = (0, quad_1.quadDistance)(candidate, anchor);
|
|
132
|
-
|
|
172
|
+
const anchorCenter = (0, quad_1.quadCenter)(anchor);
|
|
173
|
+
const candidateCenter = (0, quad_1.quadCenter)(candidate);
|
|
174
|
+
const anchorArea = (0, quad_1.quadArea)(anchor);
|
|
175
|
+
const candidateArea = (0, quad_1.quadArea)(candidate);
|
|
176
|
+
const centerDelta = Math.hypot(candidateCenter.x - anchorCenter.x, candidateCenter.y - anchorCenter.y);
|
|
177
|
+
const areaShift = anchorArea > 0 ? Math.abs(anchorArea - candidateArea) / anchorArea : 0;
|
|
178
|
+
if (centerDelta >= REJECT_CENTER_DELTA || areaShift > 1.2) {
|
|
179
|
+
missingFrameCountRef.current += 1;
|
|
180
|
+
if (missingFrameCountRef.current <= 2) {
|
|
181
|
+
setQuad(anchor);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
smoothingBufferRef.current = [];
|
|
185
|
+
anchorQuadRef.current = null;
|
|
186
|
+
lastQuadRef.current = null;
|
|
187
|
+
setQuad(null);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (delta <= SNAP_DISTANCE && centerDelta <= SNAP_CENTER_DISTANCE && areaShift <= 0.08) {
|
|
133
191
|
candidate = anchor;
|
|
134
192
|
}
|
|
135
|
-
else if (delta <= BLEND_DISTANCE) {
|
|
136
|
-
|
|
193
|
+
else if (delta <= BLEND_DISTANCE && centerDelta <= MAX_CENTER_DELTA && areaShift <= MAX_AREA_SHIFT) {
|
|
194
|
+
const normalizedDelta = Math.min(1, delta / BLEND_DISTANCE);
|
|
195
|
+
const adaptiveAlpha = 0.25 + normalizedDelta * 0.45; // 0.25..0.7 range
|
|
196
|
+
candidate = (0, quad_1.blendQuads)(anchor, candidate, adaptiveAlpha);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
smoothingBufferRef.current = [sanitized];
|
|
137
200
|
}
|
|
138
201
|
}
|
|
139
202
|
candidate = (0, quad_1.orderQuadPoints)(candidate);
|
|
@@ -150,6 +213,7 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
|
|
|
150
213
|
}, []);
|
|
151
214
|
const [frameSize, setFrameSize] = (0, react_1.useState)(null);
|
|
152
215
|
const updateFrameSize = (0, react_native_worklets_core_1.useRunOnJS)((width, height) => {
|
|
216
|
+
frameSizeRef.current = { width, height };
|
|
153
217
|
setFrameSize({ width, height });
|
|
154
218
|
}, []);
|
|
155
219
|
const frameProcessor = (0, react_native_vision_camera_1.useFrameProcessor)((frame) => {
|
package/dist/utils/quad.d.ts
CHANGED
|
@@ -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[];
|
package/dist/utils/quad.js
CHANGED
|
@@ -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
package/src/DocScanner.tsx
CHANGED
|
@@ -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 {
|
|
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,20 @@ 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
|
|
108
|
-
const
|
|
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 REJECT_CENTER_DELTA = 145;
|
|
123
|
+
const MAX_AREA_SHIFT = 0.45;
|
|
124
|
+
const HISTORY_RESET_DISTANCE = 70;
|
|
125
|
+
const MIN_AREA_RATIO = 0.0035;
|
|
126
|
+
const MAX_AREA_RATIO = 0.9;
|
|
127
|
+
const MIN_EDGE_RATIO = 0.025;
|
|
109
128
|
|
|
110
129
|
const updateQuad = useRunOnJS((value: Point[] | null) => {
|
|
111
130
|
if (__DEV__) {
|
|
@@ -132,6 +151,44 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
132
151
|
const ordered = orderQuadPoints(value);
|
|
133
152
|
const sanitized = sanitizeQuad(ordered);
|
|
134
153
|
|
|
154
|
+
const frameSize = frameSizeRef.current;
|
|
155
|
+
const frameArea = frameSize ? frameSize.width * frameSize.height : null;
|
|
156
|
+
const area = quadArea(sanitized);
|
|
157
|
+
const edges = quadEdgeLengths(sanitized);
|
|
158
|
+
const minEdge = Math.min(...edges);
|
|
159
|
+
const maxEdge = Math.max(...edges);
|
|
160
|
+
const aspectRatio = maxEdge > 0 ? maxEdge / Math.max(minEdge, 1) : 0;
|
|
161
|
+
|
|
162
|
+
const minEdgeThreshold = frameSize
|
|
163
|
+
? Math.max(14, Math.min(frameSize.width, frameSize.height) * MIN_EDGE_RATIO)
|
|
164
|
+
: 14;
|
|
165
|
+
|
|
166
|
+
const areaTooSmall = frameArea ? area < frameArea * MIN_AREA_RATIO : area === 0;
|
|
167
|
+
const areaTooLarge = frameArea ? area > frameArea * MAX_AREA_RATIO : false;
|
|
168
|
+
const edgesTooShort = minEdge < minEdgeThreshold;
|
|
169
|
+
const aspectTooExtreme = aspectRatio > 7;
|
|
170
|
+
|
|
171
|
+
if (areaTooSmall || areaTooLarge || edgesTooShort || aspectTooExtreme) {
|
|
172
|
+
missingFrameCountRef.current += 1;
|
|
173
|
+
|
|
174
|
+
if (lastQuadRef.current && missingFrameCountRef.current <= 2) {
|
|
175
|
+
setQuad(lastQuadRef.current);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
smoothingBufferRef.current = [];
|
|
180
|
+
anchorQuadRef.current = null;
|
|
181
|
+
lastQuadRef.current = null;
|
|
182
|
+
setQuad(null);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const lastMeasurement = lastMeasurementRef.current;
|
|
187
|
+
if (lastMeasurement && quadDistance(lastMeasurement, sanitized) > HISTORY_RESET_DISTANCE) {
|
|
188
|
+
smoothingBufferRef.current = [];
|
|
189
|
+
}
|
|
190
|
+
lastMeasurementRef.current = sanitized;
|
|
191
|
+
|
|
135
192
|
smoothingBufferRef.current.push(sanitized);
|
|
136
193
|
if (smoothingBufferRef.current.length > MAX_HISTORY) {
|
|
137
194
|
smoothingBufferRef.current.shift();
|
|
@@ -139,16 +196,41 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
139
196
|
|
|
140
197
|
const history = smoothingBufferRef.current;
|
|
141
198
|
const hasHistory = history.length >= 2;
|
|
142
|
-
let candidate = hasHistory ?
|
|
199
|
+
let candidate = hasHistory ? weightedAverageQuad(history) : sanitized;
|
|
143
200
|
|
|
144
201
|
const anchor = anchorQuadRef.current;
|
|
145
202
|
if (anchor && isValidQuad(anchor)) {
|
|
146
203
|
const delta = quadDistance(candidate, anchor);
|
|
204
|
+
const anchorCenter = quadCenter(anchor);
|
|
205
|
+
const candidateCenter = quadCenter(candidate);
|
|
206
|
+
const anchorArea = quadArea(anchor);
|
|
207
|
+
const candidateArea = quadArea(candidate);
|
|
208
|
+
const centerDelta = Math.hypot(candidateCenter.x - anchorCenter.x, candidateCenter.y - anchorCenter.y);
|
|
209
|
+
const areaShift = anchorArea > 0 ? Math.abs(anchorArea - candidateArea) / anchorArea : 0;
|
|
210
|
+
|
|
211
|
+
if (centerDelta >= REJECT_CENTER_DELTA || areaShift > 1.2) {
|
|
212
|
+
missingFrameCountRef.current += 1;
|
|
213
|
+
|
|
214
|
+
if (missingFrameCountRef.current <= 2) {
|
|
215
|
+
setQuad(anchor);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
smoothingBufferRef.current = [];
|
|
220
|
+
anchorQuadRef.current = null;
|
|
221
|
+
lastQuadRef.current = null;
|
|
222
|
+
setQuad(null);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
147
225
|
|
|
148
|
-
if (delta <= SNAP_DISTANCE) {
|
|
226
|
+
if (delta <= SNAP_DISTANCE && centerDelta <= SNAP_CENTER_DISTANCE && areaShift <= 0.08) {
|
|
149
227
|
candidate = anchor;
|
|
150
|
-
} else if (delta <= BLEND_DISTANCE) {
|
|
151
|
-
|
|
228
|
+
} else if (delta <= BLEND_DISTANCE && centerDelta <= MAX_CENTER_DELTA && areaShift <= MAX_AREA_SHIFT) {
|
|
229
|
+
const normalizedDelta = Math.min(1, delta / BLEND_DISTANCE);
|
|
230
|
+
const adaptiveAlpha = 0.25 + normalizedDelta * 0.45; // 0.25..0.7 range
|
|
231
|
+
candidate = blendQuads(anchor, candidate, adaptiveAlpha);
|
|
232
|
+
} else {
|
|
233
|
+
smoothingBufferRef.current = [sanitized];
|
|
152
234
|
}
|
|
153
235
|
}
|
|
154
236
|
|
|
@@ -169,6 +251,7 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
169
251
|
|
|
170
252
|
const [frameSize, setFrameSize] = useState<{ width: number; height: number } | null>(null);
|
|
171
253
|
const updateFrameSize = useRunOnJS((width: number, height: number) => {
|
|
254
|
+
frameSizeRef.current = { width, height };
|
|
172
255
|
setFrameSize({ width, height });
|
|
173
256
|
}, []);
|
|
174
257
|
|
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
|
+
};
|