@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.mjs CHANGED
@@ -356,7 +356,17 @@ function selectOuterChain(args) {
356
356
  }
357
357
  function createBaseShape(options) {
358
358
  var _a;
359
- const { shape, width, height, radius, x, y, pathData } = options;
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
- path.position = center;
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 dilate = (m, r) => {
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 = -r * 2;
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 + r * 2;
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 = r * r;
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 - r);
2984
- const maxY = Math.min(height - 1, y + r);
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 erode = (m, r) => {
2999
- const inverted = new Uint8Array(m.length);
3000
- for (let i = 0; i < m.length; i++) inverted[i] = m[i] ? 0 : 1;
3001
- const dilatedInverted = dilate(inverted, r);
3002
- const result = new Uint8Array(m.length);
3003
- for (let i = 0; i < m.length; i++) result[i] = dilatedInverted[i] ? 0 : 1;
3004
- return result;
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 dilate(mask, radius);
3078
+ return dilateDisk(mask, r);
3009
3079
  case "erode":
3010
- return erode(mask, radius);
3011
- case "closing":
3012
- return erode(dilate(mask, radius), radius);
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 dilate(erode(mask, radius), radius);
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, _j, _k, _l, _m, _n;
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 = (_a = options.threshold) != null ? _a : 10;
3193
- const componentMode = (_b = options.componentMode) != null ? _b : "largest";
3194
- const minComponentArea = Math.max(0, (_c = options.minComponentArea) != null ? _c : 0);
3195
- const forceConnected = options.forceConnected === true;
3196
- const adaptiveRadius = Math.max(
3197
- 5,
3198
- Math.floor(Math.max(width, height) * 0.02)
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 radius = (_d = options.morphologyRadius) != null ? _d : adaptiveRadius;
3201
- const expand = (_e = options.expand) != null ? _e : 0;
3202
- const noChannels = options.noChannels !== false;
3203
- const alphaOpaqueCutoff = (_f = options.alphaOpaqueCutoff) != null ? _f : 250;
3204
- const resolvedMaskMode = ((_g = options.maskMode) != null ? _g : "auto") === "auto" ? inferMaskMode(imageData, alphaOpaqueCutoff) : options.maskMode;
3205
- const alphaAnalysis = analyzeAlpha(imageData, alphaOpaqueCutoff);
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
- noChannels,
3213
- maskMode: (_h = options.maskMode) != null ? _h : "auto",
3214
- resolvedMaskMode,
3215
- alphaOpaqueCutoff,
3216
- alpha: {
3217
- minAlpha: alphaAnalysis.minAlpha,
3218
- belowOpaqueRatio: Number(alphaAnalysis.belowOpaqueRatio.toFixed(4)),
3219
- veryTransparentRatio: Number(
3220
- alphaAnalysis.veryTransparentRatio.toFixed(4)
3221
- )
3222
- },
3223
- componentMode,
3224
- minComponentArea,
3225
- forceConnected,
3226
- simplifyTolerance: (_i = options.simplifyTolerance) != null ? _i : 2.5,
3227
- smoothing: options.smoothing !== false
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 = radius + expand + 2;
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: options.maskMode,
3238
- whiteThreshold: options.whiteThreshold,
3280
+ maskMode,
3281
+ whiteThreshold,
3239
3282
  alphaOpaqueCutoff
3240
3283
  });
3241
- if (radius > 0) {
3242
- mask = circularMorphology(mask, paddedWidth, paddedHeight, radius, "closing");
3243
- }
3244
- if (noChannels) {
3245
- mask = fillHoles(mask, paddedWidth, paddedHeight);
3284
+ if (debug) {
3285
+ debugLog(
3286
+ "traceWithBounds:mask:after-create",
3287
+ summarizeMaskContours(mask)
3288
+ );
3246
3289
  }
3247
- if (radius > 0) {
3248
- const smoothRadius = Math.max(2, Math.floor(radius * 0.3));
3249
- mask = circularMorphology(mask, paddedWidth, paddedHeight, smoothRadius, "closing");
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
- const autoConnectRadiusMax = Math.max(
3252
- 10,
3253
- Math.floor(Math.max(width, height) * 0.12)
3313
+ mask = circularMorphology(
3314
+ mask,
3315
+ paddedWidth,
3316
+ paddedHeight,
3317
+ smoothDilateRadius,
3318
+ "dilate"
3254
3319
  );
3255
- const requestedConnectRadiusMax = options.connectRadiusMax;
3256
- const connectRadiusMax = requestedConnectRadiusMax === void 0 ? autoConnectRadiusMax : requestedConnectRadiusMax > 0 ? requestedConnectRadiusMax : forceConnected ? autoConnectRadiusMax : 0;
3257
- let rConnect = 0;
3258
- if (connectRadiusMax > 0) {
3259
- rConnect = forceConnected ? this.findMinimalMergeRadiusByContourCount(
3260
- mask,
3261
- paddedWidth,
3262
- paddedHeight,
3263
- connectRadiusMax,
3264
- minComponentArea
3265
- ) : findMinimalConnectRadius(
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
- connectRadiusMax
3347
+ minComponentArea,
3348
+ connectStartDilateRadius,
3349
+ connectMaxDilateRadius,
3350
+ connectErodeRatio
3270
3351
  );
3271
- if (rConnect > 0) {
3272
- mask = circularMorphology(
3273
- mask,
3274
- paddedWidth,
3275
- paddedHeight,
3276
- rConnect,
3277
- "closing"
3278
- );
3279
- if (noChannels) {
3280
- mask = fillHoles(mask, paddedWidth, paddedHeight);
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 = (_j = options.scaleToWidth) != null ? _j : width;
3297
- const h = (_k = options.scaleToHeight) != null ? _k : height;
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) => this.clampPointsToImageBounds(
3307
- contour.map((p) => ({
3308
- x: p.x - padding,
3309
- y: p.y - padding
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 = (_l = options.scaleToWidth) != null ? _l : width;
3317
- const h = (_m = options.scaleToHeight) != null ? _m : height;
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) => this.clampPointsToImageBounds(
3363
- contour.map((p) => ({
3364
- x: p.x - padding,
3365
- y: p.y - padding
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(this.flattenContours(baseScaledContours));
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 countSelectedContours(mask, width, height, minComponentArea) {
3498
- const contours = this.traceAllContours(mask, width, height);
3499
- return this.selectContours(contours, "all", minComponentArea).length;
3500
- }
3501
- static findMinimalMergeRadiusByContourCount(mask, width, height, maxRadius, minComponentArea) {
3502
- if (maxRadius <= 0) return 0;
3503
- if (this.countSelectedContours(mask, width, height, minComponentArea) <= 1) {
3504
- return 0;
3505
- }
3506
- let low = 0;
3507
- let high = 1;
3508
- while (high <= maxRadius) {
3509
- const closed = circularMorphology(mask, width, height, high, "closing");
3510
- if (this.countSelectedContours(closed, width, height, minComponentArea) <= 1) {
3511
- break;
3512
- }
3513
- high *= 2;
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
- if (high > maxRadius) high = maxRadius;
3516
- const highMask = circularMorphology(mask, width, height, high, "closing");
3517
- if (this.countSelectedContours(highMask, width, height, minComponentArea) > 1) {
3518
- return high;
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 midMask = circularMorphology(mask, width, height, mid, "closing");
3523
- if (this.countSelectedContours(midMask, width, height, minComponentArea) <= 1) {
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 high;
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 clampPointsToImageBounds(points, width, height) {
3863
- const maxX = Math.max(0, width);
3864
- const maxY = Math.max(0, height);
3865
- return points.map((p) => ({
3866
- x: Math.max(0, Math.min(maxX, p.x)),
3867
- y: Math.max(0, Math.min(maxY, p.y))
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, detectOptions)
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
- expand: (_a = detectOptions.expand) != null ? _a : 0,
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 snapshot = this.getImageSnapshot(this.getPrimaryImageObject());
7506
+ const baseSnapshot = this.getImageSnapshot(this.getPrimaryImageObject());
7385
7507
  const item = this.getEffectiveWhiteInkItem(this.resolveRenderItems());
7386
- if (snapshot && item) {
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) {