@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.
Files changed (39) hide show
  1. package/.test-dist/src/extensions/background.js +203 -0
  2. package/.test-dist/src/extensions/bridgeSelection.js +20 -0
  3. package/.test-dist/src/extensions/constraints.js +237 -0
  4. package/.test-dist/src/extensions/dieline.js +828 -0
  5. package/.test-dist/src/extensions/edgeScale.js +12 -0
  6. package/.test-dist/src/extensions/feature.js +825 -0
  7. package/.test-dist/src/extensions/featureComplete.js +32 -0
  8. package/.test-dist/src/extensions/film.js +167 -0
  9. package/.test-dist/src/extensions/geometry.js +545 -0
  10. package/.test-dist/src/extensions/image.js +1529 -0
  11. package/.test-dist/src/extensions/index.js +30 -0
  12. package/.test-dist/src/extensions/maskOps.js +279 -0
  13. package/.test-dist/src/extensions/mirror.js +104 -0
  14. package/.test-dist/src/extensions/ruler.js +345 -0
  15. package/.test-dist/src/extensions/sceneLayout.js +96 -0
  16. package/.test-dist/src/extensions/sceneLayoutModel.js +196 -0
  17. package/.test-dist/src/extensions/sceneVisibility.js +62 -0
  18. package/.test-dist/src/extensions/size.js +331 -0
  19. package/.test-dist/src/extensions/tracer.js +538 -0
  20. package/.test-dist/src/extensions/white-ink.js +1190 -0
  21. package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
  22. package/.test-dist/src/index.js +2 -19
  23. package/.test-dist/src/services/CanvasService.js +249 -0
  24. package/.test-dist/src/services/ViewportSystem.js +76 -0
  25. package/.test-dist/src/services/index.js +24 -0
  26. package/.test-dist/src/services/renderSpec.js +2 -0
  27. package/CHANGELOG.md +12 -0
  28. package/dist/index.d.mts +11 -0
  29. package/dist/index.d.ts +11 -0
  30. package/dist/index.js +519 -395
  31. package/dist/index.mjs +519 -395
  32. package/package.json +1 -1
  33. package/src/extensions/dieline.ts +66 -17
  34. package/src/extensions/geometry.ts +36 -3
  35. package/src/extensions/image.ts +2 -0
  36. package/src/extensions/maskOps.ts +84 -18
  37. package/src/extensions/sceneLayoutModel.ts +10 -0
  38. package/src/extensions/tracer.ts +360 -389
  39. 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 { shape, width, height, radius, x, y, pathData } = options;
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
- path.position = center;
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 dilate = (m, r) => {
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 = -r * 2;
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 + r * 2;
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 = r * r;
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 - r);
3019
- const maxY = Math.min(height - 1, y + r);
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 erode = (m, r) => {
3034
- const inverted = new Uint8Array(m.length);
3035
- for (let i = 0; i < m.length; i++) inverted[i] = m[i] ? 0 : 1;
3036
- const dilatedInverted = dilate(inverted, r);
3037
- const result = new Uint8Array(m.length);
3038
- for (let i = 0; i < m.length; i++) result[i] = dilatedInverted[i] ? 0 : 1;
3039
- return result;
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 dilate(mask, radius);
3113
+ return dilateDisk(mask, r);
3044
3114
  case "erode":
3045
- return erode(mask, radius);
3046
- case "closing":
3047
- return erode(dilate(mask, radius), radius);
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 dilate(erode(mask, radius), radius);
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, _j, _k, _l, _m, _n;
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 = (_a = options.threshold) != null ? _a : 10;
3228
- const componentMode = (_b = options.componentMode) != null ? _b : "largest";
3229
- const minComponentArea = Math.max(0, (_c = options.minComponentArea) != null ? _c : 0);
3230
- const forceConnected = options.forceConnected === true;
3231
- const adaptiveRadius = Math.max(
3232
- 5,
3233
- Math.floor(Math.max(width, height) * 0.02)
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 radius = (_d = options.morphologyRadius) != null ? _d : adaptiveRadius;
3236
- const expand = (_e = options.expand) != null ? _e : 0;
3237
- const noChannels = options.noChannels !== false;
3238
- const alphaOpaqueCutoff = (_f = options.alphaOpaqueCutoff) != null ? _f : 250;
3239
- const resolvedMaskMode = ((_g = options.maskMode) != null ? _g : "auto") === "auto" ? inferMaskMode(imageData, alphaOpaqueCutoff) : options.maskMode;
3240
- const alphaAnalysis = analyzeAlpha(imageData, alphaOpaqueCutoff);
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
- noChannels,
3248
- maskMode: (_h = options.maskMode) != null ? _h : "auto",
3249
- resolvedMaskMode,
3250
- alphaOpaqueCutoff,
3251
- alpha: {
3252
- minAlpha: alphaAnalysis.minAlpha,
3253
- belowOpaqueRatio: Number(alphaAnalysis.belowOpaqueRatio.toFixed(4)),
3254
- veryTransparentRatio: Number(
3255
- alphaAnalysis.veryTransparentRatio.toFixed(4)
3256
- )
3257
- },
3258
- componentMode,
3259
- minComponentArea,
3260
- forceConnected,
3261
- simplifyTolerance: (_i = options.simplifyTolerance) != null ? _i : 2.5,
3262
- smoothing: options.smoothing !== false
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 = radius + expand + 2;
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: options.maskMode,
3273
- whiteThreshold: options.whiteThreshold,
3315
+ maskMode,
3316
+ whiteThreshold,
3274
3317
  alphaOpaqueCutoff
3275
3318
  });
