@srsergio/taptapp-ar 1.0.60 → 1.0.61
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/compiler/detector/crop-detector.js +1 -1
- package/dist/compiler/node-worker.js +12 -37
- package/dist/compiler/offline-compiler.d.ts +4 -1
- package/dist/compiler/offline-compiler.js +10 -86
- package/package.json +1 -1
- package/src/compiler/detector/crop-detector.js +1 -1
- package/src/compiler/node-worker.js +12 -44
- package/src/compiler/offline-compiler.ts +11 -98
|
@@ -8,7 +8,7 @@ class CropDetector {
|
|
|
8
8
|
let minDimension = Math.min(width, height) / 2;
|
|
9
9
|
let cropSize = Math.pow(2, Math.round(Math.log(minDimension) / Math.log(2)));
|
|
10
10
|
this.cropSize = cropSize;
|
|
11
|
-
this.detector = new DetectorLite(cropSize, cropSize, { useLSH: true });
|
|
11
|
+
this.detector = new DetectorLite(cropSize, cropSize, { useLSH: true, maxOctaves: 1 });
|
|
12
12
|
this.lastRandomIndex = 4;
|
|
13
13
|
}
|
|
14
14
|
detect(input) {
|
|
@@ -64,40 +64,15 @@ parentPort.on('message', async (msg) => {
|
|
|
64
64
|
else if (msg.type === 'match') {
|
|
65
65
|
const { targetImage, percentPerImage, basePercent } = msg;
|
|
66
66
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
useLSH: true
|
|
71
|
-
});
|
|
72
|
-
parentPort.postMessage({ type: 'progress', percent: basePercent + percentPerImage * 0.1 });
|
|
73
|
-
const { featurePoints: allPoints } = detector.detect(targetImage.data);
|
|
74
|
-
parentPort.postMessage({ type: 'progress', percent: basePercent + percentPerImage * 0.5 });
|
|
75
|
-
// Group points by their scale (octave)
|
|
76
|
-
const scalesMap = new Map();
|
|
77
|
-
for (const p of allPoints) {
|
|
78
|
-
const octaveScale = p.scale;
|
|
79
|
-
let list = scalesMap.get(octaveScale);
|
|
80
|
-
if (!list) {
|
|
81
|
-
list = [];
|
|
82
|
-
scalesMap.set(octaveScale, list);
|
|
83
|
-
}
|
|
84
|
-
// Coordinates in p are already full-res.
|
|
85
|
-
// We need them relative to the scaled image for the keyframe.
|
|
86
|
-
list.push({
|
|
87
|
-
...p,
|
|
88
|
-
x: p.x / octaveScale,
|
|
89
|
-
y: p.y / octaveScale,
|
|
90
|
-
scale: 1.0 // Keypoint scale is always 1.0 relative to its own keyframe image
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
// Optional: Run another detector pass at an intermediate scale to improve coverage
|
|
94
|
-
// (e.g. at 1/1.41 ratio) if tracking robustness suffers.
|
|
95
|
-
// For now, let's stick to octaves for MAXIMUM speed.
|
|
67
|
+
const { buildImageList } = await import('./image-list.js');
|
|
68
|
+
const imageList = buildImageList(targetImage);
|
|
69
|
+
const percentPerScale = percentPerImage / imageList.length;
|
|
96
70
|
const keyframes = [];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
71
|
+
for (let i = 0; i < imageList.length; i++) {
|
|
72
|
+
const image = imageList[i];
|
|
73
|
+
// Disable internal pyramid (maxOctaves: 1) as we are already processing a scale list
|
|
74
|
+
const detector = new DetectorLite(image.width, image.height, { useLSH: true, maxOctaves: 1 });
|
|
75
|
+
const { featurePoints: ps } = detector.detect(image.data);
|
|
101
76
|
const sortedPs = sortPoints(ps);
|
|
102
77
|
const maximaPoints = sortedPs.filter((p) => p.maxima);
|
|
103
78
|
const minimaPoints = sortedPs.filter((p) => !p.maxima);
|
|
@@ -108,13 +83,13 @@ parentPort.on('message', async (msg) => {
|
|
|
108
83
|
minimaPoints,
|
|
109
84
|
maximaPointsCluster,
|
|
110
85
|
minimaPointsCluster,
|
|
111
|
-
width:
|
|
112
|
-
height:
|
|
113
|
-
scale:
|
|
86
|
+
width: image.width,
|
|
87
|
+
height: image.height,
|
|
88
|
+
scale: image.scale,
|
|
114
89
|
});
|
|
115
90
|
parentPort.postMessage({
|
|
116
91
|
type: 'progress',
|
|
117
|
-
percent: basePercent +
|
|
92
|
+
percent: basePercent + (i + 1) * percentPerScale
|
|
118
93
|
});
|
|
119
94
|
}
|
|
120
95
|
parentPort.postMessage({
|
|
@@ -12,7 +12,10 @@ export declare class OfflineCompiler {
|
|
|
12
12
|
constructor();
|
|
13
13
|
_initNodeWorkers(): Promise<void>;
|
|
14
14
|
compileImageTargets(images: any[], progressCallback: (p: number) => void): Promise<any>;
|
|
15
|
-
_compileTarget(targetImages: any[], progressCallback: (p: number) => void): Promise<
|
|
15
|
+
_compileTarget(targetImages: any[], progressCallback: (p: number) => void): Promise<{
|
|
16
|
+
matchingData: any;
|
|
17
|
+
trackingData: any;
|
|
18
|
+
}[]>;
|
|
16
19
|
_compileMatch(targetImages: any[], progressCallback: (p: number) => void): Promise<any[]>;
|
|
17
20
|
_compileTrack(targetImages: any[], progressCallback: (p: number) => void): Promise<any[]>;
|
|
18
21
|
compileTrack({ progressCallback, targetImages, basePercent }: {
|
|
@@ -88,91 +88,14 @@ export class OfflineCompiler {
|
|
|
88
88
|
async _compileTarget(targetImages, progressCallback) {
|
|
89
89
|
if (isNode)
|
|
90
90
|
await this._initNodeWorkers();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
100
|
-
progressCallback(sum / targetImages.length);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
return Promise.all(wrappedPromises);
|
|
105
|
-
}
|
|
106
|
-
// 🚀 MOONSHOT BROWSER FALLBACK:
|
|
107
|
-
// Combined detection to avoid redundant pyramid processing
|
|
108
|
-
const results = [];
|
|
109
|
-
for (let i = 0; i < targetImages.length; i++) {
|
|
110
|
-
const targetImage = targetImages[i];
|
|
111
|
-
// 1. Single Pass Detection + Pyramid Generation
|
|
112
|
-
const detector = new DetectorLite(targetImage.width, targetImage.height, { useLSH: true });
|
|
113
|
-
progressCallback((i / targetImages.length) * 100 + 10);
|
|
114
|
-
const { featurePoints, pyramid } = detector.detect(targetImage.data);
|
|
115
|
-
progressCallback((i / targetImages.length) * 100 + 40);
|
|
116
|
-
// 2. Extract Tracking Data using the ALREADY BLURRED pyramid
|
|
117
|
-
const trackingImageList = [];
|
|
118
|
-
const targetSizes = [256, 128];
|
|
119
|
-
for (const targetSize of targetSizes) {
|
|
120
|
-
let bestLevel = 0;
|
|
121
|
-
let minDiff = Math.abs(Math.min(targetImage.width, targetImage.height) - targetSize);
|
|
122
|
-
for (let l = 1; l < pyramid.length; l++) {
|
|
123
|
-
const img = pyramid[l][0];
|
|
124
|
-
const diff = Math.abs(Math.min(img.width, img.height) - targetSize);
|
|
125
|
-
if (diff < minDiff) {
|
|
126
|
-
minDiff = diff;
|
|
127
|
-
bestLevel = l;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const levelImg = pyramid[bestLevel][0];
|
|
131
|
-
trackingImageList.push({
|
|
132
|
-
data: levelImg.data,
|
|
133
|
-
width: levelImg.width,
|
|
134
|
-
height: levelImg.height,
|
|
135
|
-
scale: levelImg.width / targetImage.width
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
const trackingData = extractTrackingFeatures(trackingImageList, () => { });
|
|
139
|
-
progressCallback((i / targetImages.length) * 100 + 60);
|
|
140
|
-
// 3. Build Keyframes for Matching (Group by scale)
|
|
141
|
-
const scalesMap = new Map();
|
|
142
|
-
for (const p of featurePoints) {
|
|
143
|
-
const s = p.scale;
|
|
144
|
-
let list = scalesMap.get(s);
|
|
145
|
-
if (!list) {
|
|
146
|
-
list = [];
|
|
147
|
-
scalesMap.set(s, list);
|
|
148
|
-
}
|
|
149
|
-
list.push({ ...p, x: p.x / s, y: p.y / s, scale: 1.0 });
|
|
150
|
-
}
|
|
151
|
-
const keyframes = [];
|
|
152
|
-
const sortedScales = Array.from(scalesMap.keys()).sort((a, b) => a - b);
|
|
153
|
-
for (const s of sortedScales) {
|
|
154
|
-
const ps = scalesMap.get(s);
|
|
155
|
-
const maximaPoints = ps.filter((p) => p.maxima);
|
|
156
|
-
const minimaPoints = ps.filter((p) => !p.maxima);
|
|
157
|
-
const maximaPointsCluster = hierarchicalClusteringBuild({ points: maximaPoints });
|
|
158
|
-
const minimaPointsCluster = hierarchicalClusteringBuild({ points: minimaPoints });
|
|
159
|
-
keyframes.push({
|
|
160
|
-
maximaPoints,
|
|
161
|
-
minimaPoints,
|
|
162
|
-
maximaPointsCluster,
|
|
163
|
-
minimaPointsCluster,
|
|
164
|
-
width: Math.round(targetImage.width / s),
|
|
165
|
-
height: Math.round(targetImage.height / s),
|
|
166
|
-
scale: 1.0 / s,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
results.push({
|
|
170
|
-
matchingData: keyframes,
|
|
171
|
-
trackingData: trackingData
|
|
172
|
-
});
|
|
173
|
-
progressCallback(((i + 1) / targetImages.length) * 100);
|
|
174
|
-
}
|
|
175
|
-
return results;
|
|
91
|
+
// Reverted: 'compile-all' combined task was causing issues with pyramid processing
|
|
92
|
+
// We go back to sequential match and track for reliability
|
|
93
|
+
const matchingResults = await this._compileMatch(targetImages, (p) => progressCallback(p * 0.5));
|
|
94
|
+
const trackingResults = await this._compileTrack(targetImages, (p) => progressCallback(50 + p * 0.5));
|
|
95
|
+
return targetImages.map((_, i) => ({
|
|
96
|
+
matchingData: matchingResults[i],
|
|
97
|
+
trackingData: trackingResults[i]
|
|
98
|
+
}));
|
|
176
99
|
}
|
|
177
100
|
async _compileMatch(targetImages, progressCallback) {
|
|
178
101
|
const percentPerImage = 100 / targetImages.length;
|
|
@@ -203,7 +126,8 @@ export class OfflineCompiler {
|
|
|
203
126
|
const percentPerImageScale = percentPerImage / imageList.length;
|
|
204
127
|
const keyframes = [];
|
|
205
128
|
for (const image of imageList) {
|
|
206
|
-
|
|
129
|
+
// Disabling internal pyramid (maxOctaves: 1) as we are already processing a scale list
|
|
130
|
+
const detector = new DetectorLite(image.width, image.height, { useLSH: true, maxOctaves: 1 });
|
|
207
131
|
const { featurePoints: ps } = detector.detect(image.data);
|
|
208
132
|
const maximaPoints = ps.filter((p) => p.maxima);
|
|
209
133
|
const minimaPoints = ps.filter((p) => !p.maxima);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srsergio/taptapp-ar",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.61",
|
|
4
4
|
"author": "Sergio Lazaro <srsergiolazaro@gmail.com>",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"description": "Ultra-fast, lightweight Augmented Reality Image Tracking SDK for the web. Features an optimized offline compiler, React components, and compatibility with Three.js/A-Frame. No heavy ML frameworks required.",
|
|
@@ -11,7 +11,7 @@ class CropDetector {
|
|
|
11
11
|
let cropSize = Math.pow(2, Math.round(Math.log(minDimension) / Math.log(2)));
|
|
12
12
|
this.cropSize = cropSize;
|
|
13
13
|
|
|
14
|
-
this.detector = new DetectorLite(cropSize, cropSize, { useLSH: true });
|
|
14
|
+
this.detector = new DetectorLite(cropSize, cropSize, { useLSH: true, maxOctaves: 1 });
|
|
15
15
|
|
|
16
16
|
this.lastRandomIndex = 4;
|
|
17
17
|
}
|
|
@@ -71,49 +71,17 @@ parentPort.on('message', async (msg) => {
|
|
|
71
71
|
const { targetImage, percentPerImage, basePercent } = msg;
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
useLSH: true
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
parentPort.postMessage({ type: 'progress', percent: basePercent + percentPerImage * 0.1 });
|
|
81
|
-
|
|
82
|
-
const { featurePoints: allPoints } = detector.detect(targetImage.data);
|
|
83
|
-
|
|
84
|
-
parentPort.postMessage({ type: 'progress', percent: basePercent + percentPerImage * 0.5 });
|
|
85
|
-
|
|
86
|
-
// Group points by their scale (octave)
|
|
87
|
-
const scalesMap = new Map();
|
|
88
|
-
for (const p of allPoints) {
|
|
89
|
-
const octaveScale = p.scale;
|
|
90
|
-
let list = scalesMap.get(octaveScale);
|
|
91
|
-
if (!list) {
|
|
92
|
-
list = [];
|
|
93
|
-
scalesMap.set(octaveScale, list);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Coordinates in p are already full-res.
|
|
97
|
-
// We need them relative to the scaled image for the keyframe.
|
|
98
|
-
list.push({
|
|
99
|
-
...p,
|
|
100
|
-
x: p.x / octaveScale,
|
|
101
|
-
y: p.y / octaveScale,
|
|
102
|
-
scale: 1.0 // Keypoint scale is always 1.0 relative to its own keyframe image
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Optional: Run another detector pass at an intermediate scale to improve coverage
|
|
107
|
-
// (e.g. at 1/1.41 ratio) if tracking robustness suffers.
|
|
108
|
-
// For now, let's stick to octaves for MAXIMUM speed.
|
|
109
|
-
|
|
74
|
+
const { buildImageList } = await import('./image-list.js');
|
|
75
|
+
const imageList = buildImageList(targetImage);
|
|
76
|
+
const percentPerScale = percentPerImage / imageList.length;
|
|
110
77
|
const keyframes = [];
|
|
111
|
-
const sortedScales = Array.from(scalesMap.keys()).sort((a, b) => a - b);
|
|
112
78
|
|
|
113
|
-
|
|
79
|
+
for (let i = 0; i < imageList.length; i++) {
|
|
80
|
+
const image = imageList[i];
|
|
81
|
+
// Disable internal pyramid (maxOctaves: 1) as we are already processing a scale list
|
|
82
|
+
const detector = new DetectorLite(image.width, image.height, { useLSH: true, maxOctaves: 1 });
|
|
83
|
+
const { featurePoints: ps } = detector.detect(image.data);
|
|
114
84
|
|
|
115
|
-
for (const s of sortedScales) {
|
|
116
|
-
const ps = scalesMap.get(s);
|
|
117
85
|
const sortedPs = sortPoints(ps);
|
|
118
86
|
const maximaPoints = sortedPs.filter((p) => p.maxima);
|
|
119
87
|
const minimaPoints = sortedPs.filter((p) => !p.maxima);
|
|
@@ -126,14 +94,14 @@ parentPort.on('message', async (msg) => {
|
|
|
126
94
|
minimaPoints,
|
|
127
95
|
maximaPointsCluster,
|
|
128
96
|
minimaPointsCluster,
|
|
129
|
-
width:
|
|
130
|
-
height:
|
|
131
|
-
scale:
|
|
97
|
+
width: image.width,
|
|
98
|
+
height: image.height,
|
|
99
|
+
scale: image.scale,
|
|
132
100
|
});
|
|
133
101
|
|
|
134
102
|
parentPort.postMessage({
|
|
135
103
|
type: 'progress',
|
|
136
|
-
percent: basePercent +
|
|
104
|
+
percent: basePercent + (i + 1) * percentPerScale
|
|
137
105
|
});
|
|
138
106
|
}
|
|
139
107
|
|
|
@@ -110,103 +110,15 @@ export class OfflineCompiler {
|
|
|
110
110
|
async _compileTarget(targetImages: any[], progressCallback: (p: number) => void) {
|
|
111
111
|
if (isNode) await this._initNodeWorkers();
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
progressCallback(sum / targetImages.length);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
return Promise.all(wrappedPromises);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 🚀 MOONSHOT BROWSER FALLBACK:
|
|
130
|
-
// Combined detection to avoid redundant pyramid processing
|
|
131
|
-
const results = [];
|
|
132
|
-
for (let i = 0; i < targetImages.length; i++) {
|
|
133
|
-
const targetImage = targetImages[i];
|
|
134
|
-
|
|
135
|
-
// 1. Single Pass Detection + Pyramid Generation
|
|
136
|
-
const detector = new DetectorLite(targetImage.width, targetImage.height, { useLSH: true });
|
|
137
|
-
progressCallback((i / targetImages.length) * 100 + 10);
|
|
138
|
-
|
|
139
|
-
const { featurePoints, pyramid }: any = detector.detect(targetImage.data);
|
|
140
|
-
progressCallback((i / targetImages.length) * 100 + 40);
|
|
141
|
-
|
|
142
|
-
// 2. Extract Tracking Data using the ALREADY BLURRED pyramid
|
|
143
|
-
const trackingImageList: any[] = [];
|
|
144
|
-
const targetSizes = [256, 128];
|
|
145
|
-
for (const targetSize of targetSizes) {
|
|
146
|
-
let bestLevel = 0;
|
|
147
|
-
let minDiff = Math.abs(Math.min(targetImage.width, targetImage.height) - targetSize);
|
|
148
|
-
|
|
149
|
-
for (let l = 1; l < pyramid.length; l++) {
|
|
150
|
-
const img = pyramid[l][0];
|
|
151
|
-
const diff = Math.abs(Math.min(img.width, img.height) - targetSize);
|
|
152
|
-
if (diff < minDiff) {
|
|
153
|
-
minDiff = diff;
|
|
154
|
-
bestLevel = l;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const levelImg = pyramid[bestLevel][0];
|
|
159
|
-
trackingImageList.push({
|
|
160
|
-
data: levelImg.data,
|
|
161
|
-
width: levelImg.width,
|
|
162
|
-
height: levelImg.height,
|
|
163
|
-
scale: levelImg.width / targetImage.width
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const trackingData = extractTrackingFeatures(trackingImageList, () => { });
|
|
168
|
-
progressCallback((i / targetImages.length) * 100 + 60);
|
|
169
|
-
|
|
170
|
-
// 3. Build Keyframes for Matching (Group by scale)
|
|
171
|
-
const scalesMap = new Map();
|
|
172
|
-
for (const p of featurePoints) {
|
|
173
|
-
const s = p.scale;
|
|
174
|
-
let list = scalesMap.get(s);
|
|
175
|
-
if (!list) {
|
|
176
|
-
list = [];
|
|
177
|
-
scalesMap.set(s, list);
|
|
178
|
-
}
|
|
179
|
-
list.push({ ...p, x: p.x / s, y: p.y / s, scale: 1.0 });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const keyframes = [];
|
|
183
|
-
const sortedScales = Array.from(scalesMap.keys()).sort((a, b) => a - b);
|
|
184
|
-
for (const s of sortedScales) {
|
|
185
|
-
const ps = scalesMap.get(s);
|
|
186
|
-
const maximaPoints = ps.filter((p: any) => p.maxima);
|
|
187
|
-
const minimaPoints = ps.filter((p: any) => !p.maxima);
|
|
188
|
-
const maximaPointsCluster = hierarchicalClusteringBuild({ points: maximaPoints });
|
|
189
|
-
const minimaPointsCluster = hierarchicalClusteringBuild({ points: minimaPoints });
|
|
190
|
-
|
|
191
|
-
keyframes.push({
|
|
192
|
-
maximaPoints,
|
|
193
|
-
minimaPoints,
|
|
194
|
-
maximaPointsCluster,
|
|
195
|
-
minimaPointsCluster,
|
|
196
|
-
width: Math.round(targetImage.width / s),
|
|
197
|
-
height: Math.round(targetImage.height / s),
|
|
198
|
-
scale: 1.0 / s,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
results.push({
|
|
203
|
-
matchingData: keyframes,
|
|
204
|
-
trackingData: trackingData
|
|
205
|
-
});
|
|
206
|
-
progressCallback(((i + 1) / targetImages.length) * 100);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return results;
|
|
113
|
+
// Reverted: 'compile-all' combined task was causing issues with pyramid processing
|
|
114
|
+
// We go back to sequential match and track for reliability
|
|
115
|
+
const matchingResults = await this._compileMatch(targetImages, (p) => progressCallback(p * 0.5));
|
|
116
|
+
const trackingResults = await this._compileTrack(targetImages, (p) => progressCallback(50 + p * 0.5));
|
|
117
|
+
|
|
118
|
+
return targetImages.map((_, i) => ({
|
|
119
|
+
matchingData: matchingResults[i],
|
|
120
|
+
trackingData: trackingResults[i]
|
|
121
|
+
}));
|
|
210
122
|
}
|
|
211
123
|
|
|
212
124
|
async _compileMatch(targetImages: any[], progressCallback: (p: number) => void) {
|
|
@@ -243,7 +155,8 @@ export class OfflineCompiler {
|
|
|
243
155
|
const keyframes = [];
|
|
244
156
|
|
|
245
157
|
for (const image of imageList as any[]) {
|
|
246
|
-
|
|
158
|
+
// Disabling internal pyramid (maxOctaves: 1) as we are already processing a scale list
|
|
159
|
+
const detector = new DetectorLite(image.width, image.height, { useLSH: true, maxOctaves: 1 });
|
|
247
160
|
const { featurePoints: ps } = detector.detect(image.data);
|
|
248
161
|
|
|
249
162
|
const maximaPoints = ps.filter((p: any) => p.maxima);
|