@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.mjs
CHANGED
|
@@ -356,7 +356,17 @@ function selectOuterChain(args) {
|
|
|
356
356
|
}
|
|
357
357
|
function createBaseShape(options) {
|
|
358
358
|
var _a;
|
|
359
|
-
const {
|
|
359
|
+
const {
|
|
360
|
+
shape,
|
|
361
|
+
width,
|
|
362
|
+
height,
|
|
363
|
+
radius,
|
|
364
|
+
x,
|
|
365
|
+
y,
|
|
366
|
+
pathData,
|
|
367
|
+
customSourceWidthPx,
|
|
368
|
+
customSourceHeightPx
|
|
369
|
+
} = options;
|
|
360
370
|
const center = new paper.Point(x, y);
|
|
361
371
|
if (shape === "rect") {
|
|
362
372
|
return new paper.Path.Rectangle({
|
|
@@ -382,10 +392,21 @@ function createBaseShape(options) {
|
|
|
382
392
|
single.pathData = pathData;
|
|
383
393
|
return single;
|
|
384
394
|
})();
|
|
385
|
-
|
|
395
|
+
const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
|
|
396
|
+
const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
|
|
397
|
+
if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
|
|
398
|
+
const targetLeft = x - width / 2;
|
|
399
|
+
const targetTop = y - height / 2;
|
|
400
|
+
path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
|
|
401
|
+
path.translate(new paper.Point(targetLeft, targetTop));
|
|
402
|
+
return path;
|
|
403
|
+
}
|
|
386
404
|
if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
|
|
405
|
+
path.position = center;
|
|
387
406
|
path.scale(width / path.bounds.width, height / path.bounds.height);
|
|
407
|
+
return path;
|
|
388
408
|
}
|
|
409
|
+
path.position = center;
|
|
389
410
|
return path;
|
|
390
411
|
} else {
|
|
391
412
|
return new paper.Path.Rectangle({
|
|
@@ -1020,6 +1041,10 @@ function buildSceneGeometry(configService, layout) {
|
|
|
1020
1041
|
"mm"
|
|
1021
1042
|
);
|
|
1022
1043
|
const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
|
|
1044
|
+
const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
|
|
1045
|
+
const sourceHeight = Number(
|
|
1046
|
+
configService.get("dieline.customSourceHeightPx", 0)
|
|
1047
|
+
);
|
|
1023
1048
|
return {
|
|
1024
1049
|
shape: configService.get("dieline.shape", "rect"),
|
|
1025
1050
|
unit: "mm",
|
|
@@ -1031,7 +1056,9 @@ function buildSceneGeometry(configService, layout) {
|
|
|
1031
1056
|
radius: radiusMm * layout.scale,
|
|
1032
1057
|
offset,
|
|
1033
1058
|
scale: layout.scale,
|
|
1034
|
-
pathData: configService.get("dieline.pathData")
|
|
1059
|
+
pathData: configService.get("dieline.pathData"),
|
|
1060
|
+
customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
|
|
1061
|
+
customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
|
|
1035
1062
|
};
|
|
1036
1063
|
}
|
|
1037
1064
|
|
|
@@ -2458,6 +2485,8 @@ var ImageTool = class {
|
|
|
2458
2485
|
this.normalizeItem({
|
|
2459
2486
|
...item,
|
|
2460
2487
|
url,
|
|
2488
|
+
// Keep original source for next image-tool session editing,
|
|
2489
|
+
// and use committedUrl as non-image-tools render source.
|
|
2461
2490
|
sourceUrl,
|
|
2462
2491
|
committedUrl: url
|
|
2463
2492
|
})
|
|
@@ -2958,15 +2987,19 @@ function analyzeAlpha(imageData, alphaOpaqueCutoff) {
|
|
|
2958
2987
|
};
|
|
2959
2988
|
}
|
|
2960
2989
|
function circularMorphology(mask, width, height, radius, op) {
|
|
2961
|
-
const
|
|
2990
|
+
const r = Math.max(0, Math.floor(radius));
|
|
2991
|
+
if (r <= 0) {
|
|
2992
|
+
return mask.slice();
|
|
2993
|
+
}
|
|
2994
|
+
const dilateDisk = (m, radiusPx) => {
|
|
2962
2995
|
const horizontalDist = new Int32Array(width * height);
|
|
2963
2996
|
for (let y = 0; y < height; y++) {
|
|
2964
|
-
let lastSolid = -
|
|
2997
|
+
let lastSolid = -radiusPx * 2;
|
|
2965
2998
|
for (let x = 0; x < width; x++) {
|
|
2966
2999
|
if (m[y * width + x]) lastSolid = x;
|
|
2967
3000
|
horizontalDist[y * width + x] = x - lastSolid;
|
|
2968
3001
|
}
|
|
2969
|
-
lastSolid = width +
|
|
3002
|
+
lastSolid = width + radiusPx * 2;
|
|
2970
3003
|
for (let x = width - 1; x >= 0; x--) {
|
|
2971
3004
|
if (m[y * width + x]) lastSolid = x;
|
|
2972
3005
|
horizontalDist[y * width + x] = Math.min(
|
|
@@ -2976,12 +3009,12 @@ function circularMorphology(mask, width, height, radius, op) {
|
|
|
2976
3009
|
}
|
|
2977
3010
|
}
|
|
2978
3011
|
const result = new Uint8Array(width * height);
|
|
2979
|
-
const r2 =
|
|
3012
|
+
const r2 = radiusPx * radiusPx;
|
|
2980
3013
|
for (let x = 0; x < width; x++) {
|
|
2981
3014
|
for (let y = 0; y < height; y++) {
|
|
2982
3015
|
let found = false;
|
|
2983
|
-
const minY = Math.max(0, y -
|
|
2984
|
-
const maxY = Math.min(height - 1, y +
|
|
3016
|
+
const minY = Math.max(0, y - radiusPx);
|
|
3017
|
+
const maxY = Math.min(height - 1, y + radiusPx);
|
|
2985
3018
|
for (let dy = minY; dy <= maxY; dy++) {
|
|
2986
3019
|
const dY = dy - y;
|
|
2987
3020
|
const hDist = horizontalDist[dy * width + x];
|
|
@@ -2995,23 +3028,62 @@ function circularMorphology(mask, width, height, radius, op) {
|
|
|
2995
3028
|
}
|
|
2996
3029
|
return result;
|
|
2997
3030
|
};
|
|
2998
|
-
const
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3031
|
+
const erodeDiamond = (m, radiusPx) => {
|
|
3032
|
+
if (radiusPx <= 0) return m.slice();
|
|
3033
|
+
let current = m;
|
|
3034
|
+
for (let step = 0; step < radiusPx; step++) {
|
|
3035
|
+
const next = new Uint8Array(width * height);
|
|
3036
|
+
for (let y = 1; y < height - 1; y++) {
|
|
3037
|
+
const row = y * width;
|
|
3038
|
+
for (let x = 1; x < width - 1; x++) {
|
|
3039
|
+
const idx = row + x;
|
|
3040
|
+
if (current[idx] && current[idx - 1] && current[idx + 1] && current[idx - width] && current[idx + width]) {
|
|
3041
|
+
next[idx] = 1;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
current = next;
|
|
3046
|
+
}
|
|
3047
|
+
return current;
|
|
3048
|
+
};
|
|
3049
|
+
const restoreBridgePixels = (source, eroded) => {
|
|
3050
|
+
const restored = eroded.slice();
|
|
3051
|
+
for (let y = 1; y < height - 1; y++) {
|
|
3052
|
+
const row = y * width;
|
|
3053
|
+
for (let x = 1; x < width - 1; x++) {
|
|
3054
|
+
const idx = row + x;
|
|
3055
|
+
if (!source[idx] || restored[idx]) continue;
|
|
3056
|
+
const up = source[idx - width] === 1;
|
|
3057
|
+
const down = source[idx + width] === 1;
|
|
3058
|
+
const left = source[idx - 1] === 1;
|
|
3059
|
+
const right = source[idx + 1] === 1;
|
|
3060
|
+
const upLeft = source[idx - width - 1] === 1;
|
|
3061
|
+
const upRight = source[idx - width + 1] === 1;
|
|
3062
|
+
const downLeft = source[idx + width - 1] === 1;
|
|
3063
|
+
const downRight = source[idx + width + 1] === 1;
|
|
3064
|
+
const keepsBridge = left && right || up && down || upLeft && downRight || upRight && downLeft;
|
|
3065
|
+
if (keepsBridge) {
|
|
3066
|
+
restored[idx] = 1;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
return restored;
|
|
3071
|
+
};
|
|
3072
|
+
const erodePreservingBridges = (m, radiusPx) => {
|
|
3073
|
+
const eroded = erodeDiamond(m, radiusPx);
|
|
3074
|
+
return restoreBridgePixels(m, eroded);
|
|
3005
3075
|
};
|
|
3006
3076
|
switch (op) {
|
|
3007
3077
|
case "dilate":
|
|
3008
|
-
return
|
|
3078
|
+
return dilateDisk(mask, r);
|
|
3009
3079
|
case "erode":
|
|
3010
|
-
return
|
|
3011
|
-
case "closing":
|
|
3012
|
-
|
|
3080
|
+
return erodePreservingBridges(mask, r);
|
|
3081
|
+
case "closing": {
|
|
3082
|
+
const erodeRadius = Math.max(1, Math.floor(r * 0.65));
|
|
3083
|
+
return erodePreservingBridges(dilateDisk(mask, r), erodeRadius);
|
|
3084
|
+
}
|
|
3013
3085
|
case "opening":
|
|
3014
|
-
return
|
|
3086
|
+
return dilateDisk(erodePreservingBridges(mask, r), r);
|
|
3015
3087
|
default:
|
|
3016
3088
|
return mask;
|
|
3017
3089
|
}
|
|
@@ -3074,78 +3146,6 @@ function fillHoles(mask, width, height) {
|
|
|
3074
3146
|
}
|
|
3075
3147
|
return filledMask;
|
|
3076
3148
|
}
|
|
3077
|
-
function countForeground(mask) {
|
|
3078
|
-
let c = 0;
|
|
3079
|
-
for (let i = 0; i < mask.length; i++) c += mask[i] ? 1 : 0;
|
|
3080
|
-
return c;
|
|
3081
|
-
}
|
|
3082
|
-
function isMaskConnected8(mask, width, height) {
|
|
3083
|
-
const total = countForeground(mask);
|
|
3084
|
-
if (total === 0) return true;
|
|
3085
|
-
let start = -1;
|
|
3086
|
-
for (let i = 0; i < mask.length; i++) {
|
|
3087
|
-
if (mask[i]) {
|
|
3088
|
-
start = i;
|
|
3089
|
-
break;
|
|
3090
|
-
}
|
|
3091
|
-
}
|
|
3092
|
-
if (start === -1) return true;
|
|
3093
|
-
const visited = new Uint8Array(mask.length);
|
|
3094
|
-
const queue = [start];
|
|
3095
|
-
visited[start] = 1;
|
|
3096
|
-
let seen = 1;
|
|
3097
|
-
let head = 0;
|
|
3098
|
-
while (head < queue.length) {
|
|
3099
|
-
const idx = queue[head++];
|
|
3100
|
-
const x = idx % width;
|
|
3101
|
-
const y = (idx - x) / width;
|
|
3102
|
-
for (let dy = -1; dy <= 1; dy++) {
|
|
3103
|
-
const ny = y + dy;
|
|
3104
|
-
if (ny < 0 || ny >= height) continue;
|
|
3105
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
3106
|
-
if (dx === 0 && dy === 0) continue;
|
|
3107
|
-
const nx = x + dx;
|
|
3108
|
-
if (nx < 0 || nx >= width) continue;
|
|
3109
|
-
const nidx = ny * width + nx;
|
|
3110
|
-
if (mask[nidx] && !visited[nidx]) {
|
|
3111
|
-
visited[nidx] = 1;
|
|
3112
|
-
queue.push(nidx);
|
|
3113
|
-
seen++;
|
|
3114
|
-
}
|
|
3115
|
-
}
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
return seen === total;
|
|
3119
|
-
}
|
|
3120
|
-
function findMinimalConnectRadius(mask, width, height, maxRadius) {
|
|
3121
|
-
if (maxRadius <= 0) return 0;
|
|
3122
|
-
if (isMaskConnected8(mask, width, height)) return 0;
|
|
3123
|
-
let low = 0;
|
|
3124
|
-
let high = 1;
|
|
3125
|
-
while (high <= maxRadius) {
|
|
3126
|
-
const closed = circularMorphology(mask, width, height, high, "closing");
|
|
3127
|
-
if (isMaskConnected8(closed, width, height)) break;
|
|
3128
|
-
high *= 2;
|
|
3129
|
-
}
|
|
3130
|
-
if (high > maxRadius) high = maxRadius;
|
|
3131
|
-
if (!isMaskConnected8(
|
|
3132
|
-
circularMorphology(mask, width, height, high, "closing"),
|
|
3133
|
-
width,
|
|
3134
|
-
height
|
|
3135
|
-
)) {
|
|
3136
|
-
return high;
|
|
3137
|
-
}
|
|
3138
|
-
while (low + 1 < high) {
|
|
3139
|
-
const mid = Math.floor((low + high) / 2);
|
|
3140
|
-
const closed = circularMorphology(mask, width, height, mid, "closing");
|
|
3141
|
-
if (isMaskConnected8(closed, width, height)) {
|
|
3142
|
-
high = mid;
|
|
3143
|
-
} else {
|
|
3144
|
-
low = mid;
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
return high;
|
|
3148
|
-
}
|
|
3149
3149
|
function polygonSignedArea(points) {
|
|
3150
3150
|
if (points.length < 3) return 0;
|
|
3151
3151
|
let sum = 0;
|
|
@@ -3169,10 +3169,19 @@ var ImageTracer = class {
|
|
|
3169
3169
|
return pathData;
|
|
3170
3170
|
}
|
|
3171
3171
|
static async traceWithBounds(imageUrl, options = {}) {
|
|
3172
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i
|
|
3172
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
3173
3173
|
const img = await this.loadImage(imageUrl);
|
|
3174
3174
|
const width = img.width;
|
|
3175
3175
|
const height = img.height;
|
|
3176
|
+
if (width <= 0 || height <= 0) {
|
|
3177
|
+
const w = (_a = options.scaleToWidth) != null ? _a : 0;
|
|
3178
|
+
const h = (_b = options.scaleToHeight) != null ? _b : 0;
|
|
3179
|
+
return {
|
|
3180
|
+
pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
|
|
3181
|
+
baseBounds: { x: 0, y: 0, width: w, height: h },
|
|
3182
|
+
bounds: { x: 0, y: 0, width: w, height: h }
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3176
3185
|
const debug = options.debug === true;
|
|
3177
3186
|
const debugLog = (message, payload) => {
|
|
3178
3187
|
if (!debug) return;
|
|
@@ -3189,96 +3198,178 @@ var ImageTracer = class {
|
|
|
3189
3198
|
if (!ctx) throw new Error("Could not get 2D context");
|
|
3190
3199
|
ctx.drawImage(img, 0, 0);
|
|
3191
3200
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
3192
|
-
const threshold = (
|
|
3193
|
-
const
|
|
3194
|
-
const
|
|
3195
|
-
const
|
|
3196
|
-
const
|
|
3197
|
-
|
|
3198
|
-
|
|
3201
|
+
const threshold = (_c = options.threshold) != null ? _c : 10;
|
|
3202
|
+
const expand = Math.max(0, Math.floor((_d = options.expand) != null ? _d : 0));
|
|
3203
|
+
const simplifyTolerance = (_e = options.simplifyTolerance) != null ? _e : 2.5;
|
|
3204
|
+
const useSmoothing = options.smoothing !== false;
|
|
3205
|
+
const componentMode = "all";
|
|
3206
|
+
const minComponentArea = 0;
|
|
3207
|
+
const maxDim = Math.max(width, height);
|
|
3208
|
+
const maskMode = "auto";
|
|
3209
|
+
const whiteThreshold = 240;
|
|
3210
|
+
const alphaOpaqueCutoff = 250;
|
|
3211
|
+
const preprocessDilateRadius = Math.max(
|
|
3212
|
+
2,
|
|
3213
|
+
Math.floor(Math.max(maxDim * 0.012, expand * 0.35))
|
|
3214
|
+
);
|
|
3215
|
+
const preprocessErodeRadius = Math.max(
|
|
3216
|
+
1,
|
|
3217
|
+
Math.floor(preprocessDilateRadius * 0.65)
|
|
3218
|
+
);
|
|
3219
|
+
const smoothDilateRadius = Math.max(
|
|
3220
|
+
1,
|
|
3221
|
+
Math.floor(preprocessDilateRadius * 0.25)
|
|
3199
3222
|
);
|
|
3200
|
-
const
|
|
3201
|
-
const
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
const
|
|
3223
|
+
const smoothErodeRadius = Math.max(1, Math.floor(smoothDilateRadius * 0.8));
|
|
3224
|
+
const connectStartDilateRadius = Math.max(
|
|
3225
|
+
1,
|
|
3226
|
+
Math.floor(Math.max(maxDim * 6e-3, expand * 0.2))
|
|
3227
|
+
);
|
|
3228
|
+
const connectMaxDilateRadius = Math.max(
|
|
3229
|
+
connectStartDilateRadius,
|
|
3230
|
+
Math.floor(Math.max(maxDim * 0.2, expand * 2.5))
|
|
3231
|
+
);
|
|
3232
|
+
const connectErodeRatio = 0.65;
|
|
3206
3233
|
debugLog("traceWithBounds:start", {
|
|
3207
3234
|
width,
|
|
3208
3235
|
height,
|
|
3209
3236
|
threshold,
|
|
3210
|
-
radius,
|
|
3211
3237
|
expand,
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3238
|
+
simplifyTolerance,
|
|
3239
|
+
smoothing: useSmoothing,
|
|
3240
|
+
strategy: {
|
|
3241
|
+
maskMode,
|
|
3242
|
+
whiteThreshold,
|
|
3243
|
+
alphaOpaqueCutoff,
|
|
3244
|
+
fillHoles: true,
|
|
3245
|
+
preprocessDilateRadius,
|
|
3246
|
+
preprocessErodeRadius,
|
|
3247
|
+
smoothDilateRadius,
|
|
3248
|
+
smoothErodeRadius,
|
|
3249
|
+
connectEnabled: true,
|
|
3250
|
+
connectStartDilateRadius,
|
|
3251
|
+
connectMaxDilateRadius,
|
|
3252
|
+
connectErodeRatio
|
|
3253
|
+
}
|
|
3228
3254
|
});
|
|
3229
|
-
const padding =
|
|
3255
|
+
const padding = Math.max(
|
|
3256
|
+
preprocessDilateRadius,
|
|
3257
|
+
smoothDilateRadius,
|
|
3258
|
+
connectMaxDilateRadius,
|
|
3259
|
+
expand
|
|
3260
|
+
) + 2;
|
|
3230
3261
|
const paddedWidth = width + padding * 2;
|
|
3231
3262
|
const paddedHeight = height + padding * 2;
|
|
3263
|
+
const summarizeMaskContours = (m) => {
|
|
3264
|
+
const summary = this.summarizeAllContours(
|
|
3265
|
+
m,
|
|
3266
|
+
paddedWidth,
|
|
3267
|
+
paddedHeight,
|
|
3268
|
+
minComponentArea
|
|
3269
|
+
);
|
|
3270
|
+
return {
|
|
3271
|
+
rawContourCount: summary.rawCount,
|
|
3272
|
+
selectedContourCount: summary.selectedCount
|
|
3273
|
+
};
|
|
3274
|
+
};
|
|
3232
3275
|
let mask = createMask(imageData, {
|
|
3233
3276
|
threshold,
|
|
3234
3277
|
padding,
|
|
3235
3278
|
paddedWidth,
|
|
3236
3279
|
paddedHeight,
|
|
3237
|
-
maskMode
|
|
3238
|
-
whiteThreshold
|
|
3280
|
+
maskMode,
|
|
3281
|
+
whiteThreshold,
|
|
3239
3282
|
alphaOpaqueCutoff
|
|
3240
3283
|
});
|
|
3241
|
-
if (
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3284
|
+
if (debug) {
|
|
3285
|
+
debugLog(
|
|
3286
|
+
"traceWithBounds:mask:after-create",
|
|
3287
|
+
summarizeMaskContours(mask)
|
|
3288
|
+
);
|
|
3246
3289
|
}
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3290
|
+
mask = circularMorphology(
|
|
3291
|
+
mask,
|
|
3292
|
+
paddedWidth,
|
|
3293
|
+
paddedHeight,
|
|
3294
|
+
preprocessDilateRadius,
|
|
3295
|
+
"dilate"
|
|
3296
|
+
);
|
|
3297
|
+
mask = fillHoles(mask, paddedWidth, paddedHeight);
|
|
3298
|
+
mask = circularMorphology(
|
|
3299
|
+
mask,
|
|
3300
|
+
paddedWidth,
|
|
3301
|
+
paddedHeight,
|
|
3302
|
+
preprocessErodeRadius,
|
|
3303
|
+
"erode"
|
|
3304
|
+
);
|
|
3305
|
+
mask = fillHoles(mask, paddedWidth, paddedHeight);
|
|
3306
|
+
if (debug) {
|
|
3307
|
+
debugLog("traceWithBounds:mask:after-preprocess", {
|
|
3308
|
+
dilateRadius: preprocessDilateRadius,
|
|
3309
|
+
erodeRadius: preprocessErodeRadius,
|
|
3310
|
+
...summarizeMaskContours(mask)
|
|
3311
|
+
});
|
|
3250
3312
|
}
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3313
|
+
mask = circularMorphology(
|
|
3314
|
+
mask,
|
|
3315
|
+
paddedWidth,
|
|
3316
|
+
paddedHeight,
|
|
3317
|
+
smoothDilateRadius,
|
|
3318
|
+
"dilate"
|
|
3254
3319
|
);
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3320
|
+
mask = fillHoles(mask, paddedWidth, paddedHeight);
|
|
3321
|
+
mask = circularMorphology(
|
|
3322
|
+
mask,
|
|
3323
|
+
paddedWidth,
|
|
3324
|
+
paddedHeight,
|
|
3325
|
+
smoothErodeRadius,
|
|
3326
|
+
"erode"
|
|
3327
|
+
);
|
|
3328
|
+
mask = fillHoles(mask, paddedWidth, paddedHeight);
|
|
3329
|
+
if (debug) {
|
|
3330
|
+
debugLog("traceWithBounds:mask:after-smooth", {
|
|
3331
|
+
dilateRadius: smoothDilateRadius,
|
|
3332
|
+
erodeRadius: smoothErodeRadius,
|
|
3333
|
+
...summarizeMaskContours(mask)
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
const beforeConnectSummary = summarizeMaskContours(mask);
|
|
3337
|
+
if (beforeConnectSummary.selectedContourCount <= 1) {
|
|
3338
|
+
debugLog("traceWithBounds:mask:connect-skipped", {
|
|
3339
|
+
reason: "already-single-component",
|
|
3340
|
+
before: beforeConnectSummary
|
|
3341
|
+
});
|
|
3342
|
+
} else {
|
|
3343
|
+
const connectResult = this.findForceConnectResult(
|
|
3266
3344
|
mask,
|
|
3267
3345
|
paddedWidth,
|
|
3268
3346
|
paddedHeight,
|
|
3269
|
-
|
|
3347
|
+
minComponentArea,
|
|
3348
|
+
connectStartDilateRadius,
|
|
3349
|
+
connectMaxDilateRadius,
|
|
3350
|
+
connectErodeRatio
|
|
3270
3351
|
);
|
|
3271
|
-
if (
|
|
3272
|
-
mask
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
}
|
|
3352
|
+
if (debug) {
|
|
3353
|
+
debugLog("traceWithBounds:mask:after-connect", {
|
|
3354
|
+
before: beforeConnectSummary,
|
|
3355
|
+
appliedDilateRadius: connectResult.appliedDilateRadius,
|
|
3356
|
+
appliedErodeRadius: connectResult.appliedErodeRadius,
|
|
3357
|
+
reachedSingleComponent: connectResult.reachedSingleComponent,
|
|
3358
|
+
after: {
|
|
3359
|
+
rawContourCount: connectResult.rawContourCount,
|
|
3360
|
+
selectedContourCount: connectResult.selectedContourCount
|
|
3361
|
+
}
|
|
3362
|
+
});
|
|
3363
|
+
}
|
|
3364
|
+
mask = connectResult.mask;
|
|
3365
|
+
}
|
|
3366
|
+
if (debug) {
|
|
3367
|
+
const afterConnectSummary = summarizeMaskContours(mask);
|
|
3368
|
+
if (afterConnectSummary.selectedContourCount > 1) {
|
|
3369
|
+
debugLog("traceWithBounds:mask:connect-warning", {
|
|
3370
|
+
reason: "still-multi-component-after-connect-search",
|
|
3371
|
+
summary: afterConnectSummary
|
|
3372
|
+
});
|
|
3282
3373
|
}
|
|
3283
3374
|
}
|
|
3284
3375
|
const baseMask = mask;
|
|
@@ -3293,8 +3384,8 @@ var ImageTracer = class {
|
|
|
3293
3384
|
minComponentArea
|
|
3294
3385
|
);
|
|
3295
3386
|
if (!baseContours.length) {
|
|
3296
|
-
const w = (
|
|
3297
|
-
const h = (
|
|
3387
|
+
const w = (_f = options.scaleToWidth) != null ? _f : width;
|
|
3388
|
+
const h = (_g = options.scaleToHeight) != null ? _g : height;
|
|
3298
3389
|
debugLog("fallback:no-base-contour", { width: w, height: h });
|
|
3299
3390
|
return {
|
|
3300
3391
|
pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
|
|
@@ -3303,18 +3394,14 @@ var ImageTracer = class {
|
|
|
3303
3394
|
};
|
|
3304
3395
|
}
|
|
3305
3396
|
const baseUnpaddedContours = baseContours.map(
|
|
3306
|
-
(contour) =>
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
})),
|
|
3311
|
-
width,
|
|
3312
|
-
height
|
|
3313
|
-
)
|
|
3397
|
+
(contour) => contour.map((p) => ({
|
|
3398
|
+
x: p.x - padding,
|
|
3399
|
+
y: p.y - padding
|
|
3400
|
+
}))
|
|
3314
3401
|
).filter((contour) => contour.length > 2);
|
|
3315
3402
|
if (!baseUnpaddedContours.length) {
|
|
3316
|
-
const w = (
|
|
3317
|
-
const h = (
|
|
3403
|
+
const w = (_h = options.scaleToWidth) != null ? _h : width;
|
|
3404
|
+
const h = (_i = options.scaleToHeight) != null ? _i : height;
|
|
3318
3405
|
debugLog("fallback:empty-base-contours", { width: w, height: h });
|
|
3319
3406
|
return {
|
|
3320
3407
|
pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
|
|
@@ -3359,14 +3446,10 @@ var ImageTracer = class {
|
|
|
3359
3446
|
};
|
|
3360
3447
|
}
|
|
3361
3448
|
const expandedUnpaddedContours = expandedContours.map(
|
|
3362
|
-
(contour) =>
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
})),
|
|
3367
|
-
width,
|
|
3368
|
-
height
|
|
3369
|
-
)
|
|
3449
|
+
(contour) => contour.map((p) => ({
|
|
3450
|
+
x: p.x - padding,
|
|
3451
|
+
y: p.y - padding
|
|
3452
|
+
}))
|
|
3370
3453
|
).filter((contour) => contour.length > 2);
|
|
3371
3454
|
if (!expandedUnpaddedContours.length) {
|
|
3372
3455
|
debugLog("fallback:empty-expanded-contours", {
|
|
@@ -3399,39 +3482,64 @@ var ImageTracer = class {
|
|
|
3399
3482
|
options.scaleToHeight,
|
|
3400
3483
|
baseBounds
|
|
3401
3484
|
);
|
|
3402
|
-
baseBounds = this.boundsFromPoints(
|
|
3485
|
+
baseBounds = this.boundsFromPoints(
|
|
3486
|
+
this.flattenContours(baseScaledContours)
|
|
3487
|
+
);
|
|
3488
|
+
}
|
|
3489
|
+
if (expand > 0) {
|
|
3490
|
+
const expectedExpandedBounds = {
|
|
3491
|
+
x: baseBounds.x - expand,
|
|
3492
|
+
y: baseBounds.y - expand,
|
|
3493
|
+
width: baseBounds.width + expand * 2,
|
|
3494
|
+
height: baseBounds.height + expand * 2
|
|
3495
|
+
};
|
|
3496
|
+
if (expectedExpandedBounds.width > 0 && expectedExpandedBounds.height > 0 && globalBounds.width > 0 && globalBounds.height > 0) {
|
|
3497
|
+
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;
|
|
3498
|
+
if (shouldNormalizeExpandBounds) {
|
|
3499
|
+
const beforeNormalize = globalBounds;
|
|
3500
|
+
finalContours = this.translateContours(
|
|
3501
|
+
this.scaleContours(
|
|
3502
|
+
finalContours,
|
|
3503
|
+
expectedExpandedBounds.width,
|
|
3504
|
+
expectedExpandedBounds.height,
|
|
3505
|
+
globalBounds
|
|
3506
|
+
),
|
|
3507
|
+
expectedExpandedBounds.x,
|
|
3508
|
+
expectedExpandedBounds.y
|
|
3509
|
+
);
|
|
3510
|
+
globalBounds = this.boundsFromPoints(
|
|
3511
|
+
this.flattenContours(finalContours)
|
|
3512
|
+
);
|
|
3513
|
+
debugLog("traceWithBounds:expand-normalized", {
|
|
3514
|
+
expand,
|
|
3515
|
+
expectedExpandedBounds,
|
|
3516
|
+
beforeNormalize,
|
|
3517
|
+
afterNormalize: globalBounds
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3403
3521
|
}
|
|
3404
|
-
const useSmoothing = options.smoothing !== false;
|
|
3405
3522
|
debugLog("traceWithBounds:contours", {
|
|
3406
3523
|
baseContourCount: baseContoursRaw.length,
|
|
3407
3524
|
baseSelectedCount: baseContours.length,
|
|
3408
3525
|
expandedContourCount: expandedContoursRaw.length,
|
|
3409
3526
|
expandedSelectedCount: expandedContours.length,
|
|
3410
|
-
connectRadiusMax,
|
|
3411
|
-
appliedConnectRadius: rConnect,
|
|
3412
3527
|
baseBounds,
|
|
3413
3528
|
expandedBounds: globalBounds,
|
|
3414
3529
|
expandedDeltaX: globalBounds.width - baseBounds.width,
|
|
3415
3530
|
expandedDeltaY: globalBounds.height - baseBounds.height,
|
|
3531
|
+
expandedMayOverflowImageBounds: expand > 0,
|
|
3416
3532
|
useSmoothing,
|
|
3417
3533
|
componentMode
|
|
3418
3534
|
});
|
|
3419
3535
|
if (useSmoothing) {
|
|
3420
3536
|
return {
|
|
3421
|
-
pathData: this.contoursToSVGPaper(
|
|
3422
|
-
finalContours,
|
|
3423
|
-
(_n = options.simplifyTolerance) != null ? _n : 2.5
|
|
3424
|
-
),
|
|
3537
|
+
pathData: this.contoursToSVGPaper(finalContours, simplifyTolerance),
|
|
3425
3538
|
baseBounds,
|
|
3426
3539
|
bounds: globalBounds
|
|
3427
3540
|
};
|
|
3428
3541
|
} else {
|
|
3429
|
-
const simplifiedContours = finalContours.map(
|
|
3430
|
-
(points) => {
|
|
3431
|
-
var _a2;
|
|
3432
|
-
return this.douglasPeucker(points, (_a2 = options.simplifyTolerance) != null ? _a2 : 2);
|
|
3433
|
-
}
|
|
3434
|
-
).filter((points) => points.length > 2);
|
|
3542
|
+
const simplifiedContours = finalContours.map((points) => this.douglasPeucker(points, simplifyTolerance)).filter((points) => points.length > 2);
|
|
3435
3543
|
const pathData = this.contoursToSVG(simplifiedContours) || this.contoursToSVG(finalContours);
|
|
3436
3544
|
return {
|
|
3437
3545
|
pathData,
|
|
@@ -3494,39 +3602,101 @@ var ImageTracer = class {
|
|
|
3494
3602
|
}
|
|
3495
3603
|
return selected;
|
|
3496
3604
|
}
|
|
3497
|
-
static
|
|
3498
|
-
const
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3605
|
+
static summarizeAllContours(mask, width, height, minComponentArea) {
|
|
3606
|
+
const raw = this.traceAllContours(mask, width, height);
|
|
3607
|
+
const selected = this.selectContours(raw, "all", minComponentArea);
|
|
3608
|
+
return {
|
|
3609
|
+
rawCount: raw.length,
|
|
3610
|
+
selectedCount: selected.length
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
static findForceConnectResult(sourceMask, width, height, minComponentArea, startDilateRadius, maxDilateRadius, erodeRatio) {
|
|
3614
|
+
const initial = this.summarizeAllContours(
|
|
3615
|
+
sourceMask,
|
|
3616
|
+
width,
|
|
3617
|
+
height,
|
|
3618
|
+
minComponentArea
|
|
3619
|
+
);
|
|
3620
|
+
if (initial.selectedCount <= 1) {
|
|
3621
|
+
return {
|
|
3622
|
+
mask: sourceMask,
|
|
3623
|
+
appliedDilateRadius: 0,
|
|
3624
|
+
appliedErodeRadius: 0,
|
|
3625
|
+
reachedSingleComponent: true,
|
|
3626
|
+
rawContourCount: initial.rawCount,
|
|
3627
|
+
selectedContourCount: initial.selectedCount
|
|
3628
|
+
};
|
|
3514
3629
|
}
|
|
3515
|
-
|
|
3516
|
-
const
|
|
3517
|
-
|
|
3518
|
-
|
|
3630
|
+
const normalizedStart = Math.max(1, Math.floor(startDilateRadius));
|
|
3631
|
+
const normalizedMax = Math.max(
|
|
3632
|
+
normalizedStart,
|
|
3633
|
+
Math.floor(maxDilateRadius)
|
|
3634
|
+
);
|
|
3635
|
+
const normalizedErodeRatio = Math.max(0, erodeRatio);
|
|
3636
|
+
const evaluate = (dilateRadius) => {
|
|
3637
|
+
const erodeRadius = Math.max(
|
|
3638
|
+
1,
|
|
3639
|
+
Math.floor(dilateRadius * normalizedErodeRatio)
|
|
3640
|
+
);
|
|
3641
|
+
let mask = sourceMask;
|
|
3642
|
+
mask = circularMorphology(mask, width, height, dilateRadius, "dilate");
|
|
3643
|
+
mask = fillHoles(mask, width, height);
|
|
3644
|
+
mask = circularMorphology(mask, width, height, erodeRadius, "erode");
|
|
3645
|
+
mask = fillHoles(mask, width, height);
|
|
3646
|
+
const summary = this.summarizeAllContours(
|
|
3647
|
+
mask,
|
|
3648
|
+
width,
|
|
3649
|
+
height,
|
|
3650
|
+
minComponentArea
|
|
3651
|
+
);
|
|
3652
|
+
return {
|
|
3653
|
+
dilateRadius,
|
|
3654
|
+
erodeRadius,
|
|
3655
|
+
mask,
|
|
3656
|
+
rawCount: summary.rawCount,
|
|
3657
|
+
selectedCount: summary.selectedCount
|
|
3658
|
+
};
|
|
3659
|
+
};
|
|
3660
|
+
let low = normalizedStart - 1;
|
|
3661
|
+
let high = normalizedStart;
|
|
3662
|
+
let highResult = evaluate(high);
|
|
3663
|
+
while (high < normalizedMax && highResult.selectedCount > 1) {
|
|
3664
|
+
low = high;
|
|
3665
|
+
high = Math.min(
|
|
3666
|
+
normalizedMax,
|
|
3667
|
+
Math.max(high + 1, Math.floor(high * 1.6))
|
|
3668
|
+
);
|
|
3669
|
+
highResult = evaluate(high);
|
|
3670
|
+
}
|
|
3671
|
+
if (highResult.selectedCount > 1) {
|
|
3672
|
+
return {
|
|
3673
|
+
mask: highResult.mask,
|
|
3674
|
+
appliedDilateRadius: highResult.dilateRadius,
|
|
3675
|
+
appliedErodeRadius: highResult.erodeRadius,
|
|
3676
|
+
reachedSingleComponent: false,
|
|
3677
|
+
rawContourCount: highResult.rawCount,
|
|
3678
|
+
selectedContourCount: highResult.selectedCount
|
|
3679
|
+
};
|
|
3519
3680
|
}
|
|
3681
|
+
let best = highResult;
|
|
3520
3682
|
while (low + 1 < high) {
|
|
3521
3683
|
const mid = Math.floor((low + high) / 2);
|
|
3522
|
-
const
|
|
3523
|
-
if (
|
|
3684
|
+
const midResult = evaluate(mid);
|
|
3685
|
+
if (midResult.selectedCount <= 1) {
|
|
3686
|
+
best = midResult;
|
|
3524
3687
|
high = mid;
|
|
3525
3688
|
} else {
|
|
3526
3689
|
low = mid;
|
|
3527
3690
|
}
|
|
3528
3691
|
}
|
|
3529
|
-
return
|
|
3692
|
+
return {
|
|
3693
|
+
mask: best.mask,
|
|
3694
|
+
appliedDilateRadius: best.dilateRadius,
|
|
3695
|
+
appliedErodeRadius: best.erodeRadius,
|
|
3696
|
+
reachedSingleComponent: true,
|
|
3697
|
+
rawContourCount: best.rawCount,
|
|
3698
|
+
selectedContourCount: best.selectedCount
|
|
3699
|
+
};
|
|
3530
3700
|
}
|
|
3531
3701
|
static selectContours(contours, mode, minComponentArea) {
|
|
3532
3702
|
if (!contours.length) return [];
|
|
@@ -3568,154 +3738,6 @@ var ImageTracer = class {
|
|
|
3568
3738
|
height: maxY - minY
|
|
3569
3739
|
};
|
|
3570
3740
|
}
|
|
3571
|
-
static createMask(imageData, threshold, padding, paddedWidth, paddedHeight) {
|
|
3572
|
-
const { width, height, data } = imageData;
|
|
3573
|
-
const mask = new Uint8Array(paddedWidth * paddedHeight);
|
|
3574
|
-
let hasTransparency = false;
|
|
3575
|
-
for (let i = 3; i < data.length; i += 4) {
|
|
3576
|
-
if (data[i] < 255) {
|
|
3577
|
-
hasTransparency = true;
|
|
3578
|
-
break;
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
for (let y = 0; y < height; y++) {
|
|
3582
|
-
for (let x = 0; x < width; x++) {
|
|
3583
|
-
const srcIdx = (y * width + x) * 4;
|
|
3584
|
-
const r = data[srcIdx];
|
|
3585
|
-
const g = data[srcIdx + 1];
|
|
3586
|
-
const b = data[srcIdx + 2];
|
|
3587
|
-
const a = data[srcIdx + 3];
|
|
3588
|
-
const destIdx = (y + padding) * paddedWidth + (x + padding);
|
|
3589
|
-
if (hasTransparency) {
|
|
3590
|
-
if (a > threshold) {
|
|
3591
|
-
mask[destIdx] = 1;
|
|
3592
|
-
}
|
|
3593
|
-
} else {
|
|
3594
|
-
if (!(r > 240 && g > 240 && b > 240)) {
|
|
3595
|
-
mask[destIdx] = 1;
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
}
|
|
3600
|
-
return mask;
|
|
3601
|
-
}
|
|
3602
|
-
/**
|
|
3603
|
-
* Fast circular morphology using a distance-transform inspired separable approach.
|
|
3604
|
-
* O(N * R) complexity, where R is the radius.
|
|
3605
|
-
*/
|
|
3606
|
-
static circularMorphology(mask, width, height, radius, op) {
|
|
3607
|
-
const dilate = (m, r) => {
|
|
3608
|
-
const horizontalDist = new Int32Array(width * height);
|
|
3609
|
-
for (let y = 0; y < height; y++) {
|
|
3610
|
-
let lastSolid = -r * 2;
|
|
3611
|
-
for (let x = 0; x < width; x++) {
|
|
3612
|
-
if (m[y * width + x]) lastSolid = x;
|
|
3613
|
-
horizontalDist[y * width + x] = x - lastSolid;
|
|
3614
|
-
}
|
|
3615
|
-
lastSolid = width + r * 2;
|
|
3616
|
-
for (let x = width - 1; x >= 0; x--) {
|
|
3617
|
-
if (m[y * width + x]) lastSolid = x;
|
|
3618
|
-
horizontalDist[y * width + x] = Math.min(
|
|
3619
|
-
horizontalDist[y * width + x],
|
|
3620
|
-
lastSolid - x
|
|
3621
|
-
);
|
|
3622
|
-
}
|
|
3623
|
-
}
|
|
3624
|
-
const result = new Uint8Array(width * height);
|
|
3625
|
-
const r2 = r * r;
|
|
3626
|
-
for (let x = 0; x < width; x++) {
|
|
3627
|
-
for (let y = 0; y < height; y++) {
|
|
3628
|
-
let found = false;
|
|
3629
|
-
const minY = Math.max(0, y - r);
|
|
3630
|
-
const maxY = Math.min(height - 1, y + r);
|
|
3631
|
-
for (let dy = minY; dy <= maxY; dy++) {
|
|
3632
|
-
const dY = dy - y;
|
|
3633
|
-
const hDist = horizontalDist[dy * width + x];
|
|
3634
|
-
if (hDist * hDist + dY * dY <= r2) {
|
|
3635
|
-
found = true;
|
|
3636
|
-
break;
|
|
3637
|
-
}
|
|
3638
|
-
}
|
|
3639
|
-
if (found) result[y * width + x] = 1;
|
|
3640
|
-
}
|
|
3641
|
-
}
|
|
3642
|
-
return result;
|
|
3643
|
-
};
|
|
3644
|
-
const erode = (m, r) => {
|
|
3645
|
-
const inverted = new Uint8Array(m.length);
|
|
3646
|
-
for (let i = 0; i < m.length; i++) inverted[i] = m[i] ? 0 : 1;
|
|
3647
|
-
const dilatedInverted = dilate(inverted, r);
|
|
3648
|
-
const result = new Uint8Array(m.length);
|
|
3649
|
-
for (let i = 0; i < m.length; i++) result[i] = dilatedInverted[i] ? 0 : 1;
|
|
3650
|
-
return result;
|
|
3651
|
-
};
|
|
3652
|
-
switch (op) {
|
|
3653
|
-
case "dilate":
|
|
3654
|
-
return dilate(mask, radius);
|
|
3655
|
-
case "erode":
|
|
3656
|
-
return erode(mask, radius);
|
|
3657
|
-
case "closing":
|
|
3658
|
-
return erode(dilate(mask, radius), radius);
|
|
3659
|
-
case "opening":
|
|
3660
|
-
return dilate(erode(mask, radius), radius);
|
|
3661
|
-
default:
|
|
3662
|
-
return mask;
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
/**
|
|
3666
|
-
* Fills internal holes in the binary mask using flood fill from edges.
|
|
3667
|
-
*/
|
|
3668
|
-
static fillHoles(mask, width, height) {
|
|
3669
|
-
const background = new Uint8Array(width * height);
|
|
3670
|
-
const queue = [];
|
|
3671
|
-
for (let x = 0; x < width; x++) {
|
|
3672
|
-
if (mask[x] === 0) {
|
|
3673
|
-
background[x] = 1;
|
|
3674
|
-
queue.push([x, 0]);
|
|
3675
|
-
}
|
|
3676
|
-
const lastRow = (height - 1) * width + x;
|
|
3677
|
-
if (mask[lastRow] === 0) {
|
|
3678
|
-
background[lastRow] = 1;
|
|
3679
|
-
queue.push([x, height - 1]);
|
|
3680
|
-
}
|
|
3681
|
-
}
|
|
3682
|
-
for (let y = 1; y < height - 1; y++) {
|
|
3683
|
-
if (mask[y * width] === 0) {
|
|
3684
|
-
background[y * width] = 1;
|
|
3685
|
-
queue.push([0, y]);
|
|
3686
|
-
}
|
|
3687
|
-
if (mask[y * width + width - 1] === 0) {
|
|
3688
|
-
background[y * width + width - 1] = 1;
|
|
3689
|
-
queue.push([width - 1, y]);
|
|
3690
|
-
}
|
|
3691
|
-
}
|
|
3692
|
-
const dirs = [
|
|
3693
|
-
[0, 1],
|
|
3694
|
-
[0, -1],
|
|
3695
|
-
[1, 0],
|
|
3696
|
-
[-1, 0]
|
|
3697
|
-
];
|
|
3698
|
-
let head = 0;
|
|
3699
|
-
while (head < queue.length) {
|
|
3700
|
-
const [cx, cy] = queue[head++];
|
|
3701
|
-
for (const [dx, dy] of dirs) {
|
|
3702
|
-
const nx = cx + dx;
|
|
3703
|
-
const ny = cy + dy;
|
|
3704
|
-
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
|
3705
|
-
const nidx = ny * width + nx;
|
|
3706
|
-
if (mask[nidx] === 0 && background[nidx] === 0) {
|
|
3707
|
-
background[nidx] = 1;
|
|
3708
|
-
queue.push([nx, ny]);
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
3713
|
-
const filledMask = new Uint8Array(width * height);
|
|
3714
|
-
for (let i = 0; i < width * height; i++) {
|
|
3715
|
-
filledMask[i] = background[i] === 0 ? 1 : 0;
|
|
3716
|
-
}
|
|
3717
|
-
return filledMask;
|
|
3718
|
-
}
|
|
3719
3741
|
/**
|
|
3720
3742
|
* Traces all contours in the mask with optimized start-point detection
|
|
3721
3743
|
*/
|
|
@@ -3859,13 +3881,13 @@ var ImageTracer = class {
|
|
|
3859
3881
|
(points) => this.scalePoints(points, targetWidth, targetHeight, bounds)
|
|
3860
3882
|
);
|
|
3861
3883
|
}
|
|
3862
|
-
static
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3884
|
+
static translateContours(contours, offsetX, offsetY) {
|
|
3885
|
+
return contours.map(
|
|
3886
|
+
(points) => points.map((p) => ({
|
|
3887
|
+
x: p.x + offsetX,
|
|
3888
|
+
y: p.y + offsetY
|
|
3889
|
+
}))
|
|
3890
|
+
);
|
|
3869
3891
|
}
|
|
3870
3892
|
static pointsToSVG(points) {
|
|
3871
3893
|
if (points.length === 0) return "";
|
|
@@ -4027,6 +4049,14 @@ var DielineTool = class {
|
|
|
4027
4049
|
);
|
|
4028
4050
|
s.features = configService.get("dieline.features", s.features);
|
|
4029
4051
|
s.pathData = configService.get("dieline.pathData", s.pathData);
|
|
4052
|
+
const sourceWidth = Number(
|
|
4053
|
+
configService.get("dieline.customSourceWidthPx", 0)
|
|
4054
|
+
);
|
|
4055
|
+
const sourceHeight = Number(
|
|
4056
|
+
configService.get("dieline.customSourceHeightPx", 0)
|
|
4057
|
+
);
|
|
4058
|
+
s.customSourceWidthPx = Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0;
|
|
4059
|
+
s.customSourceHeightPx = Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0;
|
|
4030
4060
|
configService.onAnyChange((e) => {
|
|
4031
4061
|
if (e.key.startsWith("size.")) {
|
|
4032
4062
|
const nextSize = readSizeState(configService);
|
|
@@ -4085,6 +4115,12 @@ var DielineTool = class {
|
|
|
4085
4115
|
case "dieline.pathData":
|
|
4086
4116
|
s.pathData = e.value;
|
|
4087
4117
|
break;
|
|
4118
|
+
case "dieline.customSourceWidthPx":
|
|
4119
|
+
s.customSourceWidthPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
|
|
4120
|
+
break;
|
|
4121
|
+
case "dieline.customSourceHeightPx":
|
|
4122
|
+
s.customSourceHeightPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
|
|
4123
|
+
break;
|
|
4088
4124
|
}
|
|
4089
4125
|
this.updateDieline();
|
|
4090
4126
|
}
|
|
@@ -4252,10 +4288,17 @@ var DielineTool = class {
|
|
|
4252
4288
|
command: "detectEdge",
|
|
4253
4289
|
title: "Detect Edge from Image",
|
|
4254
4290
|
handler: async (imageUrl, options) => {
|
|
4255
|
-
var _a;
|
|
4291
|
+
var _a, _b, _c;
|
|
4256
4292
|
try {
|
|
4257
4293
|
const detectOptions = options || {};
|
|
4258
4294
|
const debug = detectOptions.debug === true;
|
|
4295
|
+
const tracerOptions = {
|
|
4296
|
+
expand: (_a = detectOptions.expand) != null ? _a : 0,
|
|
4297
|
+
smoothing: (_b = detectOptions.smoothing) != null ? _b : true,
|
|
4298
|
+
simplifyTolerance: (_c = detectOptions.simplifyTolerance) != null ? _c : 2,
|
|
4299
|
+
threshold: detectOptions.threshold,
|
|
4300
|
+
debug
|
|
4301
|
+
};
|
|
4259
4302
|
const loadImage = (url) => {
|
|
4260
4303
|
return new Promise((resolve, reject) => {
|
|
4261
4304
|
const img2 = new Image();
|
|
@@ -4267,7 +4310,7 @@ var DielineTool = class {
|
|
|
4267
4310
|
};
|
|
4268
4311
|
const [img, traced] = await Promise.all([
|
|
4269
4312
|
loadImage(imageUrl),
|
|
4270
|
-
ImageTracer.traceWithBounds(imageUrl,
|
|
4313
|
+
ImageTracer.traceWithBounds(imageUrl, tracerOptions)
|
|
4271
4314
|
]);
|
|
4272
4315
|
const { pathData, baseBounds, bounds } = traced;
|
|
4273
4316
|
if (debug) {
|
|
@@ -4278,21 +4321,8 @@ var DielineTool = class {
|
|
|
4278
4321
|
expandedBounds: bounds,
|
|
4279
4322
|
currentDielineWidth: s.width,
|
|
4280
4323
|
currentDielineHeight: s.height,
|
|
4281
|
-
options:
|
|
4282
|
-
|
|
4283
|
-
morphologyRadius: detectOptions.morphologyRadius,
|
|
4284
|
-
connectRadiusMax: detectOptions.connectRadiusMax,
|
|
4285
|
-
smoothing: detectOptions.smoothing,
|
|
4286
|
-
simplifyTolerance: detectOptions.simplifyTolerance,
|
|
4287
|
-
threshold: detectOptions.threshold,
|
|
4288
|
-
maskMode: detectOptions.maskMode,
|
|
4289
|
-
whiteThreshold: detectOptions.whiteThreshold,
|
|
4290
|
-
alphaOpaqueCutoff: detectOptions.alphaOpaqueCutoff,
|
|
4291
|
-
noChannels: detectOptions.noChannels,
|
|
4292
|
-
componentMode: detectOptions.componentMode,
|
|
4293
|
-
minComponentArea: detectOptions.minComponentArea,
|
|
4294
|
-
forceConnected: detectOptions.forceConnected
|
|
4295
|
-
}
|
|
4324
|
+
options: tracerOptions,
|
|
4325
|
+
strategy: "single-connected-silhouette"
|
|
4296
4326
|
});
|
|
4297
4327
|
}
|
|
4298
4328
|
return {
|
|
@@ -4435,7 +4465,9 @@ var DielineTool = class {
|
|
|
4435
4465
|
x: cx,
|
|
4436
4466
|
y: cy,
|
|
4437
4467
|
features: cutFeatures,
|
|
4438
|
-
pathData: this.state.pathData
|
|
4468
|
+
pathData: this.state.pathData,
|
|
4469
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4470
|
+
customSourceHeightPx: this.state.customSourceHeightPx
|
|
4439
4471
|
});
|
|
4440
4472
|
const mask = new Path(maskPathData, {
|
|
4441
4473
|
fill: outsideColor,
|
|
@@ -4458,6 +4490,8 @@ var DielineTool = class {
|
|
|
4458
4490
|
y: cy,
|
|
4459
4491
|
features: cutFeatures,
|
|
4460
4492
|
pathData: this.state.pathData,
|
|
4493
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4494
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4461
4495
|
canvasWidth: canvasW,
|
|
4462
4496
|
canvasHeight: canvasH
|
|
4463
4497
|
});
|
|
@@ -4482,6 +4516,8 @@ var DielineTool = class {
|
|
|
4482
4516
|
y: cy,
|
|
4483
4517
|
features: cutFeatures,
|
|
4484
4518
|
pathData: this.state.pathData,
|
|
4519
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4520
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4485
4521
|
canvasWidth: canvasW,
|
|
4486
4522
|
canvasHeight: canvasH
|
|
4487
4523
|
},
|
|
@@ -4494,6 +4530,8 @@ var DielineTool = class {
|
|
|
4494
4530
|
y: cy,
|
|
4495
4531
|
features: cutFeatures,
|
|
4496
4532
|
pathData: this.state.pathData,
|
|
4533
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4534
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4497
4535
|
canvasWidth: canvasW,
|
|
4498
4536
|
canvasHeight: canvasH
|
|
4499
4537
|
},
|
|
@@ -4523,6 +4561,8 @@ var DielineTool = class {
|
|
|
4523
4561
|
y: cy,
|
|
4524
4562
|
features: cutFeatures,
|
|
4525
4563
|
pathData: this.state.pathData,
|
|
4564
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4565
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4526
4566
|
canvasWidth: canvasW,
|
|
4527
4567
|
canvasHeight: canvasH
|
|
4528
4568
|
});
|
|
@@ -4547,6 +4587,8 @@ var DielineTool = class {
|
|
|
4547
4587
|
y: cy,
|
|
4548
4588
|
features: absoluteFeatures,
|
|
4549
4589
|
pathData: this.state.pathData,
|
|
4590
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4591
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4550
4592
|
canvasWidth: canvasW,
|
|
4551
4593
|
canvasHeight: canvasH
|
|
4552
4594
|
});
|
|
@@ -4592,7 +4634,9 @@ var DielineTool = class {
|
|
|
4592
4634
|
return {
|
|
4593
4635
|
...sceneGeometry,
|
|
4594
4636
|
strokeWidth: this.state.mainLine.width,
|
|
4595
|
-
pathData: this.state.pathData
|
|
4637
|
+
pathData: this.state.pathData,
|
|
4638
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4639
|
+
customSourceHeightPx: this.state.customSourceHeightPx
|
|
4596
4640
|
};
|
|
4597
4641
|
}
|
|
4598
4642
|
async exportCutImage(options) {
|
|
@@ -4651,6 +4695,8 @@ var DielineTool = class {
|
|
|
4651
4695
|
y: cy,
|
|
4652
4696
|
features: cutFeatures,
|
|
4653
4697
|
pathData,
|
|
4698
|
+
customSourceWidthPx: this.state.customSourceWidthPx,
|
|
4699
|
+
customSourceHeightPx: this.state.customSourceHeightPx,
|
|
4654
4700
|
canvasWidth: canvasW,
|
|
4655
4701
|
canvasHeight: canvasH
|
|
4656
4702
|
});
|
|
@@ -7009,6 +7055,82 @@ var WhiteInkTool = class {
|
|
|
7009
7055
|
height
|
|
7010
7056
|
};
|
|
7011
7057
|
}
|
|
7058
|
+
getImagePlacementState(id) {
|
|
7059
|
+
const rawItems = this.getConfig("image.items", []);
|
|
7060
|
+
if (!Array.isArray(rawItems) || rawItems.length === 0) return null;
|
|
7061
|
+
const matched = (id ? rawItems.find(
|
|
7062
|
+
(item) => item && typeof item === "object" && typeof item.id === "string" && item.id === id
|
|
7063
|
+
) : void 0) || rawItems[0];
|
|
7064
|
+
if (!matched || typeof matched !== "object") return null;
|
|
7065
|
+
const sourceUrl = typeof matched.sourceUrl === "string" && matched.sourceUrl.length > 0 ? matched.sourceUrl : typeof matched.url === "string" ? matched.url : "";
|
|
7066
|
+
const committedUrl = typeof matched.committedUrl === "string" ? matched.committedUrl : "";
|
|
7067
|
+
return {
|
|
7068
|
+
id: typeof matched.id === "string" && matched.id.length > 0 ? matched.id : id || "image",
|
|
7069
|
+
sourceUrl,
|
|
7070
|
+
committedUrl,
|
|
7071
|
+
left: Number.isFinite(matched.left) ? Number(matched.left) : 0.5,
|
|
7072
|
+
top: Number.isFinite(matched.top) ? Number(matched.top) : 0.5,
|
|
7073
|
+
scale: Number.isFinite(matched.scale) ? Math.max(0.05, matched.scale) : 1,
|
|
7074
|
+
angle: Number.isFinite(matched.angle) ? matched.angle : 0
|
|
7075
|
+
};
|
|
7076
|
+
}
|
|
7077
|
+
shouldRestoreSnapshotToSource(snapshot, placement) {
|
|
7078
|
+
if (!placement.sourceUrl || !placement.committedUrl) return false;
|
|
7079
|
+
if (placement.sourceUrl === placement.committedUrl) return false;
|
|
7080
|
+
return snapshot.src === placement.committedUrl;
|
|
7081
|
+
}
|
|
7082
|
+
getCoverScale(frame, source) {
|
|
7083
|
+
const frameW = Math.max(1, frame.width);
|
|
7084
|
+
const frameH = Math.max(1, frame.height);
|
|
7085
|
+
const sourceW = Math.max(1, source.width);
|
|
7086
|
+
const sourceH = Math.max(1, source.height);
|
|
7087
|
+
return Math.max(frameW / sourceW, frameH / sourceH);
|
|
7088
|
+
}
|
|
7089
|
+
async ensureSourceSize(sourceUrl) {
|
|
7090
|
+
if (!sourceUrl) return null;
|
|
7091
|
+
const cached = this.getSourceSize(sourceUrl);
|
|
7092
|
+
if (cached) return cached;
|
|
7093
|
+
try {
|
|
7094
|
+
const image = await this.loadImageElement(sourceUrl);
|
|
7095
|
+
const size = this.getElementSize(image);
|
|
7096
|
+
if (!size) return null;
|
|
7097
|
+
this.rememberSourceSize(sourceUrl, size);
|
|
7098
|
+
return {
|
|
7099
|
+
width: size.width,
|
|
7100
|
+
height: size.height
|
|
7101
|
+
};
|
|
7102
|
+
} catch (e) {
|
|
7103
|
+
return null;
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
async resolveAlignedImageSnapshot(snapshot) {
|
|
7107
|
+
const placement = this.getImagePlacementState(snapshot.id);
|
|
7108
|
+
if (!placement) return snapshot;
|
|
7109
|
+
if (!this.shouldRestoreSnapshotToSource(snapshot, placement)) {
|
|
7110
|
+
return snapshot;
|
|
7111
|
+
}
|
|
7112
|
+
const frame = this.getFrameRect();
|
|
7113
|
+
if (frame.width <= 0 || frame.height <= 0) {
|
|
7114
|
+
return snapshot;
|
|
7115
|
+
}
|
|
7116
|
+
const sourceSize = await this.ensureSourceSize(placement.sourceUrl);
|
|
7117
|
+
if (!sourceSize) return snapshot;
|
|
7118
|
+
const coverScale = this.getCoverScale(frame, sourceSize);
|
|
7119
|
+
return {
|
|
7120
|
+
...snapshot,
|
|
7121
|
+
src: placement.sourceUrl,
|
|
7122
|
+
element: void 0,
|
|
7123
|
+
left: frame.left + placement.left * frame.width,
|
|
7124
|
+
top: frame.top + placement.top * frame.height,
|
|
7125
|
+
scaleX: coverScale * placement.scale,
|
|
7126
|
+
scaleY: coverScale * placement.scale,
|
|
7127
|
+
angle: placement.angle,
|
|
7128
|
+
originX: "center",
|
|
7129
|
+
originY: "center",
|
|
7130
|
+
width: sourceSize.width,
|
|
7131
|
+
height: sourceSize.height
|
|
7132
|
+
};
|
|
7133
|
+
}
|
|
7012
7134
|
getImageElementFromObject(obj) {
|
|
7013
7135
|
if (!obj) return null;
|
|
7014
7136
|
if (typeof obj.getElement === "function") {
|
|
@@ -7381,9 +7503,11 @@ var WhiteInkTool = class {
|
|
|
7381
7503
|
let whiteSpecs = [];
|
|
7382
7504
|
let coverSpecs = [];
|
|
7383
7505
|
if (previewActive) {
|
|
7384
|
-
const
|
|
7506
|
+
const baseSnapshot = this.getImageSnapshot(this.getPrimaryImageObject());
|
|
7385
7507
|
const item = this.getEffectiveWhiteInkItem(this.resolveRenderItems());
|
|
7386
|
-
if (
|
|
7508
|
+
if (baseSnapshot && item) {
|
|
7509
|
+
const snapshot = await this.resolveAlignedImageSnapshot(baseSnapshot);
|
|
7510
|
+
if (seq !== this.renderSeq) return;
|
|
7387
7511
|
const sources = await this.resolveRenderSources(snapshot, item);
|
|
7388
7512
|
if (seq !== this.renderSeq) return;
|
|
7389
7513
|
if (sources == null ? void 0 : sources.whiteSrc) {
|