@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pooder/kit",
3
- "version": "5.2.0",
3
+ "version": "5.3.1",
4
4
  "description": "Standard plugins for Pooder editor",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -37,6 +37,8 @@ export interface DielineGeometry {
37
37
  scale?: number;
38
38
  strokeWidth?: number;
39
39
  pathData?: string;
40
+ customSourceWidthPx?: number;
41
+ customSourceHeightPx?: number;
40
42
  }
41
43
 
42
44
  export interface LineStyle {
@@ -61,6 +63,8 @@ export interface DielineState {
61
63
  showBleedLines: boolean;
62
64
  features: DielineFeature[];
63
65
  pathData?: string;
66
+ customSourceWidthPx?: number;
67
+ customSourceHeightPx?: number;
64
68
  }
65
69
 
66
70
  const IMAGE_OBJECT_LAYER_ID = "image.user";
@@ -193,6 +197,20 @@ export class DielineTool implements Extension {
193
197
  );
194
198
  s.features = configService.get("dieline.features", s.features);
195
199
  s.pathData = configService.get("dieline.pathData", s.pathData);
200
+ const sourceWidth = Number(
201
+ configService.get("dieline.customSourceWidthPx", 0),
202
+ );
203
+ const sourceHeight = Number(
204
+ configService.get("dieline.customSourceHeightPx", 0),
205
+ );
206
+ s.customSourceWidthPx =
207
+ Number.isFinite(sourceWidth) && sourceWidth > 0
208
+ ? sourceWidth
209
+ : undefined;
210
+ s.customSourceHeightPx =
211
+ Number.isFinite(sourceHeight) && sourceHeight > 0
212
+ ? sourceHeight
213
+ : undefined;
196
214
 
197
215
  // Listen for changes
198
216
  configService.onAnyChange((e: { key: string; value: any }) => {
@@ -262,6 +280,18 @@ export class DielineTool implements Extension {
262
280
  case "dieline.pathData":
263
281
  s.pathData = e.value;
264
282
  break;
283
+ case "dieline.customSourceWidthPx":
284
+ s.customSourceWidthPx =
285
+ Number.isFinite(Number(e.value)) && Number(e.value) > 0
286
+ ? Number(e.value)
287
+ : undefined;
288
+ break;
289
+ case "dieline.customSourceHeightPx":
290
+ s.customSourceHeightPx =
291
+ Number.isFinite(Number(e.value)) && Number(e.value) > 0
292
+ ? Number(e.value)
293
+ : undefined;
294
+ break;
265
295
  }
266
296
  this.updateDieline();
267
297
  }
@@ -433,10 +463,26 @@ export class DielineTool implements Extension {
433
463
  {
434
464
  command: "detectEdge",
435
465
  title: "Detect Edge from Image",
436
- handler: async (imageUrl: string, options?: any) => {
466
+ handler: async (
467
+ imageUrl: string,
468
+ options?: {
469
+ expand?: number;
470
+ smoothing?: boolean;
471
+ simplifyTolerance?: number;
472
+ threshold?: number;
473
+ debug?: boolean;
474
+ },
475
+ ) => {
437
476
  try {
438
477
  const detectOptions = options || {};
439
478
  const debug = detectOptions.debug === true;
479
+ const tracerOptions = {
480
+ expand: detectOptions.expand ?? 0,
481
+ smoothing: detectOptions.smoothing ?? true,
482
+ simplifyTolerance: detectOptions.simplifyTolerance ?? 2,
483
+ threshold: detectOptions.threshold,
484
+ debug,
485
+ };
440
486
 
441
487
  // Helper to get image dimensions
442
488
  const loadImage = (url: string): Promise<HTMLImageElement> => {
@@ -451,7 +497,7 @@ export class DielineTool implements Extension {
451
497
 
452
498
  const [img, traced] = await Promise.all([
453
499
  loadImage(imageUrl),
454
- ImageTracer.traceWithBounds(imageUrl, detectOptions),
500
+ ImageTracer.traceWithBounds(imageUrl, tracerOptions),
455
501
  ]);
456
502
  const { pathData, baseBounds, bounds } = traced;
457
503
 
@@ -463,21 +509,8 @@ export class DielineTool implements Extension {
463
509
  expandedBounds: bounds,
464
510
  currentDielineWidth: s.width,
465
511
  currentDielineHeight: s.height,
466
- options: {
467
- expand: detectOptions.expand ?? 0,
468
- morphologyRadius: detectOptions.morphologyRadius,
469
- connectRadiusMax: detectOptions.connectRadiusMax,
470
- smoothing: detectOptions.smoothing,
471
- simplifyTolerance: detectOptions.simplifyTolerance,
472
- threshold: detectOptions.threshold,
473
- maskMode: detectOptions.maskMode,
474
- whiteThreshold: detectOptions.whiteThreshold,
475
- alphaOpaqueCutoff: detectOptions.alphaOpaqueCutoff,
476
- noChannels: detectOptions.noChannels,
477
- componentMode: detectOptions.componentMode,
478
- minComponentArea: detectOptions.minComponentArea,
479
- forceConnected: detectOptions.forceConnected,
480
- },
512
+ options: tracerOptions,
513
+ strategy: "single-connected-silhouette",
481
514
  });
482
515
  }
483
516
 
@@ -653,6 +686,8 @@ export class DielineTool implements Extension {
653
686
  y: cy,
654
687
  features: cutFeatures,
655
688
  pathData: this.state.pathData,
689
+ customSourceWidthPx: this.state.customSourceWidthPx,
690
+ customSourceHeightPx: this.state.customSourceHeightPx,
656
691
  });
657
692
  const mask = new Path(maskPathData, {
658
693
  fill: outsideColor,
@@ -680,6 +715,8 @@ export class DielineTool implements Extension {
680
715
  y: cy,
681
716
  features: cutFeatures,
682
717
  pathData: this.state.pathData,
718
+ customSourceWidthPx: this.state.customSourceWidthPx,
719
+ customSourceHeightPx: this.state.customSourceHeightPx,
683
720
  canvasWidth: canvasW,
684
721
  canvasHeight: canvasH,
685
722
  });
@@ -706,6 +743,8 @@ export class DielineTool implements Extension {
706
743
  y: cy,
707
744
  features: cutFeatures,
708
745
  pathData: this.state.pathData,
746
+ customSourceWidthPx: this.state.customSourceWidthPx,
747
+ customSourceHeightPx: this.state.customSourceHeightPx,
709
748
  canvasWidth: canvasW,
710
749
  canvasHeight: canvasH,
711
750
  },
@@ -718,6 +757,8 @@ export class DielineTool implements Extension {
718
757
  y: cy,
719
758
  features: cutFeatures,
720
759
  pathData: this.state.pathData,
760
+ customSourceWidthPx: this.state.customSourceWidthPx,
761
+ customSourceHeightPx: this.state.customSourceHeightPx,
721
762
  canvasWidth: canvasW,
722
763
  canvasHeight: canvasH,
723
764
  },
@@ -749,6 +790,8 @@ export class DielineTool implements Extension {
749
790
  y: cy,
750
791
  features: cutFeatures,
751
792
  pathData: this.state.pathData,
793
+ customSourceWidthPx: this.state.customSourceWidthPx,
794
+ customSourceHeightPx: this.state.customSourceHeightPx,
752
795
  canvasWidth: canvasW,
753
796
  canvasHeight: canvasH,
754
797
  });
@@ -778,6 +821,8 @@ export class DielineTool implements Extension {
778
821
  y: cy,
779
822
  features: absoluteFeatures,
780
823
  pathData: this.state.pathData,
824
+ customSourceWidthPx: this.state.customSourceWidthPx,
825
+ customSourceHeightPx: this.state.customSourceHeightPx,
781
826
  canvasWidth: canvasW,
782
827
  canvasHeight: canvasH,
783
828
  });
@@ -835,6 +880,8 @@ export class DielineTool implements Extension {
835
880
  ...sceneGeometry,
836
881
  strokeWidth: this.state.mainLine.width,
837
882
  pathData: this.state.pathData,
883
+ customSourceWidthPx: this.state.customSourceWidthPx,
884
+ customSourceHeightPx: this.state.customSourceHeightPx,
838
885
  } as DielineGeometry;
839
886
  }
840
887
 
@@ -901,6 +948,8 @@ export class DielineTool implements Extension {
901
948
  y: cy,
902
949
  features: cutFeatures,
903
950
  pathData,
951
+ customSourceWidthPx: this.state.customSourceWidthPx,
952
+ customSourceHeightPx: this.state.customSourceHeightPx,
904
953
  canvasWidth: canvasW,
905
954
  canvasHeight: canvasH,
906
955
  });
@@ -35,6 +35,8 @@ export interface GeometryOptions {
35
35
  y: number;
36
36
  features: Array<DielineFeature>;
37
37
  pathData?: string;
38
+ customSourceWidthPx?: number;
39
+ customSourceHeightPx?: number;
38
40
  canvasWidth?: number;
39
41
  canvasHeight?: number;
40
42
  }
@@ -191,7 +193,17 @@ function selectOuterChain(args: {
191
193
  * Creates the base dieline shape (Rect/Circle/Ellipse/Custom)
192
194
  */
193
195
  function createBaseShape(options: GeometryOptions): paper.PathItem {
194
- const { shape, width, height, radius, x, y, pathData } = options;
196
+ const {
197
+ shape,
198
+ width,
199
+ height,
200
+ radius,
201
+ x,
202
+ y,
203
+ pathData,
204
+ customSourceWidthPx,
205
+ customSourceHeightPx,
206
+ } = options;
195
207
  const center = new paper.Point(x, y);
196
208
 
197
209
  if (shape === "rect") {
@@ -220,16 +232,37 @@ function createBaseShape(options: GeometryOptions): paper.PathItem {
220
232
  single.pathData = pathData;
221
233
  return single;
222
234
  })();
223
- // Align center
224
- path.position = center;
235
+ const sourceWidth = Number(customSourceWidthPx ?? 0);
236
+ const sourceHeight = Number(customSourceHeightPx ?? 0);
237
+ if (
238
+ Number.isFinite(sourceWidth) &&
239
+ Number.isFinite(sourceHeight) &&
240
+ sourceWidth > 0 &&
241
+ sourceHeight > 0 &&
242
+ width > 0 &&
243
+ height > 0
244
+ ) {
245
+ // Preserve original detect-space offset/expand by mapping source image
246
+ // coordinates directly into the target dieline frame.
247
+ const targetLeft = x - width / 2;
248
+ const targetTop = y - height / 2;
249
+ path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
250
+ path.translate(new paper.Point(targetLeft, targetTop));
251
+ return path;
252
+ }
253
+
225
254
  if (
226
255
  width > 0 &&
227
256
  height > 0 &&
228
257
  path.bounds.width > 0 &&
229
258
  path.bounds.height > 0
230
259
  ) {
260
+ // Fallback for malformed custom-path metadata.
261
+ path.position = center;
231
262
  path.scale(width / path.bounds.width, height / path.bounds.height);
263
+ return path;
232
264
  }
265
+ path.position = center;
233
266
  return path;
234
267
  } else {
235
268
  return new paper.Path.Rectangle({
@@ -1794,6 +1794,8 @@ export class ImageTool implements Extension {
1794
1794
  this.normalizeItem({
1795
1795
  ...item,
1796
1796
  url,
1797
+ // Keep original source for next image-tool session editing,
1798
+ // and use committedUrl as non-image-tools render source.
1797
1799
  sourceUrl,
1798
1800
  committedUrl: url,
1799
1801
  }),
@@ -106,15 +106,21 @@ export function circularMorphology(
106
106
  radius: number,
107
107
  op: "dilate" | "erode" | "closing" | "opening",
108
108
  ): Uint8Array {
109
- const dilate = (m: Uint8Array, r: number) => {
109
+ const r = Math.max(0, Math.floor(radius));
110
+ if (r <= 0) {
111
+ return mask.slice();
112
+ }
113
+
114
+ // Disk kernel dilation (Euclidean metric).
115
+ const dilateDisk = (m: Uint8Array, radiusPx: number) => {
110
116
  const horizontalDist = new Int32Array(width * height);
111
117
  for (let y = 0; y < height; y++) {
112
- let lastSolid = -r * 2;
118
+ let lastSolid = -radiusPx * 2;
113
119
  for (let x = 0; x < width; x++) {
114
120
  if (m[y * width + x]) lastSolid = x;
115
121
  horizontalDist[y * width + x] = x - lastSolid;
116
122
  }
117
- lastSolid = width + r * 2;
123
+ lastSolid = width + radiusPx * 2;
118
124
  for (let x = width - 1; x >= 0; x--) {
119
125
  if (m[y * width + x]) lastSolid = x;
120
126
  horizontalDist[y * width + x] = Math.min(
@@ -125,12 +131,12 @@ export function circularMorphology(
125
131
  }
126
132
 
127
133
  const result = new Uint8Array(width * height);
128
- const r2 = r * r;
134
+ const r2 = radiusPx * radiusPx;
129
135
  for (let x = 0; x < width; x++) {
130
136
  for (let y = 0; y < height; y++) {
131
137
  let found = false;
132
- const minY = Math.max(0, y - r);
133
- const maxY = Math.min(height - 1, y + r);
138
+ const minY = Math.max(0, y - radiusPx);
139
+ const maxY = Math.min(height - 1, y + radiusPx);
134
140
  for (let dy = minY; dy <= maxY; dy++) {
135
141
  const dY = dy - y;
136
142
  const hDist = horizontalDist[dy * width + x];
@@ -145,24 +151,84 @@ export function circularMorphology(
145
151
  return result;
146
152
  };
147
153
 
148
- const erode = (m: Uint8Array, r: number) => {
149
- const inverted = new Uint8Array(m.length);
150
- for (let i = 0; i < m.length; i++) inverted[i] = m[i] ? 0 : 1;
151
- const dilatedInverted = dilate(inverted, r);
152
- const result = new Uint8Array(m.length);
153
- for (let i = 0; i < m.length; i++) result[i] = dilatedInverted[i] ? 0 : 1;
154
- return result;
154
+ // Diamond kernel erosion (L1 metric), implemented as radius iterations of
155
+ // 4-neighbor erosion. This is intentionally different from dilation kernel.
156
+ const erodeDiamond = (m: Uint8Array, radiusPx: number) => {
157
+ if (radiusPx <= 0) return m.slice();
158
+
159
+ let current = m;
160
+ for (let step = 0; step < radiusPx; step++) {
161
+ const next = new Uint8Array(width * height);
162
+ for (let y = 1; y < height - 1; y++) {
163
+ const row = y * width;
164
+ for (let x = 1; x < width - 1; x++) {
165
+ const idx = row + x;
166
+ if (
167
+ current[idx] &&
168
+ current[idx - 1] &&
169
+ current[idx + 1] &&
170
+ current[idx - width] &&
171
+ current[idx + width]
172
+ ) {
173
+ next[idx] = 1;
174
+ }
175
+ }
176
+ }
177
+ current = next;
178
+ }
179
+
180
+ return current;
181
+ };
182
+
183
+ // Restore thin bridges removed by erosion: if a removed pixel links two
184
+ // opposite neighbors in the source mask, bring it back.
185
+ const restoreBridgePixels = (source: Uint8Array, eroded: Uint8Array) => {
186
+ const restored = eroded.slice();
187
+ for (let y = 1; y < height - 1; y++) {
188
+ const row = y * width;
189
+ for (let x = 1; x < width - 1; x++) {
190
+ const idx = row + x;
191
+ if (!source[idx] || restored[idx]) continue;
192
+
193
+ const up = source[idx - width] === 1;
194
+ const down = source[idx + width] === 1;
195
+ const left = source[idx - 1] === 1;
196
+ const right = source[idx + 1] === 1;
197
+ const upLeft = source[idx - width - 1] === 1;
198
+ const upRight = source[idx - width + 1] === 1;
199
+ const downLeft = source[idx + width - 1] === 1;
200
+ const downRight = source[idx + width + 1] === 1;
201
+
202
+ const keepsBridge =
203
+ (left && right) ||
204
+ (up && down) ||
205
+ (upLeft && downRight) ||
206
+ (upRight && downLeft);
207
+ if (keepsBridge) {
208
+ restored[idx] = 1;
209
+ }
210
+ }
211
+ }
212
+
213
+ return restored;
214
+ };
215
+
216
+ const erodePreservingBridges = (m: Uint8Array, radiusPx: number) => {
217
+ const eroded = erodeDiamond(m, radiusPx);
218
+ return restoreBridgePixels(m, eroded);
155
219
  };
156
220
 
157
221
  switch (op) {
158
222
  case "dilate":
159
- return dilate(mask, radius);
223
+ return dilateDisk(mask, r);
160
224
  case "erode":
161
- return erode(mask, radius);
162
- case "closing":
163
- return erode(dilate(mask, radius), radius);
225
+ return erodePreservingBridges(mask, r);
226
+ case "closing": {
227
+ const erodeRadius = Math.max(1, Math.floor(r * 0.65));
228
+ return erodePreservingBridges(dilateDisk(mask, r), erodeRadius);
229
+ }
164
230
  case "opening":
165
- return dilate(erode(mask, radius), radius);
231
+ return dilateDisk(erodePreservingBridges(mask, r), r);
166
232
  default:
167
233
  return mask;
168
234
  }
@@ -56,6 +56,8 @@ export interface SceneGeometrySnapshot {
56
56
  offset: number;
57
57
  scale: number;
58
58
  pathData?: string;
59
+ customSourceWidthPx?: number;
60
+ customSourceHeightPx?: number;
59
61
  }
60
62
 
61
63
  const DEFAULT_SIZE_STATE: SizeState = {
@@ -325,6 +327,10 @@ export function buildSceneGeometry(
325
327
  "mm",
326
328
  );
327
329
  const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
330
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
331
+ const sourceHeight = Number(
332
+ configService.get("dieline.customSourceHeightPx", 0),
333
+ );
328
334
 
329
335
  return {
330
336
  shape: configService.get("dieline.shape", "rect"),
@@ -338,5 +344,9 @@ export function buildSceneGeometry(
338
344
  offset,
339
345
  scale: layout.scale,
340
346
  pathData: configService.get("dieline.pathData"),
347
+ customSourceWidthPx:
348
+ Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : undefined,
349
+ customSourceHeightPx:
350
+ Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : undefined,
341
351
  };
342
352
  }