react-native-rectangle-doc-scanner 1.13.0 → 1.14.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 +49 -120
- package/dist/DocScanner.d.ts +6 -8
- package/dist/DocScanner.js +55 -108
- package/package.json +4 -2
- package/src/DocScanner.tsx +121 -241
- package/src/external.d.ts +28 -12
- package/android/build.gradle +0 -55
- package/android/consumer-rules.pro +0 -1
- package/android/proguard-rules.pro +0 -1
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerModule.kt +0 -37
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerPackage.kt +0 -16
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerView.kt +0 -568
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerViewManager.kt +0 -50
- package/docs/native-module-architecture.md +0 -178
- package/ios/RNRDocScannerModule.swift +0 -49
- package/ios/RNRDocScannerView.swift +0 -694
- package/ios/RNRDocScannerViewManager.m +0 -22
- package/ios/RNRDocScannerViewManager.swift +0 -47
- package/react-native-rectangle-doc-scanner.podspec +0 -22
- package/src/utils/overlay.tsx +0 -208
- package/src/utils/quad.ts +0 -181
- package/src/utils/stability.ts +0 -32
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import React
|
|
3
|
-
|
|
4
|
-
@objc(RNRDocScannerViewManager)
|
|
5
|
-
class RNRDocScannerViewManager: RCTViewManager {
|
|
6
|
-
override static func requiresMainQueueSetup() -> Bool {
|
|
7
|
-
true
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
override func view() -> UIView! {
|
|
11
|
-
RNRDocScannerView()
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
@objc func capture(_ reactTag: NSNumber, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
15
|
-
bridge.uiManager.addUIBlock { _, viewRegistry in
|
|
16
|
-
guard let view = viewRegistry?[reactTag] as? RNRDocScannerView else {
|
|
17
|
-
reject(RNRDocScannerError.viewNotFound.code, RNRDocScannerError.viewNotFound.message, nil)
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
view.capture { result in
|
|
22
|
-
switch result {
|
|
23
|
-
case let .success(payload):
|
|
24
|
-
resolve([
|
|
25
|
-
"croppedImage": payload.croppedImage ?? NSNull(),
|
|
26
|
-
"initialImage": payload.originalImage,
|
|
27
|
-
"width": payload.width,
|
|
28
|
-
"height": payload.height,
|
|
29
|
-
])
|
|
30
|
-
case let .failure(error as RNRDocScannerError):
|
|
31
|
-
reject(error.code, error.message, error)
|
|
32
|
-
case let .failure(error):
|
|
33
|
-
reject("capture_failed", error.localizedDescription, error)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@objc func reset(_ reactTag: NSNumber) {
|
|
40
|
-
bridge.uiManager.addUIBlock { _, viewRegistry in
|
|
41
|
-
guard let view = viewRegistry?[reactTag] as? RNRDocScannerView else {
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
view.resetStability()
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
|
|
3
|
-
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
-
|
|
5
|
-
Pod::Spec.new do |s|
|
|
6
|
-
s.name = 'react-native-rectangle-doc-scanner'
|
|
7
|
-
s.version = package['version']
|
|
8
|
-
s.summary = package.fetch('description', 'Document scanner with native camera overlay support for React Native.')
|
|
9
|
-
s.homepage = package['homepage'] || 'https://github.com/danchew90/react-native-rectangle-doc-scanner'
|
|
10
|
-
s.license = package['license'] || { :type => 'MIT' }
|
|
11
|
-
s.author = package['author'] || { 'react-native-rectangle-doc-scanner' => 'opensource@example.com' }
|
|
12
|
-
s.source = { :git => package.dig('repository', 'url') || s.homepage, :tag => "v#{s.version}" }
|
|
13
|
-
|
|
14
|
-
s.platform = :ios, '13.0'
|
|
15
|
-
s.swift_version = '5.0'
|
|
16
|
-
|
|
17
|
-
s.source_files = 'ios/**/*.{h,m,mm,swift}'
|
|
18
|
-
s.public_header_files = 'ios/**/*.h'
|
|
19
|
-
s.requires_arc = true
|
|
20
|
-
|
|
21
|
-
s.dependency 'React-Core'
|
|
22
|
-
end
|
package/src/utils/overlay.tsx
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
2
|
-
import { View, StyleSheet, useWindowDimensions } from 'react-native';
|
|
3
|
-
import { Canvas, Path, Skia } from '@shopify/react-native-skia';
|
|
4
|
-
import type { Point } from '../types';
|
|
5
|
-
|
|
6
|
-
const lerp = (start: Point, end: Point, t: number): Point => ({
|
|
7
|
-
x: start.x + (end.x - start.x) * t,
|
|
8
|
-
y: start.y + (end.y - start.y) * t,
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const withAlpha = (value: string, alpha: number): string => {
|
|
12
|
-
const hexMatch = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
|
|
13
|
-
if (!hexMatch) {
|
|
14
|
-
return `rgba(231, 166, 73, ${alpha})`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const hex = hexMatch[1];
|
|
18
|
-
const normalize = hex.length === 3
|
|
19
|
-
? hex.split('').map((ch) => ch + ch).join('')
|
|
20
|
-
: hex;
|
|
21
|
-
|
|
22
|
-
const r = parseInt(normalize.slice(0, 2), 16);
|
|
23
|
-
const g = parseInt(normalize.slice(2, 4), 16);
|
|
24
|
-
const b = parseInt(normalize.slice(4, 6), 16);
|
|
25
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type OverlayProps = {
|
|
29
|
-
quad: Point[] | null;
|
|
30
|
-
color?: string;
|
|
31
|
-
frameSize: { width: number; height: number } | null;
|
|
32
|
-
showGrid?: boolean;
|
|
33
|
-
gridColor?: string;
|
|
34
|
-
gridLineWidth?: number;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type OverlayGeometry = {
|
|
38
|
-
outlinePath: ReturnType<typeof Skia.Path.Make> | null;
|
|
39
|
-
gridPaths: ReturnType<typeof Skia.Path.Make>[];
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const buildPath = (points: Point[]) => {
|
|
43
|
-
const path = Skia.Path.Make();
|
|
44
|
-
path.moveTo(points[0].x, points[0].y);
|
|
45
|
-
points.slice(1).forEach((p) => path.lineTo(p.x, p.y));
|
|
46
|
-
path.close();
|
|
47
|
-
return path;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const orderQuad = (points: Point[]): Point[] => {
|
|
51
|
-
if (points.length !== 4) {
|
|
52
|
-
return points;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const center = points.reduce(
|
|
56
|
-
(acc, point) => ({ x: acc.x + point.x / 4, y: acc.y + point.y / 4 }),
|
|
57
|
-
{ x: 0, y: 0 },
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const sorted = [...points].sort((a, b) => {
|
|
61
|
-
const angleA = Math.atan2(a.y - center.y, a.x - center.x);
|
|
62
|
-
const angleB = Math.atan2(b.y - center.y, b.x - center.x);
|
|
63
|
-
return angleA - angleB;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Ensure the first point is the top-left (smallest y, then smallest x)
|
|
67
|
-
let startIndex = 0;
|
|
68
|
-
for (let i = 1; i < sorted.length; i += 1) {
|
|
69
|
-
const current = sorted[i];
|
|
70
|
-
const candidate = sorted[startIndex];
|
|
71
|
-
if (current.y < candidate.y || (current.y === candidate.y && current.x < candidate.x)) {
|
|
72
|
-
startIndex = i;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return [
|
|
77
|
-
sorted[startIndex % 4],
|
|
78
|
-
sorted[(startIndex + 1) % 4],
|
|
79
|
-
sorted[(startIndex + 2) % 4],
|
|
80
|
-
sorted[(startIndex + 3) % 4],
|
|
81
|
-
];
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export const Overlay: React.FC<OverlayProps> = ({
|
|
85
|
-
quad,
|
|
86
|
-
color = '#e7a649',
|
|
87
|
-
frameSize,
|
|
88
|
-
showGrid = true,
|
|
89
|
-
gridColor = 'rgba(231, 166, 73, 0.35)',
|
|
90
|
-
gridLineWidth = 2,
|
|
91
|
-
}) => {
|
|
92
|
-
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
93
|
-
const fillColor = useMemo(() => withAlpha(color, 0.2), [color]);
|
|
94
|
-
|
|
95
|
-
const { outlinePath, gridPaths }: OverlayGeometry = useMemo(() => {
|
|
96
|
-
let transformedQuad: Point[] | null = null;
|
|
97
|
-
let sourceQuad: Point[] | null = null;
|
|
98
|
-
let sourceFrameSize = frameSize;
|
|
99
|
-
|
|
100
|
-
if (quad && frameSize) {
|
|
101
|
-
sourceQuad = quad;
|
|
102
|
-
} else {
|
|
103
|
-
// No detection yet – skip drawing
|
|
104
|
-
return { outlinePath: null, gridPaths: [] };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (sourceQuad && sourceFrameSize) {
|
|
108
|
-
if (__DEV__) {
|
|
109
|
-
console.log('[Overlay] drawing quad:', sourceQuad);
|
|
110
|
-
console.log('[Overlay] color:', color);
|
|
111
|
-
console.log('[Overlay] screen dimensions:', screenWidth, 'x', screenHeight);
|
|
112
|
-
console.log('[Overlay] frame dimensions:', sourceFrameSize.width, 'x', sourceFrameSize.height);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const isFrameLandscape = sourceFrameSize.width > sourceFrameSize.height;
|
|
116
|
-
const isScreenPortrait = screenHeight > screenWidth;
|
|
117
|
-
const needsRotation = isFrameLandscape && isScreenPortrait;
|
|
118
|
-
|
|
119
|
-
if (needsRotation) {
|
|
120
|
-
const scaleX = screenWidth / sourceFrameSize.height;
|
|
121
|
-
const scaleY = screenHeight / sourceFrameSize.width;
|
|
122
|
-
|
|
123
|
-
transformedQuad = sourceQuad.map((p) => ({
|
|
124
|
-
x: p.y * scaleX,
|
|
125
|
-
y: (sourceFrameSize.width - p.x) * scaleY,
|
|
126
|
-
}));
|
|
127
|
-
} else {
|
|
128
|
-
const scaleX = screenWidth / sourceFrameSize.width;
|
|
129
|
-
const scaleY = screenHeight / sourceFrameSize.height;
|
|
130
|
-
|
|
131
|
-
transformedQuad = sourceQuad.map((p) => ({
|
|
132
|
-
x: p.x * scaleX,
|
|
133
|
-
y: p.y * scaleY,
|
|
134
|
-
}));
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!transformedQuad) {
|
|
139
|
-
return { outlinePath: null, gridPaths: [] };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const normalizedQuad = orderQuad(transformedQuad);
|
|
143
|
-
const skPath = buildPath(normalizedQuad);
|
|
144
|
-
const grid: ReturnType<typeof Skia.Path.Make>[] = [];
|
|
145
|
-
|
|
146
|
-
if (showGrid) {
|
|
147
|
-
const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
|
|
148
|
-
const steps = [1 / 3, 2 / 3];
|
|
149
|
-
|
|
150
|
-
steps.forEach((t) => {
|
|
151
|
-
const start = lerp(topLeft, topRight, t);
|
|
152
|
-
const end = lerp(bottomLeft, bottomRight, t);
|
|
153
|
-
const verticalPath = Skia.Path.Make();
|
|
154
|
-
verticalPath.moveTo(start.x, start.y);
|
|
155
|
-
verticalPath.lineTo(end.x, end.y);
|
|
156
|
-
grid.push(verticalPath);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
steps.forEach((t) => {
|
|
160
|
-
const start = lerp(topLeft, bottomLeft, t);
|
|
161
|
-
const end = lerp(topRight, bottomRight, t);
|
|
162
|
-
const horizontalPath = Skia.Path.Make();
|
|
163
|
-
horizontalPath.moveTo(start.x, start.y);
|
|
164
|
-
horizontalPath.lineTo(end.x, end.y);
|
|
165
|
-
grid.push(horizontalPath);
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return { outlinePath: skPath, gridPaths: grid };
|
|
170
|
-
}, [quad, screenWidth, screenHeight, frameSize, showGrid, color]);
|
|
171
|
-
|
|
172
|
-
if (__DEV__) {
|
|
173
|
-
console.log('[Overlay] rendering Canvas with dimensions:', screenWidth, 'x', screenHeight);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<View style={styles.container} pointerEvents="none">
|
|
178
|
-
<Canvas style={{ width: screenWidth, height: screenHeight }}>
|
|
179
|
-
{outlinePath && (
|
|
180
|
-
<>
|
|
181
|
-
<Path path={outlinePath} color={color} style="stroke" strokeWidth={8} />
|
|
182
|
-
<Path path={outlinePath} color={fillColor} style="fill" />
|
|
183
|
-
{gridPaths.map((gridPath, index) => (
|
|
184
|
-
<Path
|
|
185
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
186
|
-
key={`grid-${index}`}
|
|
187
|
-
path={gridPath}
|
|
188
|
-
color={gridColor}
|
|
189
|
-
style="stroke"
|
|
190
|
-
strokeWidth={gridLineWidth}
|
|
191
|
-
/>
|
|
192
|
-
))}
|
|
193
|
-
</>
|
|
194
|
-
)}
|
|
195
|
-
</Canvas>
|
|
196
|
-
</View>
|
|
197
|
-
);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const styles = StyleSheet.create({
|
|
201
|
-
container: {
|
|
202
|
-
position: 'absolute',
|
|
203
|
-
top: 0,
|
|
204
|
-
left: 0,
|
|
205
|
-
right: 0,
|
|
206
|
-
bottom: 0,
|
|
207
|
-
},
|
|
208
|
-
});
|
package/src/utils/quad.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
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
|
-
};
|
|
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
|
-
};
|
package/src/utils/stability.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { Point } from '../types';
|
|
2
|
-
import { isValidQuad, quadDistance } from './quad';
|
|
3
|
-
|
|
4
|
-
let last: Point[] | null = null;
|
|
5
|
-
let stable = 0;
|
|
6
|
-
|
|
7
|
-
const STABILITY_DISTANCE = 8;
|
|
8
|
-
|
|
9
|
-
export function checkStability(current: Point[] | null): number {
|
|
10
|
-
if (!isValidQuad(current)) {
|
|
11
|
-
stable = 0;
|
|
12
|
-
last = null;
|
|
13
|
-
return 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (!last) {
|
|
17
|
-
last = current;
|
|
18
|
-
stable = 1;
|
|
19
|
-
return stable;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const diff = quadDistance(current, last);
|
|
23
|
-
|
|
24
|
-
if (diff < STABILITY_DISTANCE) {
|
|
25
|
-
stable++;
|
|
26
|
-
} else {
|
|
27
|
-
stable = 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
last = current;
|
|
31
|
-
return stable;
|
|
32
|
-
}
|