@pixerate/editor 0.259.0 → 0.260.0

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 (74) hide show
  1. package/dist/agent/index.cjs +2 -2
  2. package/dist/agent/index.js +1 -1
  3. package/dist/animation-editor/index.cjs +61 -59
  4. package/dist/animation-editor/index.js +5 -3
  5. package/dist/brushes/index.cjs +52 -52
  6. package/dist/brushes/index.js +6 -6
  7. package/dist/{brushstrokeRender-JI4W6F2G.js → brushstrokeRender-25Q5BEY6.js} +17 -7
  8. package/dist/{brushstrokeRender-MQ6PSDM5.cjs → brushstrokeRender-N6UPZSQS.cjs} +31 -21
  9. package/dist/canvas/brushTypes.d.cts +5 -0
  10. package/dist/canvas/brushTypes.d.ts +5 -0
  11. package/dist/canvas/drawController.d.cts +15 -1
  12. package/dist/canvas/drawController.d.ts +15 -1
  13. package/dist/canvas/index.cjs +136 -102
  14. package/dist/canvas/index.js +35 -1
  15. package/dist/{chunk-3PPJRMIG.js → chunk-3O47TPGX.js} +197 -18
  16. package/dist/{chunk-5Q5MLXAD.cjs → chunk-3TQW5MPW.cjs} +23 -23
  17. package/dist/{chunk-JBBJE76O.cjs → chunk-3ZRNK3RO.cjs} +1138 -85
  18. package/dist/{chunk-YSGW7VV4.cjs → chunk-43NT242D.cjs} +1401 -228
  19. package/dist/{chunk-SMTUQWGY.js → chunk-5IW65VQ6.js} +1 -1
  20. package/dist/{chunk-YQ4PQTBB.js → chunk-6NZL24W4.js} +9 -1
  21. package/dist/{chunk-O3ZVZSNJ.cjs → chunk-7AUQ2UJ3.cjs} +137 -129
  22. package/dist/{chunk-4M4E3N6X.js → chunk-CQVTGBQI.js} +1 -1
  23. package/dist/{chunk-SSFMWJDK.js → chunk-D4TE5JNW.js} +1192 -19
  24. package/dist/{chunk-AORZ5M5C.js → chunk-EHALWBBD.js} +1 -1
  25. package/dist/{chunk-6WCQ35GI.cjs → chunk-NHA26O5X.cjs} +2 -2
  26. package/dist/{chunk-DPYAO5U6.cjs → chunk-PVJMTLPJ.cjs} +197 -18
  27. package/dist/{chunk-MQGQTKVW.cjs → chunk-PYTJBVW5.cjs} +29 -29
  28. package/dist/{chunk-IXQZHCOF.js → chunk-QAR2N7HZ.js} +1 -1
  29. package/dist/{chunk-5P4JEOIV.js → chunk-TMDDMJCR.js} +2 -2
  30. package/dist/{chunk-RSIKDWOP.cjs → chunk-TZOX7KWV.cjs} +15 -15
  31. package/dist/{chunk-IPUTIUGQ.cjs → chunk-UVZ7RQOE.cjs} +1 -1
  32. package/dist/{chunk-A7GJ6BXI.js → chunk-WJ4WQJCC.js} +1062 -9
  33. package/dist/{chunk-ICTPLVSR.js → chunk-X2EXYY4J.js} +1 -1
  34. package/dist/{chunk-WQCNC4DK.js → chunk-XC6DPRYI.js} +695 -4
  35. package/dist/{chunk-KBYB7OWB.cjs → chunk-YHDHFJUC.cjs} +776 -85
  36. package/dist/{chunk-P3PZ63M3.cjs → chunk-YLUIFXSL.cjs} +35 -35
  37. package/dist/commands/index.cjs +2 -2
  38. package/dist/commands/index.js +1 -1
  39. package/dist/image-editor/index.cjs +1385 -2484
  40. package/dist/image-editor/index.js +139 -1238
  41. package/dist/image-editor/selection-worker.cjs +3 -4
  42. package/dist/image-editor/selection-worker.js +1 -2
  43. package/dist/paint/index.cjs +3 -5
  44. package/dist/paint/index.js +40 -42
  45. package/dist/paint-editor/contract.d.cts +29 -0
  46. package/dist/paint-editor/contract.d.ts +29 -0
  47. package/dist/paint-editor/dab-stroke.d.cts +5 -0
  48. package/dist/paint-editor/dab-stroke.d.ts +5 -0
  49. package/dist/paint-editor/index.cjs +402 -258
  50. package/dist/paint-editor/index.d.cts +1 -0
  51. package/dist/paint-editor/index.d.ts +1 -0
  52. package/dist/paint-editor/index.js +197 -53
  53. package/dist/paint-editor/stamp-tip.d.cts +17 -0
  54. package/dist/paint-editor/stamp-tip.d.ts +17 -0
  55. package/dist/paint-editor/ui.d.cts +21 -1
  56. package/dist/paint-editor/ui.d.ts +21 -1
  57. package/dist/raster/index.cjs +2 -4
  58. package/dist/raster/index.js +6 -8
  59. package/dist/vector/brushstroke.d.cts +6 -0
  60. package/dist/vector/brushstroke.d.ts +6 -0
  61. package/dist/vector/controller.d.cts +11 -2
  62. package/dist/vector/controller.d.ts +11 -2
  63. package/dist/vector/index.cjs +990 -973
  64. package/dist/vector/index.js +28 -11
  65. package/package.json +1 -1
  66. package/skills/pixerate-brushes/SKILL.md +1 -1
  67. package/skills/pixerate-canvas/SKILL.md +3 -1
  68. package/skills/pixerate-vector/SKILL.md +2 -2
  69. package/dist/chunk-2VJAVXLI.js +0 -192
  70. package/dist/chunk-62Z5WLRA.js +0 -700
  71. package/dist/chunk-BOHDJJ2O.cjs +0 -192
  72. package/dist/chunk-NKGB64CW.cjs +0 -700
  73. package/dist/chunk-SS53NNZO.cjs +0 -1066
  74. package/dist/chunk-ZNA3IQZF.js +0 -1066