3276
- if (radius > 0) {
3277
- mask = circularMorphology(mask, paddedWidth, paddedHeight, radius, "closing");
3278
- }
3279
- if (noChannels) {
3280
- mask = fillHoles(mask, paddedWidth, paddedHeight);
3319
+ if (debug) {
3320
+ debugLog(
3321
+ "traceWithBounds:mask:after-create",
3322
+ summarizeMaskContours(mask)
3323
+ );
3281
3324
  }
3282
- if (radius > 0) {
3283
- const smoothRadius = Math.max(2, Math.floor(radius * 0.3));
3284
- mask = circularMorphology(mask, paddedWidth, paddedHeight, smoothRadius, "closing");
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
- const autoConnectRadiusMax = Math.max(
3287
- 10,
3288
- Math.floor(Math.max(width, height) * 0.12)
3348
+ mask = circularMorphology(
3349
+ mask,
3350
+ paddedWidth,
3351
+ paddedHeight,
3352
+ smoothDilateRadius,
3353
+ "dilate"
3289
3354
  );
3290
- const requestedConnectRadiusMax = options.connectRadiusMax;
3291
- const connectRadiusMax = requestedConnectRadiusMax === void 0 ? autoConnectRadiusMax : requestedConnectRadiusMax > 0 ? requestedConnectRadiusMax : forceConnected ? autoConnectRadiusMax : 0;
3292
- let rConnect = 0;
3293
- if (connectRadiusMax > 0) {
3294
- rConnect = forceConnected ? this.findMinimalMergeRadiusByContourCount(
3295
- mask,
3296
- paddedWidth,
3297
- paddedHeight,
3298
- connectRadiusMax,
3299
- minComponentArea
3300
- ) : findMinimalConnectRadius(
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
- connectRadiusMax
3382
+ minComponentArea,
3383
+ connectStartDilateRadius,
3384
+ connectMaxDilateRadius,
3385
+ connectErodeRatio
3305
3386
  );
3306
- if (rConnect > 0) {
3307
- mask = circularMorphology(
3308
- mask,
3309
- paddedWidth,
3310
- paddedHeight,
3311
- rConnect,
3312
- "closing"
3313
- );
3314
- if (noChannels) {
3315
- mask = fillHoles(mask, paddedWidth, paddedHeight);
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 = (_j = options.scaleToWidth) != null ? _j : width;
3332
- const h = (_k = options.scaleToHeight) != null ? _k : height;
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) => this.clampPointsToImageBounds(
3342
- contour.map((p) => ({
3343
- x: p.x - padding,
3344
- y: p.y - padding
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 = (_l = options.scaleToWidth) != null ? _l : width;
3352
- const h = (_m = options.scaleToHeight) != null ? _m : height;
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) => this.clampPointsToImageBounds(
3398
- contour.map((p) => ({
3399
- x: p.x - padding,
3400
- y: p.y - padding
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(this.flattenContours(baseScaledContours));
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 countSelectedContours(mask, width, height, minComponentArea) {
3533
- const contours = this.traceAllContours(mask, width, height);
3534
- return this.selectContours(contours, "all", minComponentArea).length;
3535
- }
3536
- static findMinimalMergeRadiusByContourCount(mask, width, height, maxRadius, minComponentArea) {
3537
- if (maxRadius <= 0) return 0;
3538
- if (this.countSelectedContours(mask, width, height, minComponentArea) <= 1) {
3539
- return 0;
3540
- }
3541
- let low = 0;
3542
- let high = 1;
3543
- while (high <= maxRadius) {
3544
- const closed = circularMorphology(mask, width, height, high, "closing");
3545
- if (this.countSelectedContours(closed, width, height, minComponentArea) <= 1) {
3546
- break;
3547
- }
3548
- high *= 2;
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
- if (high > maxRadius) high = maxRadius;
3551
- const highMask = circularMorphology(mask, width, height, high, "closing");
3552
- if (this.countSelectedContours(highMask, width, height, minComponentArea) > 1) {
3553
- return high;
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 midMask = circularMorphology(mask, width, height, mid, "closing");
3558
- if (this.countSelectedContours(midMask, width, height, minComponentArea) <= 1) {
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 high;
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 clampPointsToImageBounds(points, width, height) {
3898
- const maxX = Math.max(0, width);
3899
- const maxY = Math.max(0, height);
3900
- return points.map((p) => ({
3901
- x: Math.max(0, Math.min(maxX, p.x)),
3902
- y: Math.max(0, Math.min(maxY, p.y))
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, detectOptions)
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
- expand: (_a = detectOptions.expand) != null ? _a : 0,
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 snapshot = this.getImageSnapshot(this.getPrimaryImageObject());
7531
+ const baseSnapshot = this.getImageSnapshot(this.getPrimaryImageObject());
7410
7532
  const item = this.getEffectiveWhiteInkItem(this.resolveRenderItems());
7411
- if (snapshot && item) {
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) {