@pooder/kit 5.2.0 → 5.3.1
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/.test-dist/src/extensions/background.js +203 -0
- package/.test-dist/src/extensions/bridgeSelection.js +20 -0
- package/.test-dist/src/extensions/constraints.js +237 -0
- package/.test-dist/src/extensions/dieline.js +828 -0
- package/.test-dist/src/extensions/edgeScale.js +12 -0
- package/.test-dist/src/extensions/feature.js +825 -0
- package/.test-dist/src/extensions/featureComplete.js +32 -0
- package/.test-dist/src/extensions/film.js +167 -0
- package/.test-dist/src/extensions/geometry.js +545 -0
- package/.test-dist/src/extensions/image.js +1529 -0
- package/.test-dist/src/extensions/index.js +30 -0
- package/.test-dist/src/extensions/maskOps.js +279 -0
- package/.test-dist/src/extensions/mirror.js +104 -0
- package/.test-dist/src/extensions/ruler.js +345 -0
- package/.test-dist/src/extensions/sceneLayout.js +96 -0
- package/.test-dist/src/extensions/sceneLayoutModel.js +196 -0
- package/.test-dist/src/extensions/sceneVisibility.js +62 -0
- package/.test-dist/src/extensions/size.js +331 -0
- package/.test-dist/src/extensions/tracer.js +538 -0
- package/.test-dist/src/extensions/white-ink.js +1190 -0
- package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
- package/.test-dist/src/index.js +2 -19
- package/.test-dist/src/services/CanvasService.js +249 -0
- package/.test-dist/src/services/ViewportSystem.js +76 -0
- package/.test-dist/src/services/index.js +24 -0
- package/.test-dist/src/services/renderSpec.js +2 -0
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +519 -395
- package/dist/index.mjs +519 -395
- package/package.json +1 -1
- package/src/extensions/dieline.ts +66 -17
- package/src/extensions/geometry.ts +36 -3
- package/src/extensions/image.ts +2 -0
- package/src/extensions/maskOps.ts +84 -18
- package/src/extensions/sceneLayoutModel.ts +10 -0
- package/src/extensions/tracer.ts +360 -389
- package/src/extensions/white-ink.ts +125 -2
package/dist/index.js
CHANGED
|
@@ -395,7 +395,17 @@ function selectOuterChain(args) {
|
|
|
395
395
|
}
|
|
396
396
|
function createBaseShape(options) {
|
|
397
397
|
var _a;
|
|
398
|
-
const {
|
|
398
|
+
const {
|
|
399
|
+
shape,
|
|
400
|
+
width,
|
|
401
|
+
height,
|
|
402
|
+
radius,
|
|
403
|
+
x,
|
|
404
|
+
y,
|
|
405
|
+
pathData,
|
|
406
|
+
customSourceWidthPx,
|
|
407
|
+
customSourceHeightPx
|
|
408
|
+
} = options;
|
|
399
409
|
const center = new import_paper.default.Point(x, y);
|
|
400
410
|
if (shape === "rect") {
|
|
401
411
|
return new import_paper.default.Path.Rectangle({
|
|
@@ -421,10 +431,21 @@ function createBaseShape(options) {
|
|
|
421
431
|
single.pathData = pathData;
|
|
422
432
|
return single;
|
|
423
433
|
})();
|
|
424
|
-
|
|
434
|
+
const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
|
|
435
|
+
const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
|
|
436
|
+
if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
|
|
437
|
+
const targetLeft = x - width / 2;
|
|
438
|
+
const targetTop = y - height / 2;
|
|
439
|
+
path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
|
|
440
|
+
path.translate(new import_paper.default.Point(targetLeft, targetTop));
|
|
441
|
+
return path;
|
|
442
|
+
}
|
|
425
443
|
if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
|
|
444
|
+
path.position = center;
|
|
426
445
|
path.scale(width / path.bounds.width, height / path.bounds.height);
|
|
446
|
+
return path;
|
|
427
447
|
}
|
|
448
|
+
path.position = center;
|
|
428
449
|
return path;
|
|
429
450
|
} else {
|
|
430
451
|
return new import_paper.default.Path.Rectangle({
|
|
@@ -1059,6 +1080,10 @@ function buildSceneGeometry(configService, layout) {
|
|
|
1059
1080
|
"mm"
|
|
1060
1081
|
);
|
|
1061
1082
|
const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
|
|
1083
|
+
const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
|
|
1084
|
+
const sourceHeight = Number(
|
|
1085
|
+
configService.get("dieline.customSourceHeightPx", 0)
|
|
1086
|
+
);
|
|
1062
1087
|
return {
|
|
1063
1088
|
shape: configService.get("dieline.shape", "rect"),
|
|
1064
1089
|
unit: "mm",
|
|
@@ -1070,7 +1095,9 @@ function buildSceneGeometry(configService, layout) {
|
|
|
1070
1095
|
radius: radiusMm * layout.scale,
|
|
1071
1096
|
offset,
|
|
1072
1097
|
scale: layout.scale,
|
|
1073
|
-
pathData: configService.get("dieline.pathData")
|
|
1098
|
+
pathData: configService.get("dieline.pathData"),
|
|
1099
|
+
customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
|
|
1100
|
+
customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
|
|
1074
1101
|
};
|
|
1075
1102
|
}
|
|
1076
1103
|
|
|
@@ -2497,6 +2524,8 @@ var ImageTool = class {
|
|
|
2497
2524
|
this.normalizeItem({
|
|
2498
2525
|
...item,
|
|
2499
2526
|
url,
|
|
2527
|
+
// Keep original source for next image-tool session editing,
|
|
2528
|
+
// and use committedUrl as non-image-tools render source.
|
|
2500
2529
|
sourceUrl,
|
|
2501
2530
|
committedUrl: url
|
|
2502
2531
|
})
|
|
@@ -2993,15 +3022,19 @@ function analyzeAlpha(imageData, alphaOpaqueCutoff) {
|
|
|
2993
3022
|
};
|
|
2994
3023
|
}
|
|
2995
3024
|
function circularMorphology(mask, width, height, radius, op) {
|
|
2996
|
-
const
|
|
3025
|
+
const r = Math.max(0, Math.floor(radius));
|
|
3026
|
+
if (r <= 0) {
|
|
3027
|
+
return mask.slice();
|
|
3028
|
+
}
|
|
3029
|
+
const dilateDisk = (m, radiusPx) => {
|
|
2997
3030
|
const horizontalDist = new Int32Array(width * height);
|
|
2998
3031
|
for (let y = 0; y < height; y++) {
|
|
2999
|
-
let lastSolid = -
|
|
3032
|
+
let lastSolid = -radiusPx * 2;
|
|
3000
3033
|
for (let x = 0; x < width; x++) {
|
|
3001
3034
|
if (m[y * width + x]) lastSolid = x;
|
|
3002
3035
|
horizontalDist[y * width + x] = x - lastSolid;
|
|
3003
3036
|
}
|
|
3004
|
-
lastSolid = width +
|
|
3037
|
+
lastSolid = width + radiusPx * 2;
|
|
3005
3038
|
for (let x = width - 1; x >= 0; x--) {
|
|
3006
3039
|
if (m[y * width + x]) lastSolid = x;
|
|
3007
3040
|
horizontalDist[y * width + x] = Math.min(
|
|
@@ -3011,12 +3044,12 @@ function circularMorphology(mask, width, height, radius, op) {
|
|
|
3011
3044
|
}
|
|
3012
3045
|
}
|
|
3013
3046
|
const result = new Uint8Array(width * height);
|
|
3014
|
-
const r2 =
|
|
3047
|
+
const r2 = radiusPx * radiusPx;
|
|
3015
3048
|
for (let x = 0; x < width; x++) {
|
|
3016
3049
|
for (let y = 0; y < height; y++) {
|
|
3017
3050
|
let found = false;
|
|
3018
|
-
const minY = Math.max(0, y -
|
|
3019
|
-
const maxY = Math.min(height - 1, y +
|
|
3051
|
+
const minY = Math.max(0, y - radiusPx);
|
|
3052
|
+
const maxY = Math.min(height - 1, y + radiusPx);
|
|
3020
3053
|
for (let dy = minY; dy <= maxY; dy++) {
|
|
3021
3054
|
const dY = dy - y;
|
|
3022
3055
|
const hDist = horizontalDist[dy * width + x];
|
|
@@ -3030,23 +3063,62 @@ function circularMorphology(mask, width, height, radius, op) {
|
|
|
3030
3063
|
}
|
|
3031
3064
|
return result;
|
|
3032
3065
|
};
|
|
3033
|
-
const
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3066
|
+
const erodeDiamond = (m, radiusPx) => {
|
|
3067
|
+
if (radiusPx <= 0) return m.slice();
|
|
3068
|
+
let current = m;
|
|
3069
|
+
for (let step = 0; step < radiusPx; step++) {
|
|
3070
|
+
const next = new Uint8Array(width * height);
|
|
3071
|
+
for (let y = 1; y < height - 1; y++) {
|
|
3072
|
+
const row = y * width;
|
|
3073
|
+
for (let x = 1; x < width - 1; x++) {
|
|
3074
|
+
const idx = row + x;
|
|
3075
|
+
if (current[idx] && current[idx - 1] && current[idx + 1] && current[idx - width] && current[idx + width]) {
|
|
3076
|
+
next[idx] = 1;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
current = next;
|
|
3081
|
+
}
|
|
3082
|
+
return current;
|
|
3083
|
+
};
|
|
3084
|
+
const restoreBridgePixels = (source, eroded) => {
|
|
3085
|
+
const restored = eroded.slice();
|
|
3086
|
+
for (let y = 1; y < height - 1; y++) {
|
|
3087
|
+
const row = y * width;
|
|
3088
|
+
for (let x = 1; x < width - 1; x++) {
|
|
3089
|
+
const idx = row + x;
|
|
3090
|
+
if (!source[idx] || restored[idx]) continue;
|
|
3091
|
+
const up = source[idx - width] === 1;
|
|
3092
|
+
const down = source[idx + width] === 1;
|
|
3093
|
+
const left = source[idx - 1] === 1;
|
|
3094
|
+
const right = source[idx + 1] === 1;
|
|
3095
|
+
const upLeft = source[idx - width - 1] === 1;
|
|
3096
|
+
const upRight = source[idx - width + 1] === 1;
|
|
3097
|
+
const downLeft = source[idx + width - 1] === 1;
|
|
3098
|
+
const downRight = source[idx + width + 1] === 1;
|
|
3099
|
+
const keepsBridge = left && right || up && down || upLeft && downRight || upRight && downLeft;
|
|
3100
|
+
if (keepsBridge) {
|
|
3101
|
+
restored[idx] = 1;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
return restored;
|
|
3106
|
+
};
|
|
3107
|
+
const erodePreservingBridges = (m, radiusPx) => {
|
|
3108
|
+
const eroded = erodeDiamond(m, radiusPx);
|
|
3109
|
+
return restoreBridgePixels(m, eroded);
|
|
3040
3110
|
};
|
|
3041
3111
|
switch (op) {
|
|
3042
3112
|
case "dilate":
|
|
3043
|
-
return
|
|
3113
|
+
return dilateDisk(mask, r);
|
|
3044
3114
|
case "erode":
|
|
3045
|
-
return
|
|
3046
|
-
case "closing":
|
|
3047
|
-
|
|
3115
|
+
return erodePreservingBridges(mask, r);
|
|
3116
|
+
case "closing": {
|
|
3117
|
+
const erodeRadius = Math.max(1, Math.floor(r * 0.65));
|
|
3118
|
+
return erodePreservingBridges(dilateDisk(mask, r), erodeRadius);
|
|
3119
|
+
}
|
|
3048
3120
|
case "opening":
|
|
3049
|
-
return
|
|
3121
|
+
return dilateDisk(erodePreservingBridges(mask, r), r);
|
|
3050
3122
|
default:
|
|
3051
3123
|
return mask;
|
|
3052
3124
|
}
|
|
@@ -3109,78 +3181,6 @@ function fillHoles(mask, width, height) {
|
|
|
3109
3181
|
}
|
|
3110
3182
|
return filledMask;
|
|
3111
3183
|
}
|
|
3112
|
-
function countForeground(mask) {
|
|
3113
|
-
let c = 0;
|
|
3114
|
-
for (let i = 0; i < mask.length; i++) c += mask[i] ? 1 : 0;
|
|
3115
|
-
return c;
|
|
3116
|
-
}
|
|
3117
|
-
function isMaskConnected8(mask, width, height) {
|
|
3118
|
-
const total = countForeground(mask);
|
|
3119
|
-
if (total === 0) return true;
|
|
3120
|
-
let start = -1;
|
|
3121
|
-
for (let i = 0; i < mask.length; i++) {
|
|
3122
|
-
if (mask[i]) {
|
|
3123
|
-
start = i;
|
|
3124
|
-
break;
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
if (start === -1) return true;
|
|
3128
|
-
const visited = new Uint8Array(mask.length);
|
|
3129
|
-
const queue = [start];
|
|
3130
|
-
visited[start] = 1;
|
|
3131
|
-
let seen = 1;
|
|
3132
|
-
let head = 0;
|
|
3133
|
-
while (head < queue.length) {
|
|
3134
|
-
const idx = queue[head++];
|
|
3135
|
-
const x = idx % width;
|
|
3136
|
-
const y = (idx - x) / width;
|
|
3137
|
-
for (let dy = -1; dy <= 1; dy++) {
|
|
3138
|
-
const ny = y + dy;
|
|
3139
|
-
if (ny < 0 || ny >= height) continue;
|
|
3140
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
3141
|
-
if (dx === 0 && dy === 0) continue;
|
|
3142
|
-
const nx = x + dx;
|
|
3143
|
-
if (nx < 0 || nx >= width) continue;
|
|
3144
|
-
const nidx = ny * width + nx;
|
|
3145
|
-
if (mask[nidx] && !visited[nidx]) {
|
|
3146
|
-
visited[nidx] = 1;
|
|
3147
|
-
queue.push(nidx);
|
|
3148
|
-
seen++;
|
|
3149
|
-
}
|
|
3150
|
-
}
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
return seen === total;
|
|
3154
|
-
}
|
|
3155
|
-
function findMinimalConnectRadius(mask, width, height, maxRadius) {
|
|
3156
|
-
if (maxRadius <= 0) return 0;
|
|
3157
|
-
if (isMaskConnected8(mask, width, height)) return 0;
|
|
3158
|
-
let low = 0;
|
|
3159
|
-
let high = 1;
|
|
3160
|
-
while (high <= maxRadius) {
|
|
3161
|
-
const closed = circularMorphology(mask, width, height, high, "closing");
|
|
3162
|
-
if (isMaskConnected8(closed, width, height)) break;
|
|
3163
|
-
high *= 2;
|
|
3164
|
-
}
|
|
3165
|
-
if (high > maxRadius) high = maxRadius;
|
|
3166
|
-
if (!isMaskConnected8(
|
|
3167
|
-
circularMorphology(mask, width, height, high, "closing"),
|
|
3168
|
-
width,
|
|
3169
|
-
height
|
|
3170
|
-
)) {
|
|
3171
|
-
return high;
|
|
3172
|
-
}
|
|
3173
|
-
while (low + 1 < high) {
|
|
3174
|
-
const mid = Math.floor((low + high) / 2);
|
|
3175
|
-
const closed = circularMorphology(mask, width, height, mid, "closing");
|
|
3176
|
-
if (isMaskConnected8(closed, width, height)) {
|
|
3177
|
-
high = mid;
|
|
3178
|
-
} else {
|
|
3179
|
-
low = mid;
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
return high;
|
|
3183
|
-
}
|
|
3184
3184
|
function polygonSignedArea(points) {
|
|
3185
3185
|
if (points.length < 3) return 0;
|
|
3186
3186
|
let sum = 0;
|
|
@@ -3204,10 +3204,19 @@ var ImageTracer = class {
|
|
|
3204
3204
|
return pathData;
|
|
3205
3205
|
}
|
|
3206
3206
|
static async traceWithBounds(imageUrl, options = {}) {
|
|
3207
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i
|
|
3207
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
3208
3208
|
const img = await this.loadImage(imageUrl);
|
|
3209
3209
|
const width = img.width;
|
|
3210
3210
|
const height = img.height;
|
|
3211
|
+
if (width <= 0 || height <= 0) {
|
|
3212
|
+
const w = (_a = options.scaleToWidth) != null ? _a : 0;
|
|
3213
|
+
const h = (_b = options.scaleToHeight) != null ? _b : 0;
|
|
3214
|
+
return {
|
|
3215
|
+
pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
|
|
3216
|
+
baseBounds: { x: 0, y: 0, width: w, height: h },
|
|
3217
|
+
bounds: { x: 0, y: 0, width: w, height: h }
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3211
3220
|
const debug = options.debug === true;
|
|
3212
3221
|
const debugLog = (message, payload) => {
|
|
3213
3222
|
if (!debug) return;
|
|
@@ -3224,96 +3233,178 @@ var ImageTracer = class {
|
|
|
3224
3233
|
if (!ctx) throw new Error("Could not get 2D context");
|
|
3225
3234
|
ctx.drawImage(img, 0, 0);
|
|
3226
3235
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
3227
|
-
const threshold = (
|
|
3228
|
-
const
|
|
3229
|
-
const
|
|
3230
|
-
const
|
|
3231
|
-
const
|
|
3232
|
-
|
|
3233
|
-
|
|
3236
|
+
const threshold = (_c = options.threshold) != null ? _c : 10;
|
|
3237
|
+
const expand = Math.max(0, Math.floor((_d = options.expand) != null ? _d : 0));
|
|
3238
|
+
const simplifyTolerance = (_e = options.simplifyTolerance) != null ? _e : 2.5;
|
|
3239
|
+
const useSmoothing = options.smoothing !== false;
|
|
3240
|
+
const componentMode = "all";
|
|
3241
|
+
const minComponentArea = 0;
|
|
3242
|
+
const maxDim = Math.max(width, height);
|
|
3243
|
+
const maskMode = "auto";
|
|
3244
|
+
const whiteThreshold = 240;
|
|
3245
|
+
const alphaOpaqueCutoff = 250;
|
|
3246
|
+
const preprocessDilateRadius = Math.max(
|
|
3247
|
+
2,
|
|
3248
|
+
Math.floor(Math.max(maxDim * 0.012, expand * 0.35))
|
|
3249
|
+
);
|
|
3250
|
+
const preprocessErodeRadius = Math.max(
|
|
3251
|
+
1,
|
|
3252
|
+
Math.floor(preprocessDilateRadius * 0.65)
|
|
3253
|
+
);
|
|
3254
|
+
const smoothDilateRadius = Math.max(
|
|
3255
|
+
1,
|
|
3256
|
+
Math.floor(preprocessDilateRadius * 0.25)
|
|
3234
3257
|
);
|
|
3235
|
-
const
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
const
|
|
3258
|
+
const smoothErodeRadius = Math.max(1, Math.floor(smoothDilateRadius * 0.8));
|
|
3259
|
+
const connectStartDilateRadius = Math.max(
|
|
3260
|
+
1,
|
|
3261
|
+
Math.floor(Math.max(maxDim * 6e-3, expand * 0.2))
|
|
3262
|
+
);
|
|
3263
|
+
const connectMaxDilateRadius = Math.max(
|
|
3264
|
+
connectStartDilateRadius,
|
|
3265
|
+
Math.floor(Math.max(maxDim * 0.2, expand * 2.5))
|
|
3266
|
+
);
|
|
3267
|
+
const connectErodeRatio = 0.65;
|
|
3241
3268
|
debugLog("traceWithBounds:start", {
|
|
3242
3269
|
width,
|
|
3243
3270
|
height,
|
|
3244
3271
|
threshold,
|
|
3245
|
-
radius,
|
|
3246
3272
|
expand,
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3273
|
+
simplifyTolerance,
|
|
3274
|
+
smoothing: useSmoothing,
|
|
3275
|
+
strategy: {
|
|
3276
|
+
maskMode,
|
|
3277
|
+
whiteThreshold,
|
|
3278
|
+
alphaOpaqueCutoff,
|
|
3279
|
+
fillHoles: true,
|
|
3280
|
+
preprocessDilateRadius,
|
|
3281
|
+
preprocessErodeRadius,
|
|
3282
|
+
smoothDilateRadius,
|
|
3283
|
+
smoothErodeRadius,
|
|
3284
|
+
connectEnabled: true,
|
|
3285
|
+
connectStartDilateRadius,
|
|
3286
|
+
connectMaxDilateRadius,
|
|
3287
|
+
connectErodeRatio
|
|
3288
|
+
}
|
|
3263
3289
|
});
|
|
3264
|
-
const padding =
|
|
3290
|
+
const padding = Math.max(
|
|
3291
|
+
preprocessDilateRadius,
|
|
3292
|
+
smoothDilateRadius,
|
|
3293
|
+
connectMaxDilateRadius,
|
|
3294
|
+
expand
|
|
3295
|
+
) + 2;
|
|
3265
3296
|
const paddedWidth = width + padding * 2;
|
|
3266
3297
|
const paddedHeight = height + padding * 2;
|
|
3298
|
+
const summarizeMaskContours = (m) => {
|
|
3299
|
+
const summary = this.summarizeAllContours(
|
|
3300
|
+
m,
|
|
3301
|
+
paddedWidth,
|
|
3302
|
+
paddedHeight,
|
|
3303
|
+
minComponentArea
|
|
3304
|
+
);
|
|
3305
|
+
return {
|
|
3306
|
+
rawContourCount: summary.rawCount,
|
|
3307
|
+
selectedContourCount: summary.selectedCount
|
|
3308
|
+
};
|
|
3309
|
+
};
|
|
3267
3310
|
let mask = createMask(imageData, {
|
|
3268
3311
|
threshold,
|
|
3269
3312
|
padding,
|
|
3270
3313
|
paddedWidth,
|
|
3271
3314
|
paddedHeight,
|
|
3272
|
-
maskMode
|
|
3273
|
-
whiteThreshold
|
|
3315
|
+
maskMode,
|
|
3316
|
+
whiteThreshold,
|
|
3274
3317
|
alphaOpaqueCutoff
|
|
3275
3318
|
});
|
|
3276
|
-
if (
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3319
|
+
if (debug) {
|
|
3320
|
+
debugLog(
|
|
3321
|
+
"traceWithBounds:mask:after-create",
|
|
3322
|
+
summarizeMaskContours(mask)
|
|
3323
|
+
);
|
|
3281
3324
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3325
|
+
mask = circularMorphology(
|
|
3326
|
+
mask,
|
|
3327
|
+
paddedWidth,
|
|
3328
|
+
paddedHeight,
|
|
3329
|
+
preprocessDilateRadius,
|
|
3330
|
+
"dilate"
|
|
3331
|
+
);
|
|
3332
|
+
mask = fillHoles(mask, paddedWidth, paddedHeight);
|
|
3333
|
+
mask = circularMorphology(
|
|
3334
|
+
mask,
|
|
3335
|
+
paddedWidth,
|
|
3336
|
+
paddedHeight,
|
|
3337
|
+
preprocessErodeRadius,
|
|
3338
|
+
"erode"
|
|
3339
|
+
);
|
|
3340
|
+
mask = fillHoles(mask, paddedWidth, paddedHeight);
|
|
3341
|
+
if (debug) {
|
|
3342
|
+
debugLog("traceWithBounds:mask:after-preprocess", {
|
|
3343
|
+
dilateRadius: preprocessDilateRadius,
|
|
3344
|
+
erodeRadius: preprocessErodeRadius,
|
|
3345
|
+
...summarizeMaskContours(mask)
|
|
3346
|
+
});
|
|
3285
3347
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3348
|
+
mask = circularMorphology(
|
|
3349
|
+
mask,
|
|
3350
|
+
paddedWidth,
|
|
3351
|
+
paddedHeight,
|
|
3352
|
+
smoothDilateRadius,
|
|
3353
|
+
"dilate"
|
|
3289
3354
|
);
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3355
|
+
mask = fillHoles(mask, paddedWidth, paddedHeight);
|
|
3356
|
+
mask = circularMorphology(
|
|
3357
|
+
mask,
|
|
3358
|
+
paddedWidth,
|
|
3359
|
+
paddedHeight,
|
|
3360
|
+
smoothErodeRadius,
|
|
3361
|
+
"erode"
|
|
3362
|
+
);
|
|
3363
|
+
mask = fillHoles(mask, paddedWidth, paddedHeight);
|
|
3364
|
+
if (debug) {
|
|
3365
|
+
debugLog("traceWithBounds:mask:after-smooth", {
|
|
3366
|
+
dilateRadius: smoothDilateRadius,
|
|
3367
|
+
erodeRadius: smoothErodeRadius,
|
|
3368
|
+
...summarizeMaskContours(mask)
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
const beforeConnectSummary = summarizeMaskContours(mask);
|
|
3372
|
+
if (beforeConnectSummary.selectedContourCount <= 1) {
|
|
3373
|
+
debugLog("traceWithBounds:mask:connect-skipped", {
|
|
3374
|
+
reason: "already-single-component",
|
|
3375
|
+
before: beforeConnectSummary
|
|
3376
|
+
});
|
|
3377
|
+
} else {
|
|
3378
|
+
const connectResult = this.findForceConnectResult(
|
|
3301
3379
|
mask,
|
|
3302
3380
|
paddedWidth,
|
|
3303
3381
|
paddedHeight,
|
|
3304
|
-
|
|
3382
|
+
minComponentArea,
|
|
3383
|
+
connectStartDilateRadius,
|
|
3384
|
+
connectMaxDilateRadius,
|
|
3385
|
+
connectErodeRatio
|
|
3305
3386
|
);
|
|
3306
|
-
if (
|
|
3307
|
-
mask
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
}
|
|
3387
|
+
if (debug) {
|
|
3388
|
+
debugLog("traceWithBounds:mask:after-connect", {
|
|
3389
|
+
before: beforeConnectSummary,
|
|
3390
|
+
appliedDilateRadius: connectResult.appliedDilateRadius,
|
|
3391
|
+
appliedErodeRadius: connectResult.appliedErodeRadius,
|
|
3392
|
+
reachedSingleComponent: connectResult.reachedSingleComponent,
|
|
3393
|
+
after: {
|
|
3394
|
+
rawContourCount: connectResult.rawContourCount,
|
|
3395
|
+
selectedContourCount: connectResult.selectedContourCount
|
|
3396
|
+
}
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
mask = connectResult.mask;
|
|
3400
|
+
}
|
|
3401
|
+
if (debug) {
|
|
3402
|
+
const afterConnectSummary = summarizeMaskContours(mask);
|
|
3403
|
+
if (afterConnectSummary.selectedContourCount > 1) {
|
|
3404
|
+
debugLog("traceWithBounds:mask:connect-warning", {
|
|
3405
|
+
reason: "still-multi-component-after-connect-search",
|
|
3406
|
+
summary: afterConnectSummary
|
|
3407
|
+
});
|
|
3317
3408
|
}
|
|
3318
3409
|
}
|
|
3319
3410
|
const baseMask = mask;
|
|
@@ -3328,8 +3419,8 @@ var ImageTracer = class {
|
|
|
3328
3419
|
minComponentArea
|
|
3329
3420
|
);
|
|
3330
3421
|
if (!baseContours.length) {
|
|
3331
|
-
const w = (
|
|
3332
|
-
const h = (
|
|
3422
|
+
const w = (_f = options.scaleToWidth) != null ? _f : width;
|
|
3423
|
+
const h = (_g = options.scaleToHeight) != null ? _g : height;
|
|
3333
3424
|
debugLog("fallback:no-base-contour", { width: w, height: h });
|
|
3334
3425
|
return {
|
|
3335
3426
|
pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
|
|
@@ -3338,18 +3429,14 @@ var ImageTracer = class {
|
|
|
3338
3429
|
};
|
|
3339
3430
|
}
|
|
3340
3431
|
const baseUnpaddedContours = baseContours.map(
|
|
3341
|
-
(contour) =>
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
})),
|
|
3346
|
-
width,
|
|
3347
|
-
height
|
|
3348
|
-
)
|
|
3432
|
+
(contour) => contour.map((p) => ({
|
|
3433
|
+
x: p.x - padding,
|
|
3434
|
+
y: p.y - padding
|
|
3435
|
+
}))
|
|
3349
3436
|
).filter((contour) => contour.length > 2);
|
|
3350
3437
|
if (!baseUnpaddedContours.length) {
|
|
3351
|
-
const w = (
|
|
3352
|
-
const h = (
|
|
3438
|
+
const w = (_h = options.scaleToWidth) != null ? _h : width;
|
|
3439
|
+
const h = (_i = options.scaleToHeight) != null ? _i : height;
|
|
3353
3440
|
debugLog("fallback:empty-base-contours", { width: w, height: h });
|
|
3354
3441
|
return {
|
|
3355
3442
|
pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
|
|
@@ -3394,14 +3481,10 @@ var ImageTracer = class {
|
|
|
3394
3481
|
};
|
|
3395
3482
|
}
|
|
3396
3483
|
const expandedUnpaddedContours = expandedContours.map(
|
|
3397
|
-
(contour) =>
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
})),
|
|
3402
|
-
width,
|
|
3403
|
-
height
|
|
3404
|
-
)
|
|
3484
|
+
(contour) => contour.map((p) => ({
|
|
3485
|
+
x: p.x - padding,
|
|
3486
|
+
y: p.y - padding
|
|
3487
|
+
}))
|
|
3405
3488
|
).filter((contour) => contour.length > 2);
|
|
3406
3489
|
if (!expandedUnpaddedContours.length) {
|
|
3407
3490
|
debugLog("fallback:empty-expanded-contours", {
|
|
@@ -3434,39 +3517,64 @@ var ImageTracer = class {
|
|
|
3434
3517
|
options.scaleToHeight,
|
|
3435
3518
|
baseBounds
|
|
3436
3519
|
);
|
|
3437
|
-
baseBounds = this.boundsFromPoints(
|
|
3520
|
+
baseBounds = this.boundsFromPoints(
|
|
3521
|
+
this.flattenContours(baseScaledContours)
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
if (expand > 0) {
|
|
3525
|
+
const expectedExpandedBounds = {
|
|
3526
|
+
x: baseBounds.x - expand,
|
|
3527
|
+
y: baseBounds.y - expand,
|
|
3528
|
+
width: baseBounds.width + expand * 2,
|
|
3529
|
+
height: baseBounds.height + expand * 2
|
|
3530
|
+
};
|
|
3531
|
+
if (expectedExpandedBounds.width > 0 && expectedExpandedBounds.height > 0 && globalBounds.width > 0 && globalBounds.height > 0) {
|
|
3532
|
+
const shouldNormalizeExpandBounds = Math.abs(globalBounds.x - expectedExpandedBounds.x) > 1 || Math.abs(globalBounds.y - expectedExpandedBounds.y) > 1 || Math.abs(globalBounds.width - expectedExpandedBounds.width) > 1 || Math.abs(globalBounds.height - expectedExpandedBounds.height) > 1;
|
|
3533
|
+
if (shouldNormalizeExpandBounds) {
|
|
3534
|
+
const beforeNormalize = globalBounds;
|
|
3535
|
+
finalContours = this.translateContours(
|
|
3536
|
+
this.scaleContours(
|
|
3537
|
+
finalContours,
|
|
3538
|
+
expectedExpandedBounds.width,
|
|
3539
|
+
expectedExpandedBounds.height,
|
|
3540
|
+
globalBounds
|
|
3541
|
+
),
|
|
3542
|
+
expectedExpandedBounds.x,
|
|
3543
|
+
expectedExpandedBounds.y
|
|
3544
|
+
);
|
|
3545
|
+
globalBounds = this.boundsFromPoints(
|
|
3546
|
+
this.flattenContours(finalContours)
|
|
3547
|
+
);
|
|
3548
|
+
debugLog("traceWithBounds:expand-normalized", {
|
|
3549
|
+
expand,
|
|
3550
|
+
expectedExpandedBounds,
|
|
3551
|
+
beforeNormalize,
|
|
3552
|
+
afterNormalize: globalBounds
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3438
3556
|
}
|
|
3439
|
-
const useSmoothing = options.smoothing !== false;
|
|
3440
3557
|
debugLog("traceWithBounds:contours", {
|
|
3441
3558
|
baseContourCount: baseContoursRaw.length,
|
|
3442
3559
|
baseSelectedCount: baseContours.length,
|
|
3443
3560
|
expandedContourCount: expandedContoursRaw.length,
|
|
3444
3561
|
expandedSelectedCount: expandedContours.length,
|
|
3445
|
-
connectRadiusMax,
|
|
3446
|
-
appliedConnectRadius: rConnect,
|
|
3447
3562
|
baseBounds,
|
|
3448
3563
|
expandedBounds: globalBounds,
|
|
3449
3564
|
expandedDeltaX: globalBounds.width - baseBounds.width,
|
|
3450
3565
|
expandedDeltaY: globalBounds.height - baseBounds.height,
|
|
3566
|
+
expandedMayOverflowImageBounds: expand > 0,
|
|
3451
3567
|
useSmoothing,
|
|
3452
3568
|
componentMode
|
|
3453
3569
|
});
|
|
3454
3570
|
if (useSmoothing) {
|
|
3455
3571
|
return {
|
|
3456
|
-
pathData: this.contoursToSVGPaper(
|
|
3457
|
-
finalContours,
|
|
3458
|
-
(_n = options.simplifyTolerance) != null ? _n : 2.5
|
|
3459
|
-
),
|
|
3572
|
+
pathData: this.contoursToSVGPaper(finalContours, simplifyTolerance),
|
|
3460
3573
|
baseBounds,
|
|
3461
3574
|
bounds: globalBounds
|
|
3462
3575
|
};
|
|
3463
3576
|
} else {
|
|
3464
|
-
const simplifiedContours = finalContours.map(
|
|
3465
|
-
(points) => {
|
|
3466
|
-
var _a2;
|
|
3467
|
-
return this.douglasPeucker(points, (_a2 = options.simplifyTolerance) != null ? _a2 : 2);
|
|
3468
|
-
}
|
|
3469
|
-
).filter((points) => points.length > 2);
|
|
3577
|
+
const simplifiedContours = finalContours.map((points) => this.douglasPeucker(points, simplifyTolerance)).filter((points) => points.length > 2);
|
|
3470
3578
|
const pathData = this.contoursToSVG(simplifiedContours) || this.contoursToSVG(finalContours);
|
|
3471
3579
|
return {
|
|
3472
3580
|
pathData,
|
|
@@ -3529,39 +3637,101 @@ var ImageTracer = class {
|
|
|
3529
3637
|
}
|
|
3530
3638
|
return selected;
|
|
3531
3639
|
}
|
|
3532
|
-
static
|
|
3533
|
-
const
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3640
|
+
static summarizeAllContours(mask, width, height, minComponentArea) {
|
|
3641
|
+
const raw = this.traceAllContours(mask, width, height);
|
|
3642
|
+
const selected = this.selectContours(raw, "all", minComponentArea);
|
|
3643
|
+
return {
|
|
3644
|
+
rawCount: raw.length,
|
|
3645
|
+
selectedCount: selected.length
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
static findForceConnectResult(sourceMask, width, height, minComponentArea, startDilateRadius, maxDilateRadius, erodeRatio) {
|
|
3649
|
+
const initial = this.summarizeAllContours(
|
|
3650
|
+
sourceMask,
|
|
3651
|
+
width,
|
|
3652
|
+
height,
|
|
3653
|
+
minComponentArea
|
|
3654
|
+
);
|
|
3655
|
+
if (initial.selectedCount <= 1) {
|
|
3656
|
+
return {
|
|
3657
|
+
mask: sourceMask,
|
|
3658
|
+
appliedDilateRadius: 0,
|
|
3659
|
+
appliedErodeRadius: 0,
|
|
3660
|
+
reachedSingleComponent: true,
|
|
3661
|
+
rawContourCount: initial.rawCount,
|
|
3662
|
+
selectedContourCount: initial.selectedCount
|
|
3663
|
+
};
|
|
3549
3664
|
}
|
|
3550
|
-
|
|
3551
|
-
const
|
|
3552
|
-
|
|
3553
|
-
|
|
3665
|
+
const normalizedStart = Math.max(1, Math.floor(startDilateRadius));
|
|
3666
|
+
const normalizedMax = Math.max(
|
|
3667
|
+
normalizedStart,
|
|
3668
|
+
Math.floor(maxDilateRadius)
|
|
3669
|
+
);
|
|
3670
|
+
const normalizedErodeRatio = Math.max(0, erodeRatio);
|
|
3671
|
+
const evaluate = (dilateRadius) => {
|
|
3672
|
+
const erodeRadius = Math.max(
|
|
3673
|
+
1,
|
|
3674
|
+
Math.floor(dilateRadius * normalizedErodeRatio)
|
|
3675
|
+
);
|
|
3676
|
+
let mask = sourceMask;
|
|
3677
|
+
mask = circularMorphology(mask, width, height, dilateRadius, "dilate");
|
|
3678
|
+
mask = fillHoles(mask, width, height);
|
|
3679
|
+
mask = circularMorphology(mask, width, height, erodeRadius, "erode");
|
|
3680
|
+
mask = fillHoles(mask, width, height);
|
|
3681
|
+
const summary = this.summarizeAllContours(
|
|
3682
|
+
mask,
|
|
3683
|
+
width,
|
|
3684
|
+
height,
|
|
3685
|
+
minComponentArea
|
|
3686
|
+
);
|
|
3687
|
+
return {
|
|
3688
|
+
dilateRadius,
|
|
3689
|
+
erodeRadius,
|
|
3690
|
+
mask,
|
|
3691
|
+
rawCount: summary.rawCount,
|
|
3692
|
+
selectedCount: summary.selectedCount
|
|
3693
|
+
};
|
|
3694
|
+
};
|
|
3695
|
+
let low = normalizedStart - 1;
|
|
3696
|
+
let high = normalizedStart;
|
|
3697
|
+
let highResult = evaluate(high);
|
|
3698
|
+
while (high < normalizedMax && highResult.selectedCount > 1) {
|
|
3699
|
+
low = high;
|
|
3700
|
+
high = Math.min(
|
|
3701
|
+
normalizedMax,
|
|
3702
|
+
Math.max(high + 1, Math.floor(high * 1.6))
|
|
3703
|
+
);
|
|
3704
|
+
highResult = evaluate(high);
|
|
3705
|
+
}
|
|
3706
|
+
if (highResult.selectedCount > 1) {
|
|
3707
|
+
return {
|
|
3708
|
+
mask: highResult.mask,
|
|
3709
|
+
appliedDilateRadius: highResult.dilateRadius,
|
|
3710
|
+
appliedErodeRadius: highResult.erodeRadius,
|
|
3711
|
+
reachedSingleComponent: false,
|
|
3712
|
+
rawContourCount: highResult.rawCount,
|
|
3713
|
+
selectedContourCount: highResult.selectedCount
|
|
3714
|
+
};
|
|
3554
3715
|
}
|
|
3716
|
+
let best = highResult;
|
|
3555
3717
|
while (low + 1 < high) {
|
|
3556
3718
|
const mid = Math.floor((low + high) / 2);
|
|
3557
|
-
const
|
|
3558
|
-
if (
|
|
3719
|
+
const midResult = evaluate(mid);
|
|
3720
|
+
if (midResult.selectedCount <= 1) {
|
|
3721
|
+
best = midResult;
|
|
3559
3722
|
high = mid;
|
|
3560
3723
|
} else {
|
|
3561
3724
|
low = mid;
|
|
3562
3725
|
}
|
|
3563
3726
|
}
|
|
3564
|
-
return
|
|
3727
|
+
return {
|
|
3728
|
+
mask: best.mask,
|
|
3729
|
+
appliedDilateRadius: best.dilateRadius,
|
|
3730
|
+
appliedErodeRadius: best.erodeRadius,
|
|
3731
|
+
reachedSingleComponent: true,
|
|
3732
|
+
rawContourCount: best.rawCount,
|
|
3733
|
+
selectedContourCount: best.selectedCount
|
|
3734
|
+
};
|
|
3565
3735
|
}
|
|
3566
3736
|
static selectContours(contours, mode, minComponentArea) {
|
|
3567
3737
|
if (!contours.length) return [];
|
|
@@ -3603,154 +3773,6 @@ var ImageTracer = class {
|
|
|
3603
3773
|
height: maxY - minY
|
|
3604
3774
|
};
|
|
3605
3775
|
}
|
|
3606
|
-
static createMask(imageData, threshold, padding, paddedWidth, paddedHeight) {
|
|
3607
|
-
const { width, height, data } = imageData;
|
|
3608
|
-
const mask = new Uint8Array(paddedWidth * paddedHeight);
|
|
3609
|
-
let hasTransparency = false;
|
|
3610
|
-
for (let i = 3; i < data.length; i += 4) {
|
|
3611
|
-
if (data[i] < 255) {
|
|
3612
|
-
hasTransparency = true;
|
|
3613
|
-
break;
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
for (let y = 0; y < height; y++) {
|
|
3617
|
-
for (let x = 0; x < width; x++) {
|
|
3618
|
-
const srcIdx = (y * width + x) * 4;
|
|
3619
|
-
const r = data[srcIdx];
|
|
3620
|
-
const g = data[srcIdx + 1];
|
|
3621
|
-
const b = data[srcIdx + 2];
|
|
3622
|
-
const a = data[srcIdx + 3];
|
|
3623
|
-
const destIdx = (y + padding) * paddedWidth + (x + padding);
|
|
3624
|
-
if (hasTransparency) {
|
|
3625
|
-
if (a > threshold) {
|
|
3626
|
-
mask[destIdx] = 1;
|
|
3627
|
-
}
|
|
3628
|
-
} else {
|
|
3629
|
-
if (!(r > 240 && g > 240 && b > 240)) {
|
|
3630
|
-
mask[destIdx] = 1;
|
|
3631
|
-
}
|
|
3632
|
-
}
|
|
3633
|
-
}
|
|
3634
|
-
}
|
|
3635
|
-
return mask;
|
|
3636
|
-
}
|
|
3637
|
-
/**
|
|
3638
|
-
* Fast circular morphology using a distance-transform inspired separable approach.
|
|
3639
|
-
* O(N * R) complexity, where R is the radius.
|
|
3640
|
-
*/
|
|
3641
|
-
static circularMorphology(mask, width, height, radius, op) {
|
|
3642
|
-
const dilate = (m, r) => {
|
|
3643
|
-
const horizontalDist = new Int32Array(width * height);
|
|
3644
|
-
for (let y = 0; y < height; y++) {
|
|
3645
|
-
let lastSolid = -r * 2;
|
|
3646
|
-
for (let x = 0; x < width; x++) {
|
|
3647
|
-
if (m[y * width + x]) lastSolid = x;
|
|
3648
|
-
horizontalDist[y * width + x] = x - lastSolid;
|
|
3649
|
-
}
|
|
3650
|
-
lastSolid = width + r * 2;
|
|
3651
|
-
for (let x = width - 1; x >= 0; x--) {
|
|
3652
|
-
if (m[y * width + x]) lastSolid = x;
|
|
3653
|
-
horizontalDist[y * width + x] = Math.min(
|
|
3654
|
-
horizontalDist[y * width + x],
|
|
3655
|
-
lastSolid - x
|
|
3656
|
-
);
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
const result = new Uint8Array(width * height);
|
|
3660
|
-
const r2 = r * r;
|
|
3661
|
-
for (let x = 0; x < width; x++) {
|
|
3662
|
-
for (let y = 0; y < height; y++) {
|
|
3663
|
-
let found = false;
|
|
3664
|
-
const minY = Math.max(0, y - r);
|
|
3665
|
-
const maxY = Math.min(height - 1, y + r);
|
|
3666
|
-
for (let dy = minY; dy <= maxY; dy++) {
|
|
3667
|
-
const dY = dy - y;
|
|
3668
|
-
const hDist = horizontalDist[dy * width + x];
|
|
3669
|
-
if (hDist * hDist + dY * dY <= r2) {
|
|
3670
|
-
found = true;
|
|
3671
|
-
break;
|
|
3672
|
-
}
|
|
3673
|
-
}
|
|
3674
|
-
if (found) result[y * width + x] = 1;
|
|
3675
|
-
}
|
|
3676
|
-
}
|
|
3677
|
-
return result;
|
|
3678
|
-
};
|
|
3679
|
-
const erode = (m, r) => {
|
|
3680
|
-
const inverted = new Uint8Array(m.length);
|
|
3681
|
-
for (let i = 0; i < m.length; i++) inverted[i] = m[i] ? 0 : 1;
|
|
3682
|
-
const dilatedInverted = dilate(inverted, r);
|
|
3683
|
-
const result = new Uint8Array(m.length);
|
|
3684
|
-
for (let i = 0; i < m.length; i++) result[i] = dilatedInverted[i] ? 0 : 1;
|
|
3685
|
-
return result;
|
|
3686
|
-
};
|
|
3687
|
-
switch (op) {
|
|
3688
|
-
case "dilate":
|
|
3689
|
-
return dilate(mask, radius);
|
|
3690
|
-
case "erode":
|
|
3691
|
-
return erode(mask, radius);
|
|
3692
|
-
case "closing":
|
|
3693
|
-
return erode(dilate(mask, radius), radius);
|
|
3694
|
-
case "opening":
|
|
3695
|
-
return dilate(erode(mask, radius), radius);
|
|
3696
|
-
default:
|
|
3697
|
-
return mask;
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
/**
|
|
3701
|
-
* Fills internal holes in the binary mask using flood fill from edges.
|
|
3702
|
-
*/
|
|
3703
|
-
static fillHoles(mask, width, height) {
|
|
3704
|
-
const background = new Uint8Array(width * height);
|
|
3705
|
-
const queue = [];
|
|
3706
|
-
for (let x = 0; x < width; x++) {
|
|
3707
|
-
if (mask[x] === 0) {
|
|
3708
|
-
background[x] = 1;
|
|
3709
|
-
queue.push([x, 0]);
|
|
3710
|
-
}
|
|
3711
|
-
const lastRow = (height - 1) * width + x;
|
|
3712
|
-
if (mask[lastRow] === 0) {
|
|
3713
|
-
background[lastRow] = 1;
|
|
3714
|
-
queue.push([x, height - 1]);
|
|
3715
|
-
}
|
|
3716
|
-
}
|
|
3717
|
-
for (let y = 1; y < height - 1; y++) {
|
|
3718
|
-
if (mask[y * width] === 0) {
|
|
3719
|
-
background[y * width] = 1;
|
|
3720
|
-
queue.push([0, y]);
|
|
3721
|
-
}
|
|
3722
|
-
if (mask[y * width + width - 1] === 0) {
|
|
3723
|
-
background[y * width + width - 1] = 1;
|
|
3724
|
-
queue.push([width - 1, y]);
|
|
3725
|
-
}
|
|
3726
|
-
}
|
|
3727
|
-
const dirs = [
|
|
3728
|
-
[0, 1],
|
|
3729
|
-
[0, -1],
|
|
3730
|
-
[1, 0],
|
|
3731
|
-
[-1, 0]
|
|
3732
|
-
];
|
|
3733
|
-
let head = 0;
|
|
3734
|
-
while (head < queue.length) {
|
|
3735
|
-
const [cx, cy] = queue[head++];
|
|
3736
|
-
for (const [dx, dy] of dirs) {
|
|
3737
|
-
const nx = cx + dx;
|
|
3738
|
-
const ny = cy + dy;
|
|
3739
|
-
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
|
3740
|
-
const nidx = ny * width + nx;
|
|
3741
|
-
if (mask[nidx] === 0 && background[nidx] === 0) {
|
|
3742
|
-
background[nidx] = 1;
|
|
3743
|
-
queue.push([nx, ny]);
|
|
3744
|
-
}
|
|
3745
|
-
}
|
|
3746
|
-
}
|
|
3747
|
-
}
|
|
3748
|
-
const filledMask = new Uint8Array(width * height);
|
|
3749
|
-
for (let i = 0; i < width * height; i++) {
|
|
3750
|
-
filledMask[i] = background[i] === 0 ? 1 : 0;
|
|
3751
|
-
}
|
|
3752
|
-
return filledMask;
|
|
3753
|
-
}
|
|
3754
3776
|
/**
|
|
3755
3777
|
* Traces all contours in the mask with optimized start-point detection
|
|
3756
3778
|
*/
|
|
@@ -3894,13 +3916,13 @@ var ImageTracer = class {
|
|
|
3894
3916
|
(points) => this.scalePoints(points, targetWidth, targetHeight, bounds)
|
|
3895
3917
|
);
|
|
3896
3918
|
}
|
|
3897
|
-
static
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3919
|
+
static translateContours(contours, offsetX, offsetY) {
|
|
3920
|
+
return contours.map(
|
|
3921
|
+
(points) => points.map((p) => ({
|
|
3922
|
+
x: p.x + offsetX,
|
|
3923
|
+
y: p.y + offsetY
|
|
3924
|
+
}))
|
|
3925
|
+
);
|
|
3904
3926
|
}
|
|
3905
3927
|
static pointsToSVG(points) {
|
|
3906
3928
|
if (points.length === 0) return "";
|
|
@@ -4062,6 +4084,14 @@ var DielineTool = class {
|
|
|
4062
4084
|
);
|
|
4063
4085
|
s.features = configService.get("dieline.features", s.features);
|
|
4064
4086
|
s.pathData = configService.get("dieline.pathData", s.pathData);
|
|
4087
|
+
const sourceWidth = Number(
|
|
4088
|
+
configService.get("dieline.customSourceWidthPx", 0)
|
|
4089
|
+
);
|
|
4090
|
+
const sourceHeight = Number(
|
|
4091
|
+
configService.get("dieline.customSourceHeightPx", 0)
|
|
4092
|
+
);
|
|
4093
|
+
s.customSourceWidthPx = Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0;
|
|
4094
|
+
s.customSourceHeightPx = Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0;
|
|
4065
4095
|
configService.onAnyChange((e) => {
|
|
4066
4096
|
if (e.key.startsWith("size.")) {
|
|
4067
4097
|
const nextSize = readSizeState(configService);
|
|
@@ -4120,6 +4150,12 @@ var DielineTool = class {
|
|
|
4120
4150
|
case "dieline.pathData":
|
|
4121
4151
|
s.pathData = e.value;
|
|
4122
4152
|
break;
|
|
4153
|
+
case "dieline.customSourceWidthPx":
|
|
4154
|
+
s.customSourceWidthPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
|
|
4155
|
+
break;
|
|
4156
|
+
case "dieline.customSourceHeightPx":
|
|
4157
|
+
s.customSourceHeightPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
|
|
4158
|
+
break;
|
|
4123
4159
|
}
|
|
4124
4160
|
this.updateDieline();
|
|
4125
4161
|
}
|
|
@@ -4287,10 +4323,17 @@ var DielineTool = class {
|
|
|
4287
4323
|
command: "detectEdge",
|
|
4288
4324
|
title: "Detect Edge from Image",
|
|
4289
4325
|
handler: async (imageUrl, options) => {
|
|
4290
|
-
var _a;
|
|
4326
|
+
var _a, _b, _c;
|
|
4291
4327
|
try {
|
|
4292
4328
|
const detectOptions = options || {};
|
|
4293
4329
|
const debug = detectOptions.debug === true;
|
|
4330
|
+
const tracerOptions = {
|
|
4331
|
+
expand: (_a = detectOptions.expand) != null ? _a : 0,
|
|
4332
|
+
smoothing: (_b = detectOptions.smoothing) != null ? _b : true,
|
|
4333
|
+
simplifyTolerance: (_c = detectOptions.simplifyTolerance) != null ? _c : 2,
|
|
4334
|
+
threshold: detectOptions.threshold,
|
|
4335
|
+
debug
|
|
4336
|
+
};
|
|
4294
4337
|
const loadImage = (url) => {
|
|
4295
4338
|
return new Promise((resolve, reject) => {
|
|
4296
4339
|
const img2 = new Image();
|
|
@@ -4302,7 +4345,7 @@ var DielineTool = class {
|
|
|
4302
4345
|
};
|
|
4303
4346
|
const [img, traced] = await Promise.all([
|
|
4304
4347
|
loadImage(imageUrl),
|
|
4305
|
-
ImageTracer.traceWithBounds(imageUrl,
|
|
4348
|
+
ImageTracer.traceWithBounds(imageUrl, tracerOptions)
|
|
4306
4349
|
]);
|
|
4307
4350
|
const { pathData, baseBounds, bounds } = traced;
|
|
4308
4351
|
if (debug) {
|
|
@@ -4313,21 +4356,8 @@ var DielineTool = class {
|
|
|
4313
4356
|
expandedBounds: bounds,
|
|
4314
4357
|
currentDielineWidth: s.width,
|
|
4315
4358
|
currentDielineHeight: s.height,
|
|
4316
|
-
options:
|
|
4317
|
-
|
|
4318
|
-
morphologyRadius: detectOptions.morphologyRadius,
|
|
4319
|
-
connectRadiusMax: detectOptions.connectRadiusMax,
|
|
4320
|
-
smoothing: detectOptions.smoothing,
|
|
4321
|
-
simplifyTolerance: detectOptions.simplifyTolerance,
|
|
4322
|
-
threshold: detectOptions.threshold,
|
|
4323
|
-
maskMode: detectOptions.maskMode,
|
|
4324
|
-
whiteThreshold: detectOptions.whiteThreshold,
|
|
4325
|
-
alphaOpaqueCutoff: detectOptions.alphaOpaqueCutoff,
|
|
4326
|
-
noChannels: detectOptions.noChannels,
|
|
4327
|
-
componentMode: detectOptions.componentMode,
|
|
4328
|
-
minComponentArea: detectOptions.minComponentArea,
|
|
4329
|
-
forceConnected: detectOptions.forceConnected
|
|
4330
|
-
}
|
|
4359
|
+
options: tracerOptions,
|
|
4360
|
+
strategy: "single-connected-silhouette"
|
|
4331
4361
|
});
|
|
4332
4362
|
}
|
|
4333
4363
|
return {
|
|
@@ -4470,7 +4500,9 @@ var DielineTool = class {
|
|
|
4470
4500
|
x: cx,
|
|
4471
4501
|
y: cy,
|
|
4472
4502
|
features: cutFeatures,
|
|
4473
|
-
pathData: this.state.pathData
|
|
4503
|
+
pathData: this.state.pathData,
|
|
4504
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4505
|
+
customSourceHeightPx: this.state.customSourceHeightPx
|
|
4474
4506
|
});
|
|
4475
4507
|
const mask = new import_fabric3.Path(maskPathData, {
|
|
4476
4508
|
fill: outsideColor,
|
|
@@ -4493,6 +4525,8 @@ var DielineTool = class {
|
|
|
4493
4525
|
y: cy,
|
|
4494
4526
|
features: cutFeatures,
|
|
4495
4527
|
pathData: this.state.pathData,
|
|
4528
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4529
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4496
4530
|
canvasWidth: canvasW,
|
|
4497
4531
|
canvasHeight: canvasH
|
|
4498
4532
|
});
|
|
@@ -4517,6 +4551,8 @@ var DielineTool = class {
|
|
|
4517
4551
|
y: cy,
|
|
4518
4552
|
features: cutFeatures,
|
|
4519
4553
|
pathData: this.state.pathData,
|
|
4554
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4555
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4520
4556
|
canvasWidth: canvasW,
|
|
4521
4557
|
canvasHeight: canvasH
|
|
4522
4558
|
},
|
|
@@ -4529,6 +4565,8 @@ var DielineTool = class {
|
|
|
4529
4565
|
y: cy,
|
|
4530
4566
|
features: cutFeatures,
|
|
4531
4567
|
pathData: this.state.pathData,
|
|
4568
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4569
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4532
4570
|
canvasWidth: canvasW,
|
|
4533
4571
|
canvasHeight: canvasH
|
|
4534
4572
|
},
|
|
@@ -4558,6 +4596,8 @@ var DielineTool = class {
|
|
|
4558
4596
|
y: cy,
|
|
4559
4597
|
features: cutFeatures,
|
|
4560
4598
|
pathData: this.state.pathData,
|
|
4599
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4600
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4561
4601
|
canvasWidth: canvasW,
|
|
4562
4602
|
canvasHeight: canvasH
|
|
4563
4603
|
});
|
|
@@ -4582,6 +4622,8 @@ var DielineTool = class {
|
|
|
4582
4622
|
y: cy,
|
|
4583
4623
|
features: absoluteFeatures,
|
|
4584
4624
|
pathData: this.state.pathData,
|
|
4625
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4626
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4585
4627
|
canvasWidth: canvasW,
|
|
4586
4628
|
canvasHeight: canvasH
|
|
4587
4629
|
});
|
|
@@ -4627,7 +4669,9 @@ var DielineTool = class {
|
|
|
4627
4669
|
return {
|
|
4628
4670
|
...sceneGeometry,
|
|
4629
4671
|
strokeWidth: this.state.mainLine.width,
|
|
4630
|
-
pathData: this.state.pathData
|
|
4672
|
+
pathData: this.state.pathData,
|
|
4673
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4674
|
+
customSourceHeightPx: this.state.customSourceHeightPx
|
|
4631
4675
|
};
|
|
4632
4676
|
}
|
|
4633
4677
|
async exportCutImage(options) {
|
|
@@ -4686,6 +4730,8 @@ var DielineTool = class {
|
|
|
4686
4730
|
y: cy,
|
|
4687
4731
|
features: cutFeatures,
|
|
4688
4732
|
pathData,
|
|
4733
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4734
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4689
4735
|
canvasWidth: canvasW,
|
|
4690
4736
|
canvasHeight: canvasH
|
|
4691
4737
|
});
|
|
@@ -7034,6 +7080,82 @@ var WhiteInkTool = class {
|
|
|
7034
7080
|
height
|
|
7035
7081
|
};
|
|
7036
7082
|
}
|
|
7083
|
+
getImagePlacementState(id) {
|
|
7084
|
+
const rawItems = this.getConfig("image.items", []);
|
|
7085
|
+
if (!Array.isArray(rawItems) || rawItems.length === 0) return null;
|
|
7086
|
+
const matched = (id ? rawItems.find(
|
|
7087
|
+
(item) => item && typeof item === "object" && typeof item.id === "string" && item.id === id
|
|
7088
|
+
) : void 0) || rawItems[0];
|
|
7089
|
+
if (!matched || typeof matched !== "object") return null;
|
|
7090
|
+
const sourceUrl = typeof matched.sourceUrl === "string" && matched.sourceUrl.length > 0 ? matched.sourceUrl : typeof matched.url === "string" ? matched.url : "";
|
|
7091
|
+
const committedUrl = typeof matched.committedUrl === "string" ? matched.committedUrl : "";
|
|
7092
|
+
return {
|
|
7093
|
+
id: typeof matched.id === "string" && matched.id.length > 0 ? matched.id : id || "image",
|
|
7094
|
+
sourceUrl,
|
|
7095
|
+
committedUrl,
|
|
7096
|
+
left: Number.isFinite(matched.left) ? Number(matched.left) : 0.5,
|
|
7097
|
+
top: Number.isFinite(matched.top) ? Number(matched.top) : 0.5,
|
|
7098
|
+
scale: Number.isFinite(matched.scale) ? Math.max(0.05, matched.scale) : 1,
|
|
7099
|
+
angle: Number.isFinite(matched.angle) ? matched.angle : 0
|
|
7100
|
+
};
|
|
7101
|
+
}
|
|
7102
|
+
shouldRestoreSnapshotToSource(snapshot, placement) {
|
|
7103
|
+
if (!placement.sourceUrl || !placement.committedUrl) return false;
|
|
7104
|
+
if (placement.sourceUrl === placement.committedUrl) return false;
|
|
7105
|
+
return snapshot.src === placement.committedUrl;
|
|
7106
|
+
}
|
|
7107
|
+
getCoverScale(frame, source) {
|
|
7108
|
+
const frameW = Math.max(1, frame.width);
|
|
7109
|
+
const frameH = Math.max(1, frame.height);
|
|
7110
|
+
const sourceW = Math.max(1, source.width);
|
|
7111
|
+
const sourceH = Math.max(1, source.height);
|
|
7112
|
+
return Math.max(frameW / sourceW, frameH / sourceH);
|
|
7113
|
+
}
|
|
7114
|
+
async ensureSourceSize(sourceUrl) {
|
|
7115
|
+
if (!sourceUrl) return null;
|
|
7116
|
+
const cached = this.getSourceSize(sourceUrl);
|
|
7117
|
+
if (cached) return cached;
|
|
7118
|
+
try {
|
|
7119
|
+
const image = await this.loadImageElement(sourceUrl);
|
|
7120
|
+
const size = this.getElementSize(image);
|
|
7121
|
+
if (!size) return null;
|
|
7122
|
+
this.rememberSourceSize(sourceUrl, size);
|
|
7123
|
+
return {
|
|
7124
|
+
width: size.width,
|
|
7125
|
+
height: size.height
|
|
7126
|
+
};
|
|
7127
|
+
} catch (e) {
|
|
7128
|
+
return null;
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
async resolveAlignedImageSnapshot(snapshot) {
|
|
7132
|
+
const placement = this.getImagePlacementState(snapshot.id);
|
|
7133
|
+
if (!placement) return snapshot;
|
|
7134
|
+
if (!this.shouldRestoreSnapshotToSource(snapshot, placement)) {
|
|
7135
|
+
return snapshot;
|
|
7136
|
+
}
|
|
7137
|
+
const frame = this.getFrameRect();
|
|
7138
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
7139
|
+
return snapshot;
|
|
7140
|
+
}
|
|
7141
|
+
const sourceSize = await this.ensureSourceSize(placement.sourceUrl);
|
|
7142
|
+
if (!sourceSize) return snapshot;
|
|
7143
|
+
const coverScale = this.getCoverScale(frame, sourceSize);
|
|
7144
|
+
return {
|
|
7145
|
+
...snapshot,
|
|
7146
|
+
src: placement.sourceUrl,
|
|
7147
|
+
element: void 0,
|
|
7148
|
+
left: frame.left + placement.left * frame.width,
|
|
7149
|
+
top: frame.top + placement.top * frame.height,
|
|
7150
|
+
scaleX: coverScale * placement.scale,
|
|
7151
|
+
scaleY: coverScale * placement.scale,
|
|
7152
|
+
angle: placement.angle,
|
|
7153
|
+
originX: "center",
|
|
7154
|
+
originY: "center",
|
|
7155
|
+
width: sourceSize.width,
|
|
7156
|
+
height: sourceSize.height
|
|
7157
|
+
};
|
|
7158
|
+
}
|
|
7037
7159
|
getImageElementFromObject(obj) {
|
|
7038
7160
|
if (!obj) return null;
|
|
7039
7161
|
if (typeof obj.getElement === "function") {
|
|
@@ -7406,9 +7528,11 @@ var WhiteInkTool = class {
|
|
|
7406
7528
|
let whiteSpecs = [];
|
|
7407
7529
|
let coverSpecs = [];
|
|
7408
7530
|
if (previewActive) {
|
|
7409
|
-
const
|
|
7531
|
+
const baseSnapshot = this.getImageSnapshot(this.getPrimaryImageObject());
|
|
7410
7532
|
const item = this.getEffectiveWhiteInkItem(this.resolveRenderItems());
|
|
7411
|
-
if (
|
|
7533
|
+
if (baseSnapshot && item) {
|
|
7534
|
+
const snapshot = await this.resolveAlignedImageSnapshot(baseSnapshot);
|
|
7535
|
+
if (seq !== this.renderSeq) return;
|
|
7412
7536
|
const sources = await this.resolveRenderSources(snapshot, item);
|
|
7413
7537
|
if (seq !== this.renderSeq) return;
|
|
7414
7538
|
if (sources == null ? void 0 : sources.whiteSrc) {
|