@@ -1,16 +1,18 @@
1
1
  import {
2
2
  boxMean,
3
+ combineSelections,
3
4
  createSelectionMask,
4
5
  decodeSelectionRle,
5
6
  decontaminateColors,
7
+ ellipsePoints,
6
8
  encodeSelectionRle,
7
9
  guidedFilterColor,
8
10
  hashMaskContent,
9
- resizeSelection
10
- } from "./chunk-ZNA3IQZF.js";
11
- import {
12
- parseCssColor
13
- } from "./chunk-3CI7FRMK.js";
11
+ normalizeRect,
12
+ resizeSelection,
13
+ selectionArea,
14
+ serializeMask
15
+ } from "./chunk-WJ4WQJCC.js";
14
16
  import {
15
17
  DEFAULT_GLOBAL_LIGHT,
16
18
  DeepCompositor,
@@ -48,7 +50,7 @@ import {
48
50
  validateEffectGradient,
49
51
  validateLayerEffects,
50
52
  warpSurface
51
- } from "./chunk-3PPJRMIG.js";
53
+ } from "./chunk-3O47TPGX.js";
52
54
  import {
53
55
  affineFromHomography,
54
56
  applyHomography,
@@ -106,7 +108,7 @@ import {
106
108
  transformTextRect,
107
109
  translateMatrix,
108
110
  warpPresetMesh
109
- } from "./chunk-YQ4PQTBB.js";
111
+ } from "./chunk-6NZL24W4.js";
110
112
  import {
111
113
  bezierPathBounds,
112
114
  outlineStroke,
@@ -115,6 +117,12 @@ import {
115
117
  strokeOutlineRings,
116
118
  transformBezierPath
117
119
  } from "./chunk-WEIK3UYB.js";
120
+ import {
121
+ parseCssColor
122
+ } from "./chunk-3CI7FRMK.js";
123
+ import {
124
+ MAX_BRUSH_SIZE
125
+ } from "./chunk-GCVFEPJG.js";
118
126
  import {
119
127
  base64ToBytes,
120
128
  bytesToBase64,
@@ -4547,6 +4555,312 @@ function isLegacyShape(stack) {
4547
4555
  return true;
4548
4556
  }
4549
4557
 
4558
+ // src/image-editor/selection/magnetic.ts
4559
+ var BLOCK = 64;
4560
+ var W_ZERO = 0.3;
4561
+ var W_GRADIENT = 0.4;
4562
+ var W_DIRECTION = 0.3;
4563
+ var EdgeMap = class {
4564
+ width;
4565
+ height;
4566
+ contrast;
4567
+ luma;
4568
+ blocks = /* @__PURE__ */ new Map();
4569
+ constructor(pixels, options = {}) {
4570
+ const { width, height, data } = pixels;
4571
+ if (!(width >= 1 && height >= 1) || data.length < width * height * 4) throw new Error("Invalid pixel buffer for edge detection.");
4572
+ this.width = width;
4573
+ this.height = height;
4574
+ this.contrast = Math.min(1, Math.max(0, options.contrast ?? 0.1));
4575
+ const luma = new Uint8Array(width * height);
4576
+ for (let i = 0, p = 0; i < luma.length; i++, p += 4) {
4577
+ const a = data[p + 3] / 255;
4578
+ const y = 0.299 * data[p] + 0.587 * data[p + 1] + 0.114 * data[p + 2];
4579
+ luma[i] = Math.round(y * a + 255 * (1 - a));
4580
+ }
4581
+ this.luma = luma;
4582
+ }
4583
+ /** Edge strength at (x, y) in whole pixels, 0..1 (0 outside the image or below the contrast). */
4584
+ strength(x, y) {
4585
+ if (x < 0 || y < 0 || x >= this.width || y >= this.height) return 0;
4586
+ const b = this.block(x, y);
4587
+ return b.strength[y % BLOCK * BLOCK + x % BLOCK];
4588
+ }
4589
+ block(x, y) {
4590
+ const bx = Math.floor(x / BLOCK);
4591
+ const by = Math.floor(y / BLOCK);
4592
+ const key = by * 65536 + bx;
4593
+ let b = this.blocks.get(key);
4594
+ if (!b) {
4595
+ b = this.computeBlock(bx * BLOCK, by * BLOCK);
4596
+ this.blocks.set(key, b);
4597
+ }
4598
+ return b;
4599
+ }
4600
+ computeBlock(x0, y0) {
4601
+ const { width: W, height: H, luma } = this;
4602
+ const M = 2;
4603
+ const S = BLOCK + 2 * M;
4604
+ const smooth = new Float32Array(S * S);
4605
+ const at = (x, y) => luma[Math.min(H - 1, Math.max(0, y)) * W + Math.min(W - 1, Math.max(0, x))];
4606
+ for (let j = 0; j < S; j++) {
4607
+ const y = y0 - M + j;
4608
+ for (let i = 0; i < S; i++) {
4609
+ const x = x0 - M + i;
4610
+ const v = at(x - 1, y - 1) + 2 * at(x, y - 1) + at(x + 1, y - 1) + 2 * at(x - 1, y) + 4 * at(x, y) + 2 * at(x + 1, y) + at(x - 1, y + 1) + 2 * at(x, y + 1) + at(x + 1, y + 1);
4611
+ smooth[j * S + i] = v / (16 * 255);
4612
+ }
4613
+ }
4614
+ const lap = new Float32Array(S * S);
4615
+ for (let j = 1; j < S - 1; j++) {
4616
+ for (let i = 1; i < S - 1; i++) {
4617
+ const k = j * S + i;
4618
+ lap[k] = smooth[k - 1] + smooth[k + 1] + smooth[k - S] + smooth[k + S] - 4 * smooth[k];
4619
+ }
4620
+ }
4621
+ const n = BLOCK * BLOCK;
4622
+ const strength = new Float32Array(n);
4623
+ const dirX = new Float32Array(n);
4624
+ const dirY = new Float32Array(n);
4625
+ const zero = new Uint8Array(n);
4626
+ const c = this.contrast;
4627
+ for (let j = 0; j < BLOCK; j++) {
4628
+ if (y0 + j >= H) break;
4629
+ for (let i = 0; i < BLOCK; i++) {
4630
+ if (x0 + i >= W) break;
4631
+ const k = (j + M) * S + (i + M);
4632
+ const gx = (smooth[k + 1] - smooth[k - 1]) / 2;
4633
+ const gy = (smooth[k + S] - smooth[k - S]) / 2;
4634
+ const g = Math.hypot(gx, gy);
4635
+ const e = Math.min(1, 2 * g);
4636
+ if (e < c || g === 0) continue;
4637
+ const o = j * BLOCK + i;
4638
+ strength[o] = e;
4639
+ dirX[o] = gy / g;
4640
+ dirY[o] = -gx / g;
4641
+ const l = lap[k];
4642
+ for (const d of [1, -1, S, -S]) {
4643
+ const m = lap[k + d];
4644
+ if (l > 0 && m < 0 || l < 0 && m > 0 || l === 0) {
4645
+ if (Math.abs(l) <= Math.abs(m)) {
4646
+ zero[o] = 1;
4647
+ break;
4648
+ }
4649
+ }
4650
+ }
4651
+ }
4652
+ }
4653
+ return { strength, dirX, dirY, zero };
4654
+ }
4655
+ /** Features of the pixel at (x, y), which must be inside the image. */
4656
+ features(x, y) {
4657
+ const b = this.block(x, y);
4658
+ const o = y % BLOCK * BLOCK + x % BLOCK;
4659
+ return { strength: b.strength[o], dirX: b.dirX[o], dirY: b.dirY[o], zero: b.zero[o] };
4660
+ }
4661
+ };
4662
+ var clampPixel = (v, max) => Math.min(max - 1, Math.max(0, Math.floor(v)));
4663
+ function snapToEdge(map, point2, width = 10) {
4664
+ const cx = clampPixel(point2.x, map.width);
4665
+ const cy = clampPixel(point2.y, map.height);
4666
+ const r = Math.max(0, Math.round(width));
4667
+ let best = 0;
4668
+ let bestD = Infinity;
4669
+ let bx = cx;
4670
+ let by = cy;
4671
+ for (let y = Math.max(0, cy - r); y <= Math.min(map.height - 1, cy + r); y++) {
4672
+ for (let x = Math.max(0, cx - r); x <= Math.min(map.width - 1, cx + r); x++) {
4673
+ const d = (x - cx) ** 2 + (y - cy) ** 2;
4674
+ if (d > r * r) continue;
4675
+ const s = map.strength(x, y);
4676
+ if (s > best + 0.02 || s > 0 && Math.abs(s - best) <= 0.02 && d < bestD) {
4677
+ if (s > best) best = s;
4678
+ bestD = d;
4679
+ bx = x;
4680
+ by = y;
4681
+ }
4682
+ }
4683
+ }
4684
+ return best > 0 ? { x: bx + 0.5, y: by + 0.5 } : point2;
4685
+ }
4686
+ var Heap = class {
4687
+ ids = new Int32Array(256);
4688
+ keys = new Float64Array(256);
4689
+ size = 0;
4690
+ push(id, key) {
4691
+ if (this.size === this.ids.length) {
4692
+ const ids = new Int32Array(this.size * 2);
4693
+ ids.set(this.ids);
4694
+ const keys = new Float64Array(this.size * 2);
4695
+ keys.set(this.keys);
4696
+ this.ids = ids;
4697
+ this.keys = keys;
4698
+ }
4699
+ let i = this.size++;
4700
+ while (i > 0) {
4701
+ const p = i - 1 >> 1;
4702
+ if (this.keys[p] <= key) break;
4703
+ this.ids[i] = this.ids[p];
4704
+ this.keys[i] = this.keys[p];
4705
+ i = p;
4706
+ }
4707
+ this.ids[i] = id;
4708
+ this.keys[i] = key;
4709
+ }
4710
+ /** Removes the cheapest entry; read it from `topId` / `topKey` first. */
4711
+ pop() {
4712
+ const n = --this.size;
4713
+ const id = this.ids[n];
4714
+ const key = this.keys[n];
4715
+ let i = 0;
4716
+ for (; ; ) {
4717
+ let c = 2 * i + 1;
4718
+ if (c >= n) break;
4719
+ if (c + 1 < n && this.keys[c + 1] < this.keys[c]) c++;
4720
+ if (this.keys[c] >= key) break;
4721
+ this.ids[i] = this.ids[c];
4722
+ this.keys[i] = this.keys[c];
4723
+ i = c;
4724
+ }
4725
+ this.ids[i] = id;
4726
+ this.keys[i] = key;
4727
+ }
4728
+ get topId() {
4729
+ return this.ids[0];
4730
+ }
4731
+ get topKey() {
4732
+ return this.keys[0];
4733
+ }
4734
+ };
4735
+ var NEIGHBOURS = [
4736
+ [1, 0],
4737
+ [-1, 0],
4738
+ [0, 1],
4739
+ [0, -1],
4740
+ [1, 1],
4741
+ [1, -1],
4742
+ [-1, 1],
4743
+ [-1, -1]
4744
+ ];
4745
+ function liveWire(map, from, to, band) {
4746
+ const ax = clampPixel(from.x, map.width);
4747
+ const ay = clampPixel(from.y, map.height);
4748
+ const bx = clampPixel(to.x, map.width);
4749
+ const by = clampPixel(to.y, map.height);
4750
+ if (ax === bx && ay === by) return [{ x: ax + 0.5, y: ay + 0.5 }];
4751
+ const len = Math.hypot(bx - ax, by - ay);
4752
+ const half = Math.max(2, band ?? Math.max(16, len / 4));
4753
+ const rx0 = Math.max(0, Math.floor(Math.min(ax, bx) - half));
4754
+ const ry0 = Math.max(0, Math.floor(Math.min(ay, by) - half));
4755
+ const rx1 = Math.min(map.width - 1, Math.ceil(Math.max(ax, bx) + half));
4756
+ const ry1 = Math.min(map.height - 1, Math.ceil(Math.max(ay, by) + half));
4757
+ const w = rx1 - rx0 + 1;
4758
+ const h = ry1 - ry0 + 1;
4759
+ const n = w * h;
4760
+ const strength = new Float32Array(n);
4761
+ const dirX = new Float32Array(n);
4762
+ const dirY = new Float32Array(n);
4763
+ const zero = new Uint8Array(n);
4764
+ const inside = new Uint8Array(n);
4765
+ let maxStrength = 0;
4766
+ const ux = (bx - ax) / len;
4767
+ const uy = (by - ay) / len;
4768
+ for (let y = 0; y < h; y++) {
4769
+ for (let x = 0; x < w; x++) {
4770
+ const px2 = rx0 + x - ax;
4771
+ const py = ry0 + y - ay;
4772
+ const t = Math.min(len, Math.max(0, px2 * ux + py * uy));
4773
+ if (Math.hypot(px2 - t * ux, py - t * uy) > half) continue;
4774
+ const i = y * w + x;
4775
+ inside[i] = 1;
4776
+ const f = map.features(rx0 + x, ry0 + y);
4777
+ strength[i] = f.strength;
4778
+ dirX[i] = f.dirX;
4779
+ dirY[i] = f.dirY;
4780
+ zero[i] = f.zero;
4781
+ if (f.strength > maxStrength) maxStrength = f.strength;
4782
+ }
4783
+ }
4784
+ const scale = maxStrength > 0 ? 1 / maxStrength : 0;
4785
+ const dist = new Float64Array(n).fill(Infinity);
4786
+ const prev = new Int32Array(n).fill(-1);
4787
+ const done = new Uint8Array(n);
4788
+ const start = (ay - ry0) * w + (ax - rx0);
4789
+ const goal = (by - ry0) * w + (bx - rx0);
4790
+ dist[start] = 0;
4791
+ const heap = new Heap();
4792
+ heap.push(start, 0);
4793
+ while (heap.size) {
4794
+ const p = heap.topId;
4795
+ const d = heap.topKey;
4796
+ heap.pop();
4797
+ if (done[p] || d > dist[p]) continue;
4798
+ done[p] = 1;
4799
+ if (p === goal) break;
4800
+ const x = p % w;
4801
+ const y = (p - x) / w;
4802
+ for (const [dx, dy] of NEIGHBOURS) {
4803
+ const qx = x + dx;
4804
+ const qy = y + dy;
4805
+ if (qx < 0 || qy < 0 || qx >= w || qy >= h) continue;
4806
+ const q2 = qy * w + qx;
4807
+ if (!inside[q2] || done[q2]) continue;
4808
+ const step = dx && dy ? Math.SQRT2 : 1;
4809
+ const lx = dx / step;
4810
+ const ly = dy / step;
4811
+ const dp = Math.min(1, Math.abs(dirX[p] * lx + dirY[p] * ly));
4812
+ const dq = Math.min(1, Math.abs(dirX[q2] * lx + dirY[q2] * ly));
4813
+ const fD = (Math.acos(dp) + Math.acos(dq)) / Math.PI;
4814
+ const fG = 1 - strength[q2] * scale;
4815
+ const fZ = 1 - zero[q2];
4816
+ const cost = (W_ZERO * fZ + W_GRADIENT * fG + W_DIRECTION * fD + 0.01) * step;
4817
+ const nd = d + cost;
4818
+ if (nd < dist[q2]) {
4819
+ dist[q2] = nd;
4820
+ prev[q2] = p;
4821
+ heap.push(q2, nd);
4822
+ }
4823
+ }
4824
+ }
4825
+ if (prev[goal] < 0) return [{ x: ax + 0.5, y: ay + 0.5 }, { x: bx + 0.5, y: by + 0.5 }];
4826
+ const path = [];
4827
+ for (let p = goal; p >= 0; p = prev[p]) {
4828
+ const x = p % w;
4829
+ path.push({ x: rx0 + x + 0.5, y: ry0 + (p - x) / w + 0.5 });
4830
+ }
4831
+ return path.reverse();
4832
+ }
4833
+ function simplifyPixelPath(points) {
4834
+ if (points.length < 3) return points.slice();
4835
+ const out = [points[0]];
4836
+ for (let i = 1; i < points.length - 1; i++) {
4837
+ const a = out[out.length - 1];
4838
+ const b = points[i];
4839
+ const c = points[i + 1];
4840
+ const cross = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
4841
+ if (Math.abs(cross) > 1e-9) out.push(b);
4842
+ }
4843
+ out.push(points[points.length - 1]);
4844
+ return out;
4845
+ }
4846
+ function snapPolygonToEdges(pixels, points, options = {}) {
4847
+ const map = pixels instanceof EdgeMap ? pixels : new EdgeMap(pixels, options);
4848
+ const width = options.width ?? 10;
4849
+ const snapped = points.map((p) => snapToEdge(map, p, width));
4850
+ const out = [];
4851
+ for (let i = 0; i < snapped.length; i++) {
4852
+ const a = snapped[i];
4853
+ const b = snapped[(i + 1) % snapped.length];
4854
+ const seg = liveWire(map, a, b, Math.max(2 * width, 16, Math.hypot(b.x - a.x, b.y - a.y) / 4));
4855
+ out.push(...seg.slice(0, -1));
4856
+ }
4857
+ return simplifyPixelPath(out);
4858
+ }
4859
+ function magneticAnchorSpacing(frequency = 57) {
4860
+ const f = Math.min(100, Math.max(0, frequency));
4861
+ return 8 + (100 - f);
4862
+ }
4863
+
4550
4864
  // src/image-editor/transform.ts
4551
4865
  var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
4552
4866
  var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
@@ -5473,7 +5787,7 @@ function baseFields(layer, ctx) {
5473
5787
  blend: layer.blend
5474
5788
  };
5475
5789
  if (layer.clipped) out.clipped = true;
5476
- if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
5790
+ if (layer.mask) out.mask = serializeMask2(layer.mask, `${layer.id}-mask`, ctx);
5477
5791
  if (layer.vectorMask) out.vectorMask = serializeVectorMask(layer.vectorMask);
5478
5792
  if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
5479
5793
  if (layer.effects) out.effects = serializeEffects(layer.effects);
@@ -5558,7 +5872,7 @@ function readVectorMask(r) {
5558
5872
  }
5559
5873
  return vm;
5560
5874
  }
5561
- function serializeMask(mask, hint, ctx) {
5875
+ function serializeMask2(mask, hint, ctx) {
5562
5876
  const out = { defaultValue: mask.defaultValue };
5563
5877
  if (!mask.source) {
5564
5878
  const enc = encodeSurface(mask.surface);
@@ -5757,7 +6071,7 @@ function serializeSmartFilters(stack, layerId, ctx) {
5757
6071
  enabled: stack.enabled,
5758
6072
  filters: stack.filters.map((f) => f.kind === "psd" ? { ...f, data: bytesToBase64(f.data) } : JSON.parse(JSON.stringify(f)))
5759
6073
  };
5760
- if (stack.mask) out.mask = serializeMask(stack.mask, `${layerId}-filter-mask`, ctx);
6074
+ if (stack.mask) out.mask = serializeMask2(stack.mask, `${layerId}-filter-mask`, ctx);
5761
6075
  return out;
5762
6076
  }
5763
6077
  function serializeSmartObject(layer, base, ctx) {
@@ -6105,7 +6419,7 @@ function readSmartFilters(raw, ctx, what) {
6105
6419
  const r = raw;
6106
6420
  const filters = [];
6107
6421
  const list2 = Array.isArray(r.filters) ? r.filters : [];
6108
- const plain = readFilters(list2.filter((f) => f && f.kind !== "psd")) ?? [];
6422
+ const plain2 = readFilters(list2.filter((f) => f && f.kind !== "psd")) ?? [];
6109
6423
  let k = 0;
6110
6424
  for (const f of list2) {
6111
6425
  if (!f || typeof f !== "object") continue;
@@ -6118,7 +6432,7 @@ function readSmartFilters(raw, ctx, what) {
6118
6432
  ctx.problems.push(`${what}: a Photoshop smart filter could not be read`);
6119
6433
  }
6120
6434
  } else if (f.kind === "develop" || f.kind === "blur") {
6121
- const base = plain[k++];
6435
+ const base = plain2[k++];
6122
6436
  if (base) filters.push({ ...base, blend, opacity });
6123
6437
  } else if (f.kind === "effect") {
6124
6438
  const ref = readEffectRef(f);
@@ -11332,12 +11646,12 @@ function prepareNestedLayers(layers) {
11332
11646
  const b = l.surface.bounds;
11333
11647
  if (b) {
11334
11648
  const r = { x: b.x + l.offset.x - 64, y: b.y + l.offset.y - 64, width: b.width + 128, height: b.height + 128 };
11335
- const plain = { ...l, visible: true, opacity: 1, blend: "normal" };
11336
- delete plain.mask;
11337
- delete plain.clipped;
11338
- delete plain.effects;
11339
- delete plain.fillOpacity;
11340
- const data = new LayerCompositor().composite([plain], r, { width: r.x + r.width, height: r.y + r.height });
11649
+ const plain2 = { ...l, visible: true, opacity: 1, blend: "normal" };
11650
+ delete plain2.mask;
11651
+ delete plain2.clipped;
11652
+ delete plain2.effects;
11653
+ delete plain2.fillOpacity;
11654
+ const data = new LayerCompositor().composite([plain2], r, { width: r.x + r.width, height: r.y + r.height });
11341
11655
  p.surface = surfaceFromPixels({ width: r.width, height: r.height, data }, 4, r.x - l.offset.x, r.y - l.offset.y);
11342
11656
  }
11343
11657
  }
@@ -11728,6 +12042,95 @@ function exportPsd(input) {
11728
12042
  return { bytes, psb, records: records.length, warnings };
11729
12043
  }
11730
12044
 
12045
+ // src/image-editor/selection/tool-options.ts
12046
+ var SELECTION_TOOLS = [
12047
+ { id: "wand", label: "Magic Wand" },
12048
+ { id: "rect", label: "Rectangle" },
12049
+ { id: "ellipse", label: "Ellipse" },
12050
+ { id: "row", label: "Single Row" },
12051
+ { id: "column", label: "Single Column" },
12052
+ { id: "lasso", label: "Lasso" },
12053
+ { id: "polygon", label: "Polygonal Lasso" },
12054
+ { id: "magnetic", label: "Magnetic Lasso" },
12055
+ { id: "quick", label: "Quick Selection" },
12056
+ { id: "object", label: "Object Selection" },
12057
+ { id: "range", label: "Color Range" },
12058
+ { id: "type", label: "Type Mask" },
12059
+ { id: "type-vertical", label: "Vertical Type Mask" }
12060
+ ];
12061
+ var COLOR_RANGE_PRESET_LABELS = {
12062
+ reds: "Reds",
12063
+ yellows: "Yellows",
12064
+ greens: "Greens",
12065
+ cyans: "Cyans",
12066
+ blues: "Blues",
12067
+ magentas: "Magentas",
12068
+ highlights: "Highlights",
12069
+ midtones: "Midtones",
12070
+ shadows: "Shadows",
12071
+ skinTones: "Skin Tones"
12072
+ };
12073
+ var SELECTION_MODE_LABELS = {
12074
+ replace: "New",
12075
+ add: "Add",
12076
+ subtract: "Subtract",
12077
+ intersect: "Intersect"
12078
+ };
12079
+ var DEFAULT_SELECTION_TOOL_OPTIONS = {
12080
+ tool: "wand",
12081
+ mode: "replace",
12082
+ tolerance: 32,
12083
+ contiguous: true,
12084
+ antiAlias: true,
12085
+ sampleMerged: false,
12086
+ distance: "rgb",
12087
+ feather: 0,
12088
+ fuzziness: 40,
12089
+ localized: 0,
12090
+ rangeInvert: false,
12091
+ magneticWidth: 10,
12092
+ magneticContrast: 10,
12093
+ magneticFrequency: 57,
12094
+ quickSize: 30,
12095
+ quickSampleAll: true,
12096
+ quickEnhance: false,
12097
+ objectSampleAll: true,
12098
+ objectEnhance: false,
12099
+ focusRange: 50,
12100
+ focusNoise: 0,
12101
+ focusSoftness: 4,
12102
+ typeText: "Type",
12103
+ typeSize: 72,
12104
+ typeFamily: "sans-serif"
12105
+ };
12106
+ function selectionModeFromModifiers(modifiers, fallback) {
12107
+ if (modifiers.shiftKey && modifiers.altKey) return "intersect";
12108
+ if (modifiers.shiftKey) return "add";
12109
+ if (modifiers.altKey) return "subtract";
12110
+ return fallback;
12111
+ }
12112
+ var REFINE_EDGE_CONTROLS = [
12113
+ { id: "radius", label: "Radius", min: 0, max: 64, step: 1, unit: "px" },
12114
+ { id: "smooth", label: "Smooth", min: 0, max: 100, step: 1, unit: "" },
12115
+ { id: "feather", label: "Feather", min: 0, max: 50, step: 0.5, unit: "px" },
12116
+ { id: "contrast", label: "Contrast", min: 0, max: 100, step: 1, unit: "%" },
12117
+ { id: "shiftEdge", label: "Shift Edge", min: -100, max: 100, step: 1, unit: "%" }
12118
+ ];
12119
+ var REFINE_OUTPUTS = [
12120
+ { id: "selection", label: "Selection" },
12121
+ { id: "layer-mask", label: "Layer Mask" },
12122
+ { id: "new-layer", label: "New Layer" },
12123
+ { id: "new-layer-with-mask", label: "New Layer with Layer Mask" },
12124
+ { id: "new-document", label: "New Document", needsDocuments: true },
12125
+ { id: "new-document-with-mask", label: "New Document with Layer Mask", needsDocuments: true }
12126
+ ];
12127
+ function describeSelection(mask) {
12128
+ if (!mask || !mask.bounds) return "No selection";
12129
+ const pct2 = selectionArea(mask) / (mask.width * mask.height) * 100;
12130
+ const shown = pct2 >= 10 ? Math.round(pct2) : Math.round(pct2 * 10) / 10;
12131
+ return `${mask.bounds.width} \xD7 ${mask.bounds.height} px selected (${shown}% of the image)`;
12132
+ }
12133
+
11731
12134
  // src/image-editor/effect-tools.ts
11732
12135
  var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11733
12136
  var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
@@ -12077,6 +12480,740 @@ function createLayerEffectTools(host, options) {
12077
12480
  return host.setGlobalLight ? [list2, add, update, remove, setEnabled, light] : [list2, add, update, remove, setEnabled];
12078
12481
  }
12079
12482
 
12483
+ // src/image-editor/selection/gesture.ts
12484
+ function snapTo45(from, point2) {
12485
+ const dx = point2.x - from.x;
12486
+ const dy = point2.y - from.y;
12487
+ const len = Math.hypot(dx, dy);
12488
+ if (len === 0) return point2;
12489
+ const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
12490
+ return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
12491
+ }
12492
+ var SelectionGesture = class {
12493
+ constructor(controller, getOptions) {
12494
+ this.controller = controller;
12495
+ this.getOptions = getOptions;
12496
+ }
12497
+ controller;
12498
+ getOptions;
12499
+ start = null;
12500
+ points = [];
12501
+ current = null;
12502
+ modifiers = {};
12503
+ /** Polygonal lasso corners while one is being drawn. */
12504
+ polygon = null;
12505
+ pressedAt = null;
12506
+ freehand = false;
12507
+ magnetic = null;
12508
+ /** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
12509
+ closeRadius = 8;
12510
+ /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
12511
+ error = null;
12512
+ pending = Promise.resolve();
12513
+ /** Quick Selection while dragging: the selection before the stroke, how the stroke combines, the pixels. */
12514
+ quick = null;
12515
+ livePreviews = Promise.resolve();
12516
+ get active() {
12517
+ return this.start !== null || this.polygon !== null || this.magnetic !== null;
12518
+ }
12519
+ /** Resolves once a Quick Selection stroke or an Object Selection has been applied. */
12520
+ get settled() {
12521
+ return this.pending;
12522
+ }
12523
+ /** Resolves once no live Quick Selection preview is being computed. */
12524
+ get previewed() {
12525
+ return this.livePreviews;
12526
+ }
12527
+ /** Grows the Quick Selection preview to the stroke so far: one at a time, the newest points following. */
12528
+ growPreview() {
12529
+ const q2 = this.quick;
12530
+ if (!q2) return;
12531
+ if (q2.running) {
12532
+ q2.dirty = true;
12533
+ return;
12534
+ }
12535
+ q2.running = true;
12536
+ this.livePreviews = (async () => {
12537
+ do {
12538
+ q2.dirty = false;
12539
+ const stroke = this.points.slice();
12540
+ const options = this.getOptions();
12541
+ try {
12542
+ const pixels = await q2.pixels;
12543
+ if (!pixels || this.quick !== q2) break;
12544
+ const { mask } = await this.controller.runSelectionJob(
12545
+ {
12546
+ op: "quick",
12547
+ stroke,
12548
+ size: options.quickSize ?? 30,
12549
+ grow: q2.mode === "add",
12550
+ enhanceEdge: options.quickEnhance ?? false,
12551
+ selection: q2.mode === "add" && q2.base?.bounds ? serializeMask(q2.base) : null
12552
+ },
12553
+ pixels
12554
+ );
12555
+ if (this.quick === q2) this.controller.setSelectionPreview(combineSelections(q2.base, mask, q2.mode));
12556
+ } catch {
12557
+ break;
12558
+ }
12559
+ } while (q2.dirty && this.quick === q2);
12560
+ q2.running = false;
12561
+ })();
12562
+ }
12563
+ /** The anchors of a Magnetic Lasso being drawn (empty otherwise). */
12564
+ get magneticAnchors() {
12565
+ const m = this.magnetic;
12566
+ return m ? m.anchors.map((i) => m.fixed[i]) : [];
12567
+ }
12568
+ /** The outline of the shape being drawn, or `null`. */
12569
+ get draftPath() {
12570
+ if (this.magnetic) return [...this.magnetic.fixed, ...this.magnetic.live.slice(1)];
12571
+ if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
12572
+ if (!this.start || !this.current) return null;
12573
+ const { tool } = this.getOptions();
12574
+ if (tool === "quick") return null;
12575
+ if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
12576
+ const rect = normalizeRect({
12577
+ x: this.start.x,
12578
+ y: this.start.y,
12579
+ width: this.current.x - this.start.x,
12580
+ height: this.current.y - this.start.y
12581
+ });
12582
+ if (rect.width < 1 && rect.height < 1) return null;
12583
+ if (tool === "ellipse") return ellipsePoints(rect);
12584
+ return [
12585
+ { x: rect.x, y: rect.y },
12586
+ { x: rect.x + rect.width, y: rect.y },
12587
+ { x: rect.x + rect.width, y: rect.y + rect.height },
12588
+ { x: rect.x, y: rect.y + rect.height }
12589
+ ];
12590
+ }
12591
+ /** Pointer down. Resolves once a wand click has been applied. */
12592
+ async begin(point2, modifiers = {}) {
12593
+ const options = this.getOptions();
12594
+ this.error = null;
12595
+ if (this.polygon && options.tool !== "polygon" || this.magnetic && options.tool !== "magnetic") this.cancel();
12596
+ if (options.tool === "magnetic") return this.magneticClick(point2, modifiers, options);
12597
+ if (options.tool === "polygon") {
12598
+ this.pressedAt = point2;
12599
+ this.freehand = false;
12600
+ if (!this.polygon) {
12601
+ this.modifiers = modifiers;
12602
+ this.polygon = [point2];
12603
+ this.current = point2;
12604
+ return;
12605
+ }
12606
+ const last = this.polygon[this.polygon.length - 1];
12607
+ const p = modifiers.shiftKey ? snapTo45(last, point2) : point2;
12608
+ const first = this.polygon[0];
12609
+ if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
12610
+ this.close();
12611
+ return;
12612
+ }
12613
+ if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
12614
+ this.current = p;
12615
+ if ((modifiers.detail ?? 1) >= 2) this.close();
12616
+ return;
12617
+ }
12618
+ this.modifiers = modifiers;
12619
+ if (options.tool === "row" || options.tool === "column") {
12620
+ this.start = null;
12621
+ try {
12622
+ this.controller.selectLine(options.tool, options.tool === "row" ? point2.y : point2.x, {
12623
+ mode: selectionModeFromModifiers(modifiers, options.mode),
12624
+ feather: options.feather ?? 0
12625
+ });
12626
+ } catch (err) {
12627
+ this.error = err instanceof Error ? err.message : String(err);
12628
+ }
12629
+ return;
12630
+ }
12631
+ if (options.tool === "type" || options.tool === "type-vertical") {
12632
+ this.start = null;
12633
+ try {
12634
+ this.controller.selectTextShape({
12635
+ text: options.typeText || "Type",
12636
+ x: point2.x,
12637
+ y: point2.y,
12638
+ orientation: options.tool === "type-vertical" ? "vertical" : "horizontal",
12639
+ style: { fontSize: options.typeSize ?? 72, fontFamily: options.typeFamily || "sans-serif" },
12640
+ mode: selectionModeFromModifiers(modifiers, options.mode),
12641
+ feather: options.feather ?? 0,
12642
+ antiAlias: options.antiAlias
12643
+ });
12644
+ } catch (err) {
12645
+ this.error = err instanceof Error ? err.message : String(err);
12646
+ }
12647
+ return;
12648
+ }
12649
+ if (options.tool === "wand" || options.tool === "range") {
12650
+ this.start = null;
12651
+ const common2 = {
12652
+ sampleMerged: options.sampleMerged,
12653
+ sample: options.sample,
12654
+ feather: options.feather ?? 0,
12655
+ mode: selectionModeFromModifiers(modifiers, options.mode)
12656
+ };
12657
+ try {
12658
+ if (options.tool === "range") {
12659
+ const pixels = await this.controller.getSamplePixels(common2);
12660
+ if (!pixels) throw new Error("The image pixels are not available to sample.");
12661
+ const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point2.x)));
12662
+ const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point2.y)));
12663
+ const p = (y * pixels.width + x) * 4;
12664
+ const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
12665
+ await this.controller.selectColorRange([{ color, at: point2 }], {
12666
+ ...common2,
12667
+ fuzziness: options.fuzziness ?? 40,
12668
+ localized: options.localized ? options.localized : void 0,
12669
+ invert: options.rangeInvert ?? false
12670
+ });
12671
+ return;
12672
+ }
12673
+ await this.controller.magicWand(point2, {
12674
+ ...common2,
12675
+ tolerance: options.tolerance,
12676
+ contiguous: options.contiguous,
12677
+ antiAlias: options.antiAlias,
12678
+ distance: options.distance
12679
+ });
12680
+ } catch (err) {
12681
+ this.error = err instanceof Error ? err.message : String(err);
12682
+ }
12683
+ return;
12684
+ }
12685
+ this.start = point2;
12686
+ this.current = point2;
12687
+ this.points = [point2];
12688
+ if (options.tool === "quick") {
12689
+ const picked = selectionModeFromModifiers(modifiers, options.mode);
12690
+ const base = this.controller.getSelection();
12691
+ this.quick = {
12692
+ base,
12693
+ mode: picked === "replace" && base ? "add" : picked,
12694
+ pixels: this.controller.getSamplePixels({ sample: options.quickSampleAll === false ? "active-layer" : "composite" }).catch(() => null),
12695
+ running: false,
12696
+ dirty: false
12697
+ };
12698
+ this.growPreview();
12699
+ }
12700
+ }
12701
+ /** Pointer move; returns true when the draft changed (re-render). */
12702
+ move(point2, modifiers = {}) {
12703
+ if (this.magnetic) {
12704
+ this.current = point2;
12705
+ this.followEdges(point2);
12706
+ return true;
12707
+ }
12708
+ if (this.polygon) {
12709
+ const last = this.polygon[this.polygon.length - 1];
12710
+ if (this.pressedAt && (this.freehand || Math.hypot(point2.x - this.pressedAt.x, point2.y - this.pressedAt.y) >= 3)) {
12711
+ this.freehand = true;
12712
+ if (Math.hypot(point2.x - last.x, point2.y - last.y) >= 0.5) this.polygon.push(point2);
12713
+ this.current = null;
12714
+ return true;
12715
+ }
12716
+ this.current = modifiers.shiftKey ? snapTo45(last, point2) : point2;
12717
+ return true;
12718
+ }
12719
+ if (!this.start) return false;
12720
+ this.current = point2;
12721
+ const drawn = this.getOptions().tool;
12722
+ if (drawn === "lasso" || drawn === "quick") {
12723
+ const last = this.points[this.points.length - 1];
12724
+ if (Math.hypot(point2.x - last.x, point2.y - last.y) < 0.5) return false;
12725
+ this.points.push(point2);
12726
+ if (drawn === "quick") this.growPreview();
12727
+ }
12728
+ return true;
12729
+ }
12730
+ /** Pointer up: commits the shape. Returns true when the selection changed. */
12731
+ end(point2, modifiers) {
12732
+ if (this.magnetic) return false;
12733
+ if (this.polygon) {
12734
+ if (point2 && this.freehand) this.move(point2);
12735
+ this.pressedAt = null;
12736
+ this.freehand = false;
12737
+ return false;
12738
+ }
12739
+ if (!this.start) return false;
12740
+ if (point2) this.move(point2);
12741
+ const options = this.getOptions();
12742
+ const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
12743
+ if (options.tool === "object") {
12744
+ const rect2 = normalizeRect({ x: this.start.x, y: this.start.y, width: this.current.x - this.start.x, height: this.current.y - this.start.y });
12745
+ this.start = null;
12746
+ this.current = null;
12747
+ this.points = [];
12748
+ const target = rect2.width >= 4 && rect2.height >= 4 ? { rect: rect2 } : { point: { x: rect2.x + rect2.width / 2, y: rect2.y + rect2.height / 2 } };
12749
+ this.pending = this.controller.selectObject(target, { mode, sampleAllLayers: options.objectSampleAll ?? true, enhanceEdge: options.objectEnhance ?? false }).then(
12750
+ () => void 0,
12751
+ (err) => {
12752
+ this.error = err instanceof Error ? err.message : String(err);
12753
+ }
12754
+ );
12755
+ return false;
12756
+ }
12757
+ if (options.tool === "quick") {
12758
+ const stroke = this.points;
12759
+ this.start = null;
12760
+ this.current = null;
12761
+ this.points = [];
12762
+ const strokeMode = this.quick?.mode ?? (mode === "replace" && this.controller.getSelection() ? "add" : mode);
12763
+ this.quick = null;
12764
+ this.pending = this.controller.quickSelect([stroke], {
12765
+ size: options.quickSize ?? 30,
12766
+ mode: strokeMode,
12767
+ sampleAllLayers: options.quickSampleAll ?? true,
12768
+ enhanceEdge: options.quickEnhance ?? false
12769
+ }).then(
12770
+ () => void 0,
12771
+ (err) => {
12772
+ this.error = err instanceof Error ? err.message : String(err);
12773
+ }
12774
+ ).finally(() => this.controller.setSelectionPreview(void 0));
12775
+ return false;
12776
+ }
12777
+ const path = this.draftPath;
12778
+ const rect = normalizeRect({
12779
+ x: this.start.x,
12780
+ y: this.start.y,
12781
+ width: this.current.x - this.start.x,
12782
+ height: this.current.y - this.start.y
12783
+ });
12784
+ this.start = null;
12785
+ this.current = null;
12786
+ this.points = [];
12787
+ if (!path) return mode === "replace" ? this.controller.deselect() : false;
12788
+ const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
12789
+ if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
12790
+ return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
12791
+ }
12792
+ /** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
12793
+ close() {
12794
+ let polygon2 = this.polygon;
12795
+ const m = this.magnetic;
12796
+ if (m) {
12797
+ const tail = m.edges && m.fixed.length > 1 ? liveWire(m.edges, m.fixed[m.fixed.length - 1], m.fixed[0], this.corridor(m.fixed[m.fixed.length - 1], m.fixed[0])) : [];
12798
+ polygon2 = simplifyPixelPath([...m.fixed, ...tail.slice(1, -1)]);
12799
+ }
12800
+ if (!polygon2) return false;
12801
+ const options = this.getOptions();
12802
+ this.cancel();
12803
+ if (polygon2.length < 3) return false;
12804
+ return this.controller.selectPolygon(polygon2, {
12805
+ mode: selectionModeFromModifiers(this.modifiers, options.mode),
12806
+ antiAlias: options.antiAlias,
12807
+ feather: options.feather ?? 0
12808
+ });
12809
+ }
12810
+ /**
12811
+ * Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
12812
+ * removes the last corner. Returns true when the key was used (the host prevents its default).
12813
+ */
12814
+ key(key) {
12815
+ if (!this.polygon && !this.magnetic) return false;
12816
+ if (key === "Enter") this.close();
12817
+ else if (key === "Escape") this.cancel();
12818
+ else if (key === "Backspace" || key === "Delete") {
12819
+ const m = this.magnetic;
12820
+ if (m) {
12821
+ m.anchors.pop();
12822
+ if (!m.anchors.length) this.cancel();
12823
+ else {
12824
+ m.fixed.length = m.anchors[m.anchors.length - 1] + 1;
12825
+ m.live = [];
12826
+ if (this.current) this.followEdges(this.current, false);
12827
+ }
12828
+ } else {
12829
+ this.polygon.pop();
12830
+ if (!this.polygon.length) this.cancel();
12831
+ }
12832
+ } else return false;
12833
+ return true;
12834
+ }
12835
+ /** Corridor half-width for a live-wire between two points. */
12836
+ corridor(a, b) {
12837
+ return Math.max(2 * (this.getOptions().magneticWidth ?? 10), 16, Math.hypot(b.x - a.x, b.y - a.y) / 4);
12838
+ }
12839
+ async magneticClick(point2, modifiers, options) {
12840
+ const m = this.magnetic;
12841
+ if (!m) {
12842
+ this.modifiers = modifiers;
12843
+ const draft = { fixed: [point2], anchors: [0], live: [], edges: null };
12844
+ this.magnetic = draft;
12845
+ this.current = point2;
12846
+ try {
12847
+ const pixels = await this.controller.getSamplePixels({ sampleMerged: options.sampleMerged, sample: options.sample });
12848
+ if (!pixels) throw new Error("The image pixels are not available to find edges in.");
12849
+ if (this.magnetic !== draft) return;
12850
+ draft.edges = new EdgeMap(pixels, { contrast: (options.magneticContrast ?? 10) / 100 });
12851
+ draft.fixed = [snapToEdge(draft.edges, point2, options.magneticWidth ?? 10)];
12852
+ if (this.current) this.followEdges(this.current);
12853
+ } catch (err) {
12854
+ this.error = err instanceof Error ? err.message : String(err);
12855
+ if (this.magnetic === draft) this.cancel();
12856
+ }
12857
+ return;
12858
+ }
12859
+ const first = m.fixed[0];
12860
+ if (m.anchors.length >= 2 && Math.hypot(point2.x - first.x, point2.y - first.y) <= this.closeRadius) {
12861
+ this.close();
12862
+ return;
12863
+ }
12864
+ this.current = point2;
12865
+ this.followEdges(point2);
12866
+ if (m.live.length > 1) {
12867
+ m.fixed.push(...m.live.slice(1));
12868
+ m.anchors.push(m.fixed.length - 1);
12869
+ m.live = [];
12870
+ }
12871
+ if ((modifiers.detail ?? 1) >= 2) this.close();
12872
+ }
12873
+ /** Updates the live path to `point`, fixing anchors along it every `frequency` px (with `place`). */
12874
+ followEdges(point2, place = true) {
12875
+ const m = this.magnetic;
12876
+ const last = () => m.fixed[m.fixed.length - 1];
12877
+ if (!m.edges) {
12878
+ m.live = [last(), point2];
12879
+ return;
12880
+ }
12881
+ const options = this.getOptions();
12882
+ const target = snapToEdge(m.edges, point2, options.magneticWidth ?? 10);
12883
+ const spacing = magneticAnchorSpacing(options.magneticFrequency ?? 57);
12884
+ for (let guard2 = 0; guard2 < 64; guard2++) {
12885
+ const path = liveWire(m.edges, last(), target, this.corridor(last(), target));
12886
+ let length = 0;
12887
+ let k = 0;
12888
+ for (k = 1; k < path.length; k++) {
12889
+ length += Math.hypot(path[k].x - path[k - 1].x, path[k].y - path[k - 1].y);
12890
+ if (length >= spacing) break;
12891
+ }
12892
+ if (!place || k >= path.length - 1) {
12893
+ m.live = path;
12894
+ return;
12895
+ }
12896
+ m.fixed.push(...path.slice(1, k + 1));
12897
+ m.anchors.push(m.fixed.length - 1);
12898
+ }
12899
+ m.live = [last(), target];
12900
+ }
12901
+ /** Abandons a drag (or a polygonal lasso) without changing the selection. */
12902
+ cancel() {
12903
+ if (this.quick) {
12904
+ this.quick = null;
12905
+ this.controller.setSelectionPreview(void 0);
12906
+ }
12907
+ this.start = null;
12908
+ this.current = null;
12909
+ this.points = [];
12910
+ this.polygon = null;
12911
+ this.magnetic = null;
12912
+ this.pressedAt = null;
12913
+ this.freehand = false;
12914
+ }
12915
+ };
12916
+ function handleSelectionShortcut(controller, e, onError) {
12917
+ if (!(e.ctrlKey || e.metaKey)) return false;
12918
+ const key = e.key.toLowerCase();
12919
+ const run = (fn) => {
12920
+ try {
12921
+ fn();
12922
+ } catch (err) {
12923
+ onError?.(err instanceof Error ? err.message : String(err));
12924
+ }
12925
+ return true;
12926
+ };
12927
+ if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
12928
+ if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
12929
+ if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
12930
+ if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
12931
+ if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
12932
+ return false;
12933
+ }
12934
+ function handleMaskViewKey(controller, e) {
12935
+ if (e.ctrlKey || e.metaKey || e.altKey || e.key !== "\\") return false;
12936
+ const id = controller.getState().activeLayerId;
12937
+ const layer = id ? controller.getLayer(id) : null;
12938
+ if (!layer?.mask || layer.mask.source) return false;
12939
+ const view = controller.getMaskView();
12940
+ controller.setMaskView(view?.layerId === layer.id && view.mode === "overlay" ? "off" : "overlay", layer.id);
12941
+ return true;
12942
+ }
12943
+ function handleQuickMaskKey(controller, e) {
12944
+ if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
12945
+ controller.setQuickMaskMode(!controller.quickMaskEditing);
12946
+ return true;
12947
+ }
12948
+
12949
+ // src/image-editor/layers/paint-ui.ts
12950
+ var plain = { kind: "plain" };
12951
+ var BRUSH_OPTION_CONTROLS = [
12952
+ { id: "size", label: "Size", section: "layer", min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain, unit: "px" },
12953
+ { id: "hardness", label: "Hardness", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: "%" },
12954
+ { id: "opacity", label: "Opacity", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: "%" },
12955
+ { id: "flow", label: "Flow", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: "%" },
12956
+ { id: "smoothing", label: "Smoothing", section: "layer", min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain, displayScale: 100, unit: "%" }
12957
+ ];
12958
+ var MIXER_OPTION_CONTROLS = [
12959
+ { id: "wet", label: "Wet", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%" },
12960
+ { id: "load", label: "Load", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%" },
12961
+ { id: "mix", label: "Mix", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%" }
12962
+ ];
12963
+ var MIXER_MIXING_OPTIONS = [
12964
+ { id: "rgb", label: "RGB", title: "Mix colours as RGB averages" },
12965
+ { id: "pigment", label: "Pigment", title: "Mix colours like paint: blue and yellow make green" }
12966
+ ];
12967
+ var PAINT_TOOLS = [
12968
+ { id: "move", label: "Move", shortcut: "V" },
12969
+ { id: "brush", label: "Brush", shortcut: "B" },
12970
+ { id: "mixer", label: "Mixer", shortcut: "Shift+B" },
12971
+ { id: "eraser", label: "Eraser", shortcut: "E" },
12972
+ { id: "clone", label: "Clone", shortcut: "S" },
12973
+ { id: "heal", label: "Heal", shortcut: "Shift+J" },
12974
+ { id: "spot-heal", label: "Spot Heal", shortcut: "J" },
12975
+ { id: "smudge", label: "Smudge", shortcut: "" },
12976
+ { id: "dodge", label: "Dodge", shortcut: "O" },
12977
+ { id: "burn", label: "Burn", shortcut: "Shift+O" },
12978
+ { id: "sponge", label: "Sponge", shortcut: "Shift+O" },
12979
+ { id: "blur", label: "Blur", shortcut: "" },
12980
+ { id: "sharpen", label: "Sharpen", shortcut: "" },
12981
+ { id: "history", label: "History", shortcut: "Y" }
12982
+ ];
12983
+ var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
12984
+ var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge" || tool === "blur" || tool === "sharpen" || tool === "history";
12985
+ var RETOUCH_OPTION_CONTROLS = [
12986
+ { id: "exposure", label: "Exposure", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%", tools: ["dodge", "burn"] },
12987
+ { id: "strength", label: "Strength", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: "%", tools: ["smudge", "blur", "sharpen"] },
12988
+ { id: "historySteps", label: "Steps back", section: "layer", min: 0, max: 50, step: 1, default: 0, decimals: 0, track: plain, unit: "", tools: ["history"] }
12989
+ ];
12990
+ var CLONE_TRANSFORM_CONTROLS = [
12991
+ { id: "width", label: "Source W", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain, unit: "%" },
12992
+ { id: "height", label: "Source H", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain, unit: "%" },
12993
+ { id: "angle", label: "Source angle", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: plain, unit: "\xB0" }
12994
+ ];
12995
+ function cloneTransformView(controller) {
12996
+ const t = controller.getState().paint.cloneTransform;
12997
+ return {
12998
+ width: Math.round(Math.abs(t.scaleX) * 100),
12999
+ height: Math.round(Math.abs(t.scaleY) * 100),
13000
+ angle: t.rotate,
13001
+ flipH: t.scaleX < 0,
13002
+ flipV: t.scaleY < 0,
13003
+ identity: t.scaleX === 1 && t.scaleY === 1 && t.rotate === 0
13004
+ };
13005
+ }
13006
+ function setCloneTransformValue(controller, id, value) {
13007
+ const t = controller.getState().paint.cloneTransform;
13008
+ if (id === "width") controller.setCloneTransform({ scaleX: Math.sign(t.scaleX) * (value ?? 100) / 100 });
13009
+ else if (id === "height") controller.setCloneTransform({ scaleY: Math.sign(t.scaleY) * (value ?? 100) / 100 });
13010
+ else if (id === "angle") controller.setCloneTransform({ rotate: value ?? 0 });
13011
+ else if (id === "flipH") controller.setCloneTransform({ scaleX: -t.scaleX });
13012
+ else controller.setCloneTransform({ scaleY: -t.scaleY });
13013
+ }
13014
+ var RETOUCH_CHOICES = [
13015
+ {
13016
+ id: "sample",
13017
+ label: "Sample",
13018
+ tools: ["clone", "heal", "spot-heal"],
13019
+ options: [
13020
+ { id: "current", label: "Current", title: "Sample the layer being retouched" },
13021
+ { id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
13022
+ { id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
13023
+ ]
13024
+ },
13025
+ {
13026
+ id: "spotType",
13027
+ label: "Type",
13028
+ tools: ["spot-heal"],
13029
+ options: [
13030
+ { id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
13031
+ { id: "content-aware", label: "Content-Aware", title: "Synthesise texture and edges from patches around the spot" },
13032
+ { id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
13033
+ ]
13034
+ },
13035
+ {
13036
+ id: "range",
13037
+ label: "Range",
13038
+ tools: ["dodge", "burn"],
13039
+ options: [
13040
+ { id: "shadows", label: "Shadows", title: "Change the dark tones most" },
13041
+ { id: "midtones", label: "Midtones", title: "Change the middle tones most" },
13042
+ { id: "highlights", label: "Highlights", title: "Change the light tones most" }
13043
+ ]
13044
+ },
13045
+ {
13046
+ id: "spongeMode",
13047
+ label: "Mode",
13048
+ tools: ["sponge"],
13049
+ options: [
13050
+ { id: "desaturate", label: "Desaturate", title: "Take colour out" },
13051
+ { id: "saturate", label: "Saturate", title: "Add colour" }
13052
+ ]
13053
+ }
13054
+ ];
13055
+ var RETOUCH_TOGGLES = [
13056
+ { id: "aligned", label: "Aligned", tools: ["clone", "heal"] },
13057
+ { id: "protectTones", label: "Protect tones", tools: ["dodge", "burn"] },
13058
+ { id: "vibrance", label: "Vibrance", tools: ["sponge"] }
13059
+ ];
13060
+ function setRetouchChoice(controller, id, value) {
13061
+ controller.setPaintOptions({ retouch: { [id]: value } });
13062
+ }
13063
+ function cloneSourceLabel(controller) {
13064
+ const cs = controller.getState().paint.cloneSource;
13065
+ return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ""}` : "Alt-click to set the source";
13066
+ }
13067
+ function rgbUnitToHex(c) {
13068
+ return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
13069
+ }
13070
+ function sizeStep(size) {
13071
+ return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
13072
+ }
13073
+ var isTyping = (t) => {
13074
+ const el = t;
13075
+ return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
13076
+ };
13077
+ function handlePaintShortcut(controller, e) {
13078
+ if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
13079
+ const brush = controller.getState().paint.brush;
13080
+ const set = (b) => controller.setPaintOptions({ brush: b });
13081
+ const key = e.key;
13082
+ if (handleMaskViewKey(controller, e)) return true;
13083
+ switch (key.toLowerCase()) {
13084
+ case "q":
13085
+ controller.setQuickMaskMode(!controller.quickMaskEditing);
13086
+ return true;
13087
+ case "b":
13088
+ controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
13089
+ return true;
13090
+ case "e":
13091
+ controller.setTool("eraser");
13092
+ return true;
13093
+ case "v":
13094
+ controller.setTool("move");
13095
+ return true;
13096
+ case "s":
13097
+ controller.setTool("clone");
13098
+ return true;
13099
+ case "j":
13100
+ controller.setTool(e.shiftKey || key === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
13101
+ return true;
13102
+ case "o": {
13103
+ const cur = controller.getState().activeTool;
13104
+ const next = e.shiftKey || key === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
13105
+ controller.setTool(next);
13106
+ return true;
13107
+ }
13108
+ case "y":
13109
+ controller.setTool("history");
13110
+ return true;
13111
+ case "d":
13112
+ controller.resetPaintColors();
13113
+ return true;
13114
+ case "x":
13115
+ controller.swapPaintColors();
13116
+ return true;
13117
+ }
13118
+ if (key === "[" || key === "{") {
13119
+ if (e.shiftKey || key === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
13120
+ else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
13121
+ return true;
13122
+ }
13123
+ if (key === "]" || key === "}") {
13124
+ if (e.shiftKey || key === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
13125
+ else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
13126
+ return true;
13127
+ }
13128
+ if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {
13129
+ set({ opacity: key === "0" ? 1 : Number(key) / 10 });
13130
+ return true;
13131
+ }
13132
+ return false;
13133
+ }
13134
+ var PaintGesture = class {
13135
+ constructor(controller) {
13136
+ this.controller = controller;
13137
+ }
13138
+ controller;
13139
+ stroke = null;
13140
+ move = null;
13141
+ /** The last refusal (e.g. "layer is locked"), for the editor to show. */
13142
+ error = null;
13143
+ get active() {
13144
+ return Boolean(this.stroke || this.move);
13145
+ }
13146
+ down(sample) {
13147
+ this.error = null;
13148
+ const tool = this.controller.getState().activeTool;
13149
+ try {
13150
+ if (tool === "move") {
13151
+ const layer = this.controller.getActiveLayer();
13152
+ if (!layer) throw new Error("Select a layer to move in the Layers panel.");
13153
+ if (layer.type !== "pixel" && layer.type !== "shape" && layer.type !== "smart") throw new Error(`Only pixel, shape and smart object layers move; "${layer.name}" is a ${layer.type} layer.`);
13154
+ if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer "${layer.name}" has its position locked.`);
13155
+ const origin = layer.type === "pixel" ? { ...layer.offset } : layer.type === "smart" ? { ...layer.placement.corners[0] } : (() => {
13156
+ const b = shapeGeometryBounds(layer);
13157
+ return b ? { x: b.x, y: b.y } : { x: 0, y: 0 };
13158
+ })();
13159
+ this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin, moved: false };
13160
+ return null;
13161
+ }
13162
+ if ((tool === "clone" || tool === "heal") && sample.altKey) {
13163
+ this.controller.setCloneSource({ x: sample.x, y: sample.y });
13164
+ return null;
13165
+ }
13166
+ if (isPaintTool(tool)) {
13167
+ this.stroke = this.controller.beginStroke({ tool });
13168
+ this.stroke.push([{ x: sample.x, y: sample.y, pressure: sample.pressure }]);
13169
+ return null;
13170
+ }
13171
+ } catch (err) {
13172
+ this.error = userErrorMessage(err);
13173
+ this.stroke = null;
13174
+ this.move = null;
13175
+ }
13176
+ return this.error;
13177
+ }
13178
+ /** Feeds pointer samples (all coalesced events of one move, in order). */
13179
+ moveTo(samples) {
13180
+ if (!samples.length) return;
13181
+ if (this.stroke) {
13182
+ this.stroke.push(samples);
13183
+ return;
13184
+ }
13185
+ const m = this.move;
13186
+ if (m) {
13187
+ const last = samples[samples.length - 1];
13188
+ const x = m.origin.x + Math.round(last.x - m.start.x);
13189
+ const y = m.origin.y + Math.round(last.y - m.start.y);
13190
+ try {
13191
+ this.controller.offsetLayer(m.id, { x, y }, !m.moved);
13192
+ m.moved = true;
13193
+ } catch (err) {
13194
+ this.error = userErrorMessage(err);
13195
+ this.move = null;
13196
+ }
13197
+ }
13198
+ }
13199
+ up() {
13200
+ if (this.stroke) {
13201
+ try {
13202
+ this.stroke.end();
13203
+ } catch (err) {
13204
+ this.error = userErrorMessage(err);
13205
+ }
13206
+ }
13207
+ this.stroke = null;
13208
+ this.move = null;
13209
+ }
13210
+ cancel() {
13211
+ this.stroke?.cancel();
13212
+ this.stroke = null;
13213
+ this.move = null;
13214
+ }
13215
+ };
13216
+
12080
13217
  export {
12081
13218
  LEGACY_PROCESS_VERSION,
12082
13219
  LATEST_PROCESS_VERSION,
@@ -12334,6 +13471,12 @@ export {
12334
13471
  developTarget,
12335
13472
  legacyView,
12336
13473
  isLegacyShape,
13474
+ EdgeMap,
13475
+ snapToEdge,
13476
+ liveWire,
13477
+ simplifyPixelPath,
13478
+ snapPolygonToEdges,
13479
+ magneticAnchorSpacing,
12337
13480
  FREE_TRANSFORM_MODES,
12338
13481
  TRANSFORM_ANCHORS,
12339
13482
  ANCHOR_FRACTION,
@@ -12439,7 +13582,37 @@ export {
12439
13582
  importPsd,
12440
13583
  smartContentsFile,
12441
13584
  exportPsd,
13585
+ SELECTION_TOOLS,
13586
+ COLOR_RANGE_PRESET_LABELS,
13587
+ SELECTION_MODE_LABELS,
13588
+ DEFAULT_SELECTION_TOOL_OPTIONS,
13589
+ selectionModeFromModifiers,
13590
+ REFINE_EDGE_CONTROLS,
13591
+ REFINE_OUTPUTS,
13592
+ describeSelection,
12442
13593
  describeLayerEffects,
12443
13594
  createImageEffectTools,
12444
- createLayerEffectTools
13595
+ createLayerEffectTools,
13596
+ snapTo45,
13597
+ SelectionGesture,
13598
+ handleSelectionShortcut,
13599
+ handleMaskViewKey,
13600
+ handleQuickMaskKey,
13601
+ BRUSH_OPTION_CONTROLS,
13602
+ MIXER_OPTION_CONTROLS,
13603
+ MIXER_MIXING_OPTIONS,
13604
+ PAINT_TOOLS,
13605
+ isPaintTool,
13606
+ isRetouchPaintTool,
13607
+ RETOUCH_OPTION_CONTROLS,
13608
+ CLONE_TRANSFORM_CONTROLS,
13609
+ cloneTransformView,
13610
+ setCloneTransformValue,
13611
+ RETOUCH_CHOICES,
13612
+ RETOUCH_TOGGLES,
13613
+ setRetouchChoice,
13614
+ cloneSourceLabel,
13615
+ rgbUnitToHex,
13616
+ handlePaintShortcut,
13617
+ PaintGesture
12445
13618
  };