@pooder/kit 3.3.0 → 3.4.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/CHANGELOG.md +6 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +287 -144
- package/dist/index.mjs +287 -144
- package/package.json +1 -1
- package/src/image.ts +113 -138
- package/src/tracer.ts +278 -164
package/src/tracer.ts
CHANGED
|
@@ -17,11 +17,12 @@ export class ImageTracer {
|
|
|
17
17
|
public static async trace(
|
|
18
18
|
imageUrl: string,
|
|
19
19
|
options: {
|
|
20
|
-
threshold?: number; // 0-255, default
|
|
21
|
-
simplifyTolerance?: number; // default
|
|
22
|
-
scale?: number; // Scale factor for the processing canvas, default 1.0
|
|
20
|
+
threshold?: number; // 0-255, default 10
|
|
21
|
+
simplifyTolerance?: number; // default 2.0 (Balanced)
|
|
22
|
+
scale?: number; // Scale factor for the processing canvas, default 1.0
|
|
23
23
|
scaleToWidth?: number;
|
|
24
24
|
scaleToHeight?: number;
|
|
25
|
+
morphologyRadius?: number; // Default 10.
|
|
25
26
|
} = {},
|
|
26
27
|
): Promise<string> {
|
|
27
28
|
const img = await this.loadImage(imageUrl);
|
|
@@ -38,29 +39,278 @@ export class ImageTracer {
|
|
|
38
39
|
ctx.drawImage(img, 0, 0);
|
|
39
40
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
40
41
|
|
|
41
|
-
// 2.
|
|
42
|
-
const
|
|
42
|
+
// 2. Morphology processing
|
|
43
|
+
const threshold = options.threshold ?? 10;
|
|
44
|
+
// Adaptive radius: 3% of the image's largest dimension, at least 5px
|
|
45
|
+
const adaptiveRadius = Math.max(
|
|
46
|
+
5,
|
|
47
|
+
Math.floor(Math.max(width, height) * 0.02),
|
|
48
|
+
);
|
|
49
|
+
const radius = options.morphologyRadius ?? adaptiveRadius;
|
|
50
|
+
|
|
51
|
+
let mask = this.createMask(imageData, threshold);
|
|
52
|
+
if (radius > 0) {
|
|
53
|
+
// Closing operation: Dilation followed by Erosion to merge parts and smooth
|
|
54
|
+
mask = this.dilate(mask, width, height, radius);
|
|
55
|
+
mask = this.erode(mask, width, height, radius);
|
|
56
|
+
// Fill internal holes to ensure we only get the overall outer contour
|
|
57
|
+
mask = this.fillHoles(mask, width, height);
|
|
58
|
+
}
|
|
43
59
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
// 3. Trace contours from the unified mask
|
|
61
|
+
const allContourPoints = this.traceAllContours(mask, width, height);
|
|
62
|
+
|
|
63
|
+
if (allContourPoints.length === 0) {
|
|
64
|
+
// Fallback: Return a rectangular outline matching dimensions
|
|
65
|
+
const w = options.scaleToWidth ?? width;
|
|
66
|
+
const h = options.scaleToHeight ?? height;
|
|
67
|
+
return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. Select the largest contour to ensure a single, consistent overall shape
|
|
71
|
+
const primaryContour = allContourPoints.sort(
|
|
72
|
+
(a, b) => b.length - a.length,
|
|
73
|
+
)[0];
|
|
74
|
+
|
|
75
|
+
// 5. Find bounds for the selected contour
|
|
76
|
+
let minX = Infinity,
|
|
77
|
+
minY = Infinity,
|
|
78
|
+
maxX = -Infinity,
|
|
79
|
+
maxY = -Infinity;
|
|
80
|
+
|
|
81
|
+
for (const p of primaryContour) {
|
|
82
|
+
if (p.x < minX) minX = p.x;
|
|
83
|
+
if (p.y < minY) minY = p.y;
|
|
84
|
+
if (p.x > maxX) maxX = p.x;
|
|
85
|
+
if (p.y > maxY) maxY = p.y;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const globalBounds = {
|
|
89
|
+
minX,
|
|
90
|
+
minY,
|
|
91
|
+
width: maxX - minX,
|
|
92
|
+
height: maxY - minY,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// 6. Post-processing
|
|
96
|
+
let finalPoints = primaryContour;
|
|
97
|
+
if (options.scaleToWidth && options.scaleToHeight) {
|
|
47
98
|
finalPoints = this.scalePoints(
|
|
48
|
-
|
|
99
|
+
primaryContour,
|
|
49
100
|
options.scaleToWidth,
|
|
50
101
|
options.scaleToHeight,
|
|
102
|
+
globalBounds,
|
|
51
103
|
);
|
|
52
104
|
}
|
|
53
105
|
|
|
54
|
-
// 3. Simplify path
|
|
55
106
|
const simplifiedPoints = this.douglasPeucker(
|
|
56
107
|
finalPoints,
|
|
57
|
-
options.simplifyTolerance ?? 0
|
|
108
|
+
options.simplifyTolerance ?? 2.0,
|
|
58
109
|
);
|
|
59
110
|
|
|
60
|
-
// 4. Convert to SVG Path
|
|
61
111
|
return this.pointsToSVG(simplifiedPoints);
|
|
62
112
|
}
|
|
63
113
|
|
|
114
|
+
private static createMask(
|
|
115
|
+
imageData: ImageData,
|
|
116
|
+
threshold: number,
|
|
117
|
+
): Uint8Array {
|
|
118
|
+
const { width, height, data } = imageData;
|
|
119
|
+
const mask = new Uint8Array(width * height);
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < width * height; i++) {
|
|
122
|
+
const idx = i * 4;
|
|
123
|
+
const r = data[idx];
|
|
124
|
+
const g = data[idx + 1];
|
|
125
|
+
const b = data[idx + 2];
|
|
126
|
+
const a = data[idx + 3];
|
|
127
|
+
|
|
128
|
+
// Alpha threshold + White background heuristic
|
|
129
|
+
if (a > threshold && !(r > 240 && g > 240 && b > 240)) {
|
|
130
|
+
mask[i] = 1;
|
|
131
|
+
} else {
|
|
132
|
+
mask[i] = 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return mask;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fast 1D-separable Dilation
|
|
140
|
+
*/
|
|
141
|
+
private static dilate(
|
|
142
|
+
mask: Uint8Array,
|
|
143
|
+
width: number,
|
|
144
|
+
height: number,
|
|
145
|
+
radius: number,
|
|
146
|
+
): Uint8Array {
|
|
147
|
+
const horizontal = new Uint8Array(width * height);
|
|
148
|
+
for (let y = 0; y < height; y++) {
|
|
149
|
+
let count = 0;
|
|
150
|
+
for (let x = -radius; x < width; x++) {
|
|
151
|
+
if (x + radius < width && mask[y * width + x + radius]) count++;
|
|
152
|
+
if (x - radius - 1 >= 0 && mask[y * width + x - radius - 1]) count--;
|
|
153
|
+
if (x >= 0) horizontal[y * width + x] = count > 0 ? 1 : 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const vertical = new Uint8Array(width * height);
|
|
158
|
+
for (let x = 0; x < width; x++) {
|
|
159
|
+
let count = 0;
|
|
160
|
+
for (let y = -radius; y < height; y++) {
|
|
161
|
+
if (y + radius < height && horizontal[(y + radius) * width + x])
|
|
162
|
+
count++;
|
|
163
|
+
if (y - radius - 1 >= 0 && horizontal[(y - radius - 1) * width + x])
|
|
164
|
+
count--;
|
|
165
|
+
if (y >= 0) vertical[y * width + x] = count > 0 ? 1 : 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return vertical;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fast 1D-separable Erosion
|
|
173
|
+
*/
|
|
174
|
+
private static erode(
|
|
175
|
+
mask: Uint8Array,
|
|
176
|
+
width: number,
|
|
177
|
+
height: number,
|
|
178
|
+
radius: number,
|
|
179
|
+
): Uint8Array {
|
|
180
|
+
const horizontal = new Uint8Array(width * height);
|
|
181
|
+
for (let y = 0; y < height; y++) {
|
|
182
|
+
let count = 0;
|
|
183
|
+
for (let x = -radius; x < width; x++) {
|
|
184
|
+
if (x + radius < width && mask[y * width + x + radius]) count++;
|
|
185
|
+
if (x - radius - 1 >= 0 && mask[y * width + x - radius - 1]) count--;
|
|
186
|
+
if (x >= 0) {
|
|
187
|
+
const winWidth =
|
|
188
|
+
Math.min(x + radius, width - 1) - Math.max(x - radius, 0) + 1;
|
|
189
|
+
horizontal[y * width + x] = count === winWidth ? 1 : 0;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const vertical = new Uint8Array(width * height);
|
|
195
|
+
for (let x = 0; x < width; x++) {
|
|
196
|
+
let count = 0;
|
|
197
|
+
for (let y = -radius; y < height; y++) {
|
|
198
|
+
if (y + radius < height && horizontal[(y + radius) * width + x])
|
|
199
|
+
count++;
|
|
200
|
+
if (y - radius - 1 >= 0 && horizontal[(y - radius - 1) * width + x])
|
|
201
|
+
count--;
|
|
202
|
+
if (y >= 0) {
|
|
203
|
+
const winHeight =
|
|
204
|
+
Math.min(y + radius, height - 1) - Math.max(y - radius, 0) + 1;
|
|
205
|
+
vertical[y * width + x] = count === winHeight ? 1 : 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return vertical;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Fills internal holes in the binary mask using flood fill from edges.
|
|
214
|
+
*/
|
|
215
|
+
private static fillHoles(
|
|
216
|
+
mask: Uint8Array,
|
|
217
|
+
width: number,
|
|
218
|
+
height: number,
|
|
219
|
+
): Uint8Array {
|
|
220
|
+
const background = new Uint8Array(width * height);
|
|
221
|
+
const queue: [number, number][] = [];
|
|
222
|
+
|
|
223
|
+
// Add all edge pixels that are 0 to the queue
|
|
224
|
+
for (let x = 0; x < width; x++) {
|
|
225
|
+
if (mask[x] === 0) {
|
|
226
|
+
background[x] = 1;
|
|
227
|
+
queue.push([x, 0]);
|
|
228
|
+
}
|
|
229
|
+
const lastRow = (height - 1) * width + x;
|
|
230
|
+
if (mask[lastRow] === 0) {
|
|
231
|
+
background[lastRow] = 1;
|
|
232
|
+
queue.push([x, height - 1]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (let y = 1; y < height - 1; y++) {
|
|
236
|
+
if (mask[y * width] === 0) {
|
|
237
|
+
background[y * width] = 1;
|
|
238
|
+
queue.push([0, y]);
|
|
239
|
+
}
|
|
240
|
+
if (mask[y * width + width - 1] === 0) {
|
|
241
|
+
background[y * width + width - 1] = 1;
|
|
242
|
+
queue.push([width - 1, y]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Flood fill from the edges to find all background pixels
|
|
247
|
+
const dirs = [
|
|
248
|
+
[0, 1],
|
|
249
|
+
[0, -1],
|
|
250
|
+
[1, 0],
|
|
251
|
+
[-1, 0],
|
|
252
|
+
];
|
|
253
|
+
let head = 0;
|
|
254
|
+
while (head < queue.length) {
|
|
255
|
+
const [cx, cy] = queue[head++];
|
|
256
|
+
for (const [dx, dy] of dirs) {
|
|
257
|
+
const nx = cx + dx;
|
|
258
|
+
const ny = cy + dy;
|
|
259
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
|
260
|
+
const nidx = ny * width + nx;
|
|
261
|
+
if (mask[nidx] === 0 && background[nidx] === 0) {
|
|
262
|
+
background[nidx] = 1;
|
|
263
|
+
queue.push([nx, ny]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Any pixel that is NOT reachable from the background is part of the "filled" mask
|
|
270
|
+
const filledMask = new Uint8Array(width * height);
|
|
271
|
+
for (let i = 0; i < width * height; i++) {
|
|
272
|
+
filledMask[i] = background[i] === 0 ? 1 : 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return filledMask;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Traces all contours in the mask with optimized start-point detection
|
|
280
|
+
*/
|
|
281
|
+
private static traceAllContours(
|
|
282
|
+
mask: Uint8Array,
|
|
283
|
+
width: number,
|
|
284
|
+
height: number,
|
|
285
|
+
): Point[][] {
|
|
286
|
+
const visited = new Uint8Array(width * height);
|
|
287
|
+
const allContours: Point[][] = [];
|
|
288
|
+
|
|
289
|
+
for (let y = 0; y < height; y++) {
|
|
290
|
+
for (let x = 0; x < width; x++) {
|
|
291
|
+
const idx = y * width + x;
|
|
292
|
+
if (mask[idx] && !visited[idx]) {
|
|
293
|
+
// Only start a new trace if it's a potential outer boundary (left edge)
|
|
294
|
+
const isLeftEdge = x === 0 || mask[idx - 1] === 0;
|
|
295
|
+
if (isLeftEdge) {
|
|
296
|
+
const contour = this.marchingSquares(
|
|
297
|
+
mask,
|
|
298
|
+
visited,
|
|
299
|
+
x,
|
|
300
|
+
y,
|
|
301
|
+
width,
|
|
302
|
+
height,
|
|
303
|
+
);
|
|
304
|
+
if (contour.length > 2) {
|
|
305
|
+
allContours.push(contour);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return allContours;
|
|
312
|
+
}
|
|
313
|
+
|
|
64
314
|
private static loadImage(url: string): Promise<HTMLImageElement> {
|
|
65
315
|
return new Promise((resolve, reject) => {
|
|
66
316
|
const img = new Image();
|
|
@@ -76,73 +326,18 @@ export class ImageTracer {
|
|
|
76
326
|
* More robust for irregular shapes than simple Marching Squares walker.
|
|
77
327
|
*/
|
|
78
328
|
private static marchingSquares(
|
|
79
|
-
|
|
80
|
-
|
|
329
|
+
mask: Uint8Array,
|
|
330
|
+
visited: Uint8Array,
|
|
331
|
+
startX: number,
|
|
332
|
+
startY: number,
|
|
333
|
+
width: number,
|
|
334
|
+
height: number,
|
|
81
335
|
): Point[] {
|
|
82
|
-
const width = imageData.width;
|
|
83
|
-
const height = imageData.height;
|
|
84
|
-
const data = imageData.data;
|
|
85
|
-
|
|
86
|
-
// Use Luminance for solid check if Alpha is fully opaque
|
|
87
|
-
// Or check Alpha first, then Luminance?
|
|
88
|
-
// Let's assume:
|
|
89
|
-
// If pixel is transparent (Alpha <= threshold), it's empty.
|
|
90
|
-
// If pixel is opaque (Alpha > threshold):
|
|
91
|
-
// If it's white (Luminance > some_high_value), it's empty (background).
|
|
92
|
-
// Else it's solid.
|
|
93
|
-
// This supports black shapes on white background (JPG).
|
|
94
|
-
|
|
95
|
-
// Luminance = 0.299*R + 0.587*G + 0.114*B
|
|
96
|
-
// We treat "Dark" as solid? Or "Light" as solid?
|
|
97
|
-
// Usually "Content" is non-white on white background.
|
|
98
|
-
// Let's add a `luminanceThreshold` option?
|
|
99
|
-
// For now, let's hardcode a heuristic:
|
|
100
|
-
// If R,G,B are all > 240, treat as background (white).
|
|
101
|
-
|
|
102
336
|
const isSolid = (x: number, y: number): boolean => {
|
|
103
337
|
if (x < 0 || x >= width || y < 0 || y >= height) return false;
|
|
104
|
-
|
|
105
|
-
const r = data[index];
|
|
106
|
-
const g = data[index + 1];
|
|
107
|
-
const b = data[index + 2];
|
|
108
|
-
const a = data[index + 3];
|
|
109
|
-
|
|
110
|
-
if (a <= alphaThreshold) return false;
|
|
111
|
-
|
|
112
|
-
// Check for White Background (approx)
|
|
113
|
-
// If average > 240, treat as empty
|
|
114
|
-
if (r > 240 && g > 240 && b > 240) return false;
|
|
115
|
-
|
|
116
|
-
return true;
|
|
338
|
+
return mask[y * width + x] === 1;
|
|
117
339
|
};
|
|
118
340
|
|
|
119
|
-
// 1. Find Starting Pixel (Scanline)
|
|
120
|
-
// We want the *largest* contour ideally, or the first one.
|
|
121
|
-
// For now, let's just find the first one.
|
|
122
|
-
// To support holes, we would need to keep scanning.
|
|
123
|
-
// But Moore Tracing follows a single contour.
|
|
124
|
-
|
|
125
|
-
let startX = -1;
|
|
126
|
-
let startY = -1;
|
|
127
|
-
|
|
128
|
-
// Gaussian Blur Simulation (Box Blur) to reduce noise?
|
|
129
|
-
// Or just simple neighbor check?
|
|
130
|
-
// Let's implement a simple noise filter in isSolid? No, isSolid is per pixel.
|
|
131
|
-
// Let's preprocess data? Too slow in JS?
|
|
132
|
-
// Let's just rely on threshold.
|
|
133
|
-
|
|
134
|
-
searchLoop: for (let y = 0; y < height; y++) {
|
|
135
|
-
for (let x = 0; x < width; x++) {
|
|
136
|
-
if (isSolid(x, y)) {
|
|
137
|
-
startX = x;
|
|
138
|
-
startY = y;
|
|
139
|
-
break searchLoop;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (startX === -1) return [];
|
|
145
|
-
|
|
146
341
|
const points: Point[] = [];
|
|
147
342
|
|
|
148
343
|
// Moore-Neighbor Tracing
|
|
@@ -175,83 +370,26 @@ export class ImageTracer {
|
|
|
175
370
|
|
|
176
371
|
do {
|
|
177
372
|
points.push({ x: cx, y: cy });
|
|
373
|
+
visited[cy * width + cx] = 1; // Mark as visited to avoid re-starting here
|
|
178
374
|
|
|
179
375
|
// Search for next solid neighbor in clockwise order, starting from backtrack
|
|
180
376
|
let found = false;
|
|
181
377
|
|
|
182
|
-
// We check 8 neighbors.
|
|
183
|
-
// Moore algorithm says: start from backtrack, go clockwise until you find a black pixel.
|
|
184
|
-
// The backtrack for the NEXT step will be the neighbor BEFORE the one we found.
|
|
185
|
-
|
|
186
378
|
for (let i = 0; i < 8; i++) {
|
|
187
|
-
// Index in neighbors array. Start from backtrack direction.
|
|
188
|
-
// Actually Moore algorithm typically starts from (backtrack + 1) % 8 ?
|
|
189
|
-
// Let's standard: Start searching clockwise from the pixel entered from.
|
|
190
|
-
|
|
191
379
|
const idx = (backtrack + 1 + i) % 8;
|
|
192
380
|
const nx = cx + neighbors[idx].x;
|
|
193
381
|
const ny = cy + neighbors[idx].y;
|
|
194
382
|
|
|
195
383
|
if (isSolid(nx, ny)) {
|
|
196
|
-
// Found next pixel P
|
|
197
384
|
cx = nx;
|
|
198
385
|
cy = ny;
|
|
199
|
-
|
|
200
|
-
// No, backtrack is the empty neighbor immediately counter-clockwise from the new P.
|
|
201
|
-
// In our loop, it's the previous index (idx - 1).
|
|
202
|
-
backtrack = (idx + 4) % 8; // Actually, backtrack direction relative to New P is opposite?
|
|
203
|
-
// Let's strictly follow Moore:
|
|
204
|
-
// Entering P from direction D. Start scan from D-1 (or D+something).
|
|
205
|
-
// Let's use the property:
|
|
206
|
-
// We entered P from `idx`. The previous check `idx-1` was empty.
|
|
207
|
-
// So for the next step, we can start checking from `idx-3` (approx 90 deg back) or `idx-2`.
|
|
208
|
-
// Standard Moore: Backtrack = neighbor index that was empty previously.
|
|
209
|
-
// Here, `idx` is the direction FROM old P TO new P.
|
|
210
|
-
// The direction FROM new P TO old P is `(idx + 4) % 8`.
|
|
211
|
-
// We want to start scanning around new P.
|
|
212
|
-
// We start scanning from the neighbor that is "Left" of the incoming edge.
|
|
213
|
-
|
|
214
|
-
// Let's simplify: Start scanning from (EntryDirection + 5) % 8 ?
|
|
215
|
-
// EntryDirection is (idx). Backwards is (idx+4).
|
|
216
|
-
// We want to start from the white pixel we just passed.
|
|
217
|
-
// That was `(idx - 1)`.
|
|
218
|
-
// Direction FROM old P to (idx-1) is neighbors[idx-1].
|
|
219
|
-
// We want direction FROM new P to that same white pixel? No.
|
|
220
|
-
|
|
221
|
-
// Working Heuristic:
|
|
222
|
-
// Next search starts from (current_incoming_direction + 4 + 1) ?
|
|
223
|
-
// Let's set backtrack to point to the neighbor we entered from, then rotate CCW?
|
|
224
|
-
// Let's use: start search from `(idx + 5) % 8`.
|
|
225
|
-
// Why? idx is direction 0..7. Back is idx+4. +1 is clockwise.
|
|
226
|
-
// We want counter-clockwise.
|
|
227
|
-
|
|
228
|
-
backtrack = (idx + 4 + 1) % 8; // Start searching from neighbor "after" the one we came from (CCW)?
|
|
229
|
-
// Wait, loop above is Clockwise.
|
|
230
|
-
// To trace outer boundary counter-clockwise, we scan neighbors counter-clockwise?
|
|
231
|
-
// Or trace outer boundary clockwise, scan neighbors clockwise.
|
|
232
|
-
|
|
233
|
-
// Let's trace Clockwise.
|
|
234
|
-
// Scan neighbors Clockwise.
|
|
235
|
-
// Start scan from (IncomingDirection + 5) % 8. (Backwards + 1 CW).
|
|
236
|
-
backtrack = (idx + 4 + 1) % 8; // Backwards + 1 step CW.
|
|
237
|
-
|
|
238
|
-
// But wait, if we are tracing an 1-pixel line, we turn around (backtrack).
|
|
239
|
-
// Let's try `(idx + 5) % 8` if neighbors are ordered CW.
|
|
240
|
-
// neighbors[0] is Up. [2] is Right.
|
|
241
|
-
// If we move Right (idx=2). Back is Left (6).
|
|
242
|
-
// We want to check UpLeft (7), Up (0), UpRight (1)...
|
|
243
|
-
// So start from 7? That is 6+1.
|
|
244
|
-
// So `(idx + 4 + 1) % 8` seems correct.
|
|
245
|
-
|
|
386
|
+
backtrack = (idx + 4 + 1) % 8;
|
|
246
387
|
found = true;
|
|
247
388
|
break;
|
|
248
389
|
}
|
|
249
390
|
}
|
|
250
391
|
|
|
251
|
-
if (!found)
|
|
252
|
-
// Isolated pixel
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
392
|
+
if (!found) break;
|
|
255
393
|
|
|
256
394
|
steps++;
|
|
257
395
|
} while ((cx !== startX || cy !== startY) && steps < maxSteps);
|
|
@@ -319,42 +457,18 @@ export class ImageTracer {
|
|
|
319
457
|
points: Point[],
|
|
320
458
|
targetWidth: number,
|
|
321
459
|
targetHeight: number,
|
|
460
|
+
bounds: { minX: number; minY: number; width: number; height: number },
|
|
322
461
|
): Point[] {
|
|
323
462
|
if (points.length === 0) return points;
|
|
324
463
|
|
|
325
|
-
|
|
326
|
-
let minX = Infinity,
|
|
327
|
-
minY = Infinity,
|
|
328
|
-
maxX = -Infinity,
|
|
329
|
-
maxY = -Infinity;
|
|
330
|
-
for (const p of points) {
|
|
331
|
-
if (p.x < minX) minX = p.x;
|
|
332
|
-
if (p.y < minY) minY = p.y;
|
|
333
|
-
if (p.x > maxX) maxX = p.x;
|
|
334
|
-
if (p.y > maxY) maxY = p.y;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const srcW = maxX - minX;
|
|
338
|
-
const srcH = maxY - minY;
|
|
339
|
-
|
|
340
|
-
if (srcW === 0 || srcH === 0) return points;
|
|
341
|
-
|
|
342
|
-
const scaleX = targetWidth / srcW;
|
|
343
|
-
const scaleY = targetHeight / srcH;
|
|
464
|
+
if (bounds.width === 0 || bounds.height === 0) return points;
|
|
344
465
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
// Let's just scale and align top-left to 0,0 for now, or center it?
|
|
348
|
-
// Dieline usually expects centered shape?
|
|
349
|
-
// geometry.ts createBaseShape aligns path.position = center.
|
|
350
|
-
// So the path data coordinates should probably be relative to 0,0 or centered.
|
|
351
|
-
// Paper.js Path(pathData) creates path in original coordinates.
|
|
352
|
-
// If we return points in 0..targetWidth, 0..targetHeight, paper will create it there.
|
|
353
|
-
// geometry.ts will then center it.
|
|
466
|
+
const scaleX = targetWidth / bounds.width;
|
|
467
|
+
const scaleY = targetHeight / bounds.height;
|
|
354
468
|
|
|
355
469
|
return points.map((p) => ({
|
|
356
|
-
x: (p.x - minX) * scaleX,
|
|
357
|
-
y: (p.y - minY) * scaleY,
|
|
470
|
+
x: (p.x - bounds.minX) * scaleX,
|
|
471
|
+
y: (p.y - bounds.minY) * scaleY,
|
|
358
472
|
}));
|
|
359
473
|
}
|
|
360
474
|
|