@pixerate/editor 0.96.0 → 0.98.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 (52) hide show
  1. package/THIRD_PARTY_NOTICES.md +4 -2
  2. package/dist/animation-editor/index.cjs +55 -53
  3. package/dist/animation-editor/index.cjs.map +1 -1
  4. package/dist/animation-editor/index.d.cts +3 -2
  5. package/dist/animation-editor/index.d.ts +3 -2
  6. package/dist/animation-editor/index.js +11 -9
  7. package/dist/animation-editor/index.js.map +1 -1
  8. package/dist/chunk-DWGZUSES.cjs +694 -0
  9. package/dist/chunk-DWGZUSES.cjs.map +1 -0
  10. package/dist/chunk-GX332W2C.js +181 -0
  11. package/dist/chunk-GX332W2C.js.map +1 -0
  12. package/dist/{chunk-7MD7C6XQ.cjs → chunk-JF5PXN7W.cjs} +96 -744
  13. package/dist/chunk-JF5PXN7W.cjs.map +1 -0
  14. package/dist/chunk-KKREERP2.js +694 -0
  15. package/dist/chunk-KKREERP2.js.map +1 -0
  16. package/dist/chunk-MUUXNRU4.cjs +181 -0
  17. package/dist/chunk-MUUXNRU4.cjs.map +1 -0
  18. package/dist/{chunk-KFTQ2PKP.js → chunk-WIR377F6.js} +1001 -61
  19. package/dist/chunk-WIR377F6.js.map +1 -0
  20. package/dist/{chunk-T422YCHR.cjs → chunk-WT6ZAWJP.cjs} +1023 -83
  21. package/dist/chunk-WT6ZAWJP.cjs.map +1 -0
  22. package/dist/{chunk-4JLB2USO.js → chunk-YGZNGDIT.js} +45 -693
  23. package/dist/chunk-YGZNGDIT.js.map +1 -0
  24. package/dist/image-editor/index.cjs +1519 -360
  25. package/dist/image-editor/index.cjs.map +1 -1
  26. package/dist/image-editor/index.d.cts +378 -5
  27. package/dist/image-editor/index.d.ts +378 -5
  28. package/dist/image-editor/index.js +1244 -85
  29. package/dist/image-editor/index.js.map +1 -1
  30. package/dist/raster/index.cjs +25 -2
  31. package/dist/raster/index.cjs.map +1 -1
  32. package/dist/raster/index.d.cts +143 -4
  33. package/dist/raster/index.d.ts +143 -4
  34. package/dist/raster/index.js +25 -2
  35. package/dist/{svg-ymShkNtu.d.cts → svg-C9r284x8.d.cts} +6 -9
  36. package/dist/{svg-Dh17jCyA.d.ts → svg-MVKBqwD4.d.ts} +6 -9
  37. package/dist/types-CwMPoRMU.d.cts +40 -0
  38. package/dist/types-CwMPoRMU.d.ts +40 -0
  39. package/dist/vector/index.cjs +490 -554
  40. package/dist/vector/index.cjs.map +1 -1
  41. package/dist/vector/index.d.cts +154 -292
  42. package/dist/vector/index.d.ts +154 -292
  43. package/dist/vector/index.js +91 -155
  44. package/dist/vector/index.js.map +1 -1
  45. package/dist/warp-B8rAguzs.d.ts +165 -0
  46. package/dist/warp-nYs2DPrx.d.cts +165 -0
  47. package/package.json +1 -1
  48. package/skills/pixerate-image-editor/SKILL.md +49 -3
  49. package/dist/chunk-4JLB2USO.js.map +0 -1
  50. package/dist/chunk-7MD7C6XQ.cjs.map +0 -1
  51. package/dist/chunk-KFTQ2PKP.js.map +0 -1
  52. package/dist/chunk-T422YCHR.cjs.map +0 -1
@@ -7,12 +7,15 @@ import {
7
7
  import {
8
8
  DEFAULT_GLOBAL_LIGHT,
9
9
  DIRECT_GAUSSIAN_MAX_SIGMA,
10
+ INTERPOLATIONS,
10
11
  LAYER_EFFECT_KINDS,
11
12
  LayerEffectError,
12
13
  MAX_EFFECT_REACH,
14
+ MAX_TRANSFORM_PIXELS,
13
15
  PatternPlacement,
14
16
  PatternTile,
15
17
  RasterCompositor,
18
+ TransformTooLargeError,
16
19
  ZlibError,
17
20
  adler32,
18
21
  base64ToBytes,
@@ -21,6 +24,7 @@ import {
21
24
  builtinPatternIds,
22
25
  bytesToBase64,
23
26
  changedTiles,
27
+ compositeSurfaceOver,
24
28
  curveLut,
25
29
  decodePng,
26
30
  defaultEffectGradient,
@@ -41,9 +45,11 @@ import {
41
45
  inflate,
42
46
  inflateRaw,
43
47
  insertLayer,
48
+ isInterpolation,
44
49
  isLayerEffectKind,
45
50
  isLayerLocked,
46
51
  isPng,
52
+ keysWeight,
47
53
  layerEffectFields,
48
54
  layerEffectLabel,
49
55
  mapLayer,
@@ -53,12 +59,27 @@ import {
53
59
  parseDataUrl,
54
60
  parseEffectColor,
55
61
  pngDataUrl,
62
+ reduceSurface,
56
63
  renderLayerEffects,
57
64
  scaleLayerEffect,
65
+ transformSurface,
66
+ transformedBounds,
58
67
  validateEffectGradient,
59
68
  validateLayerEffect,
60
- validateLayerEffects
61
- } from "../chunk-KFTQ2PKP.js";
69
+ validateLayerEffects,
70
+ warpSurface
71
+ } from "../chunk-WIR377F6.js";
72
+ import {
73
+ affineFromHomography,
74
+ applyHomography,
75
+ boxQuad,
76
+ homographyFromAffine,
77
+ homographyFromPoints,
78
+ homographyKeepsQuad,
79
+ invertHomography,
80
+ multiplyHomographies,
81
+ rectToQuad
82
+ } from "../chunk-GX332W2C.js";
62
83
  import {
63
84
  CoverageMap,
64
85
  DEFAULT_BRUSH,
@@ -136,6 +157,28 @@ import {
136
157
  uniformTile,
137
158
  writeRegion
138
159
  } from "../chunk-HEW6KDNI.js";
160
+ import {
161
+ WARP_STYLES,
162
+ WARP_STYLE_LABELS,
163
+ applyMatrix,
164
+ dragWarpMeshPoint,
165
+ evalWarpMesh,
166
+ identityWarpMesh,
167
+ invertMatrix,
168
+ isValidWarpMesh,
169
+ matrixAbout,
170
+ multiplyMatrices,
171
+ pullWarpMesh,
172
+ resizeWarpMesh,
173
+ rotateMatrix,
174
+ translateMatrix,
175
+ warpMeshColumns,
176
+ warpMeshEditPoints,
177
+ warpMeshGridCurves,
178
+ warpMeshParamAt,
179
+ warpMeshRows,
180
+ warpPresetMesh
181
+ } from "../chunk-KKREERP2.js";
139
182
 
140
183
  // src/image-editor/color.ts
141
184
  var LEGACY_PROCESS_VERSION = 1;
@@ -557,20 +600,20 @@ function hitTestCrop(canvasPt, state, vpWidth, vpHeight, handleThreshold = 14, c
557
600
  }
558
601
  function hitTestCropRect(canvasPt, rect, handleThreshold) {
559
602
  const { x, y, width: w, height: h } = rect;
560
- const px = canvasPt.x;
603
+ const px2 = canvasPt.x;
561
604
  const py = canvasPt.y;
562
605
  const dist = (x1, y1, x2, y2) => Math.hypot(x1 - x2, y1 - y2);
563
- if (dist(px, py, x, y) <= handleThreshold) return "nw";
564
- if (dist(px, py, x + w, y) <= handleThreshold) return "ne";
565
- if (dist(px, py, x + w, y + h) <= handleThreshold) return "se";
566
- if (dist(px, py, x, y + h) <= handleThreshold) return "sw";
567
- const nearX = px >= x - handleThreshold && px <= x + w + handleThreshold;
606
+ if (dist(px2, py, x, y) <= handleThreshold) return "nw";
607
+ if (dist(px2, py, x + w, y) <= handleThreshold) return "ne";
608
+ if (dist(px2, py, x + w, y + h) <= handleThreshold) return "se";
609
+ if (dist(px2, py, x, y + h) <= handleThreshold) return "sw";
610
+ const nearX = px2 >= x - handleThreshold && px2 <= x + w + handleThreshold;
568
611
  const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;
569
612
  if (nearX && Math.abs(py - y) <= handleThreshold) return "n";
570
613
  if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return "s";
571
- if (nearY && Math.abs(px - x) <= handleThreshold) return "w";
572
- if (nearY && Math.abs(px - (x + w)) <= handleThreshold) return "e";
573
- if (px >= x && px <= x + w && py >= y && py <= y + h) {
614
+ if (nearY && Math.abs(px2 - x) <= handleThreshold) return "w";
615
+ if (nearY && Math.abs(px2 - (x + w)) <= handleThreshold) return "e";
616
+ if (px2 >= x && px2 <= x + w && py >= y && py <= y + h) {
574
617
  return "inside";
575
618
  }
576
619
  return null;
@@ -1598,13 +1641,13 @@ function hslToRgb(h, s, l) {
1598
1641
  };
1599
1642
  return [f(h + 1 / 3), f(h), f(h - 1 / 3)];
1600
1643
  }
1601
- function hueRangeWeight(range, deg) {
1644
+ function hueRangeWeight(range, deg2) {
1602
1645
  const [a, b0, c0, d0] = range.bounds;
1603
1646
  const rel = (x) => Number.isFinite(x) && Number.isFinite(a) ? remEuclid(x - a, 360) : 0;
1604
1647
  const b = rel(b0);
1605
1648
  const c = Math.max(rel(c0), b);
1606
1649
  const d = Math.max(rel(d0), c);
1607
- const h = rel(deg);
1650
+ const h = rel(deg2);
1608
1651
  if (h <= b) return b <= 0 ? 1 : h / b;
1609
1652
  if (h <= c) return 1;
1610
1653
  if (h <= d) return d <= c ? 1 : (d - h) / (d - c);
@@ -1616,9 +1659,9 @@ function hueRangeTables(ranges) {
1616
1659
  const t = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];
1617
1660
  const active = ranges.filter((r) => !isNeutralRange(r));
1618
1661
  for (let k = 0; k < n; k++) {
1619
- const deg = k / (n - 1) * 360;
1662
+ const deg2 = k / (n - 1) * 360;
1620
1663
  for (const r of active) {
1621
- const w = hueRangeWeight(r, deg);
1664
+ const w = hueRangeWeight(r, deg2);
1622
1665
  t[0][k] += w * r.hue;
1623
1666
  t[1][k] += w * r.saturation / 100;
1624
1667
  t[2][k] += w * r.lightness / 100;
@@ -2181,8 +2224,8 @@ function applyFastBoxBlur(imageData, radius) {
2181
2224
  let r = 0, g = 0, b = 0, a = 0;
2182
2225
  let count = 0;
2183
2226
  for (let k = -radius; k <= radius; k++) {
2184
- const px = Math.min(w - 1, Math.max(0, x + k));
2185
- const idx = (y * w + px) * 4;
2227
+ const px2 = Math.min(w - 1, Math.max(0, x + k));
2228
+ const idx = (y * w + px2) * 4;
2186
2229
  r += src[idx];
2187
2230
  g += src[idx + 1];
2188
2231
  b += src[idx + 2];
@@ -3137,13 +3180,13 @@ var ImageEditorRenderer = class {
3137
3180
  ctx.save();
3138
3181
  try {
3139
3182
  applyImageProjectionToContext(ctx, projection);
3140
- const px = 1 / (projection.scale || 1);
3183
+ const px2 = 1 / (projection.scale || 1);
3141
3184
  ctx.beginPath();
3142
- ctx.arc(b.x, b.y, Math.max(px, b.diameter / 2), 0, Math.PI * 2);
3143
- ctx.lineWidth = 2 * px;
3185
+ ctx.arc(b.x, b.y, Math.max(px2, b.diameter / 2), 0, Math.PI * 2);
3186
+ ctx.lineWidth = 2 * px2;
3144
3187
  ctx.strokeStyle = "rgba(0,0,0,0.6)";
3145
3188
  ctx.stroke();
3146
- ctx.lineWidth = px;
3189
+ ctx.lineWidth = px2;
3147
3190
  ctx.strokeStyle = "#ffffff";
3148
3191
  ctx.stroke();
3149
3192
  } catch {
@@ -3426,32 +3469,32 @@ var ImageEditorRenderer = class {
3426
3469
  this.outlineCache.set(selection, segs);
3427
3470
  }
3428
3471
  if (!segs.length) return;
3429
- const px = 1 / (scale || 1);
3472
+ const px2 = 1 / (scale || 1);
3430
3473
  ctx.beginPath();
3431
3474
  for (let i = 0; i < segs.length; i += 4) {
3432
3475
  ctx.moveTo(segs[i], segs[i + 1]);
3433
3476
  ctx.lineTo(segs[i + 2], segs[i + 3]);
3434
3477
  }
3435
- ctx.lineWidth = px;
3478
+ ctx.lineWidth = px2;
3436
3479
  ctx.strokeStyle = "#ffffff";
3437
3480
  ctx.stroke();
3438
3481
  if (typeof ctx.setLineDash === "function") {
3439
- ctx.setLineDash([4 * px, 4 * px]);
3440
- ctx.lineDashOffset = -offset * px;
3482
+ ctx.setLineDash([4 * px2, 4 * px2]);
3483
+ ctx.lineDashOffset = -offset * px2;
3441
3484
  }
3442
3485
  ctx.strokeStyle = "#000000";
3443
3486
  ctx.stroke();
3444
3487
  if (typeof ctx.setLineDash === "function") ctx.setLineDash([]);
3445
3488
  }
3446
3489
  drawDraftPath(ctx, points, scale) {
3447
- const px = 1 / (scale || 1);
3490
+ const px2 = 1 / (scale || 1);
3448
3491
  ctx.beginPath();
3449
3492
  points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
3450
3493
  ctx.closePath();
3451
- ctx.lineWidth = px;
3494
+ ctx.lineWidth = px2;
3452
3495
  ctx.strokeStyle = "#ffffff";
3453
3496
  ctx.stroke();
3454
- if (typeof ctx.setLineDash === "function") ctx.setLineDash([4 * px, 4 * px]);
3497
+ if (typeof ctx.setLineDash === "function") ctx.setLineDash([4 * px2, 4 * px2]);
3455
3498
  ctx.strokeStyle = "#000000";
3456
3499
  ctx.stroke();
3457
3500
  if (typeof ctx.setLineDash === "function") ctx.setLineDash([]);
@@ -4297,8 +4340,8 @@ var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
4297
4340
  var smoothSigma = (p) => p.smooth / 100 * 4;
4298
4341
  var featherSigma = (p) => p.feather / 2;
4299
4342
  function shiftAmount(p) {
4300
- const px = p.shiftEdge / 100 * Math.max(p.radius, 4);
4301
- return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;
4343
+ const px2 = p.shiftEdge / 100 * Math.max(p.radius, 4);
4344
+ return Number.isFinite(px2) ? Math.min(64, Math.max(-64, px2)) : 0;
4302
4345
  }
4303
4346
  function refineEdgeHalo(params = {}) {
4304
4347
  const p = normalizeRefineEdgeParams(params);
@@ -4584,6 +4627,7 @@ function serializeMask(mask, hint, ctx) {
4584
4627
  if (mask.density !== void 0 && mask.density !== 1) out.density = mask.density;
4585
4628
  if (mask.feather) out.feather = mask.feather;
4586
4629
  if (mask.enabled === false) out.enabled = false;
4630
+ if (mask.linked === false) out.linked = false;
4587
4631
  return out;
4588
4632
  }
4589
4633
  function serializeLayer(layer, ctx) {
@@ -4702,8 +4746,8 @@ function readSurface(ref, x, y, channels, defaultValue, ctx, what) {
4702
4746
  return emptySurface(channels, defaultValue);
4703
4747
  }
4704
4748
  try {
4705
- const px = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);
4706
- return surfaceFromPixels(px, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);
4749
+ const px2 = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);
4750
+ return surfaceFromPixels(px2, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);
4707
4751
  } catch (err) {
4708
4752
  ctx.problems.push(`${what}: asset "${ref}" could not be decoded (${err.message})`);
4709
4753
  return emptySurface(channels, defaultValue);
@@ -4720,6 +4764,7 @@ function readMask(raw, ctx, what) {
4720
4764
  if (typeof r.density === "number" && r.density !== 1) mask.density = num(r.density, 0, 1, 1);
4721
4765
  if (typeof r.feather === "number" && r.feather > 0) mask.feather = num(r.feather, 0, 1e3, 0);
4722
4766
  if (r.enabled === false) mask.enabled = false;
4767
+ if (r.linked === false) mask.linked = false;
4723
4768
  if (r.source && typeof r.source === "object") {
4724
4769
  const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...r.source };
4725
4770
  mask.source = { ...s, depthRange: [...s.depthRange] };
@@ -4989,7 +5034,12 @@ function offsetLayer(layers, id, offset) {
4989
5034
  requireUnlocked(loc.layer, "position", "move it");
4990
5035
  if (!Number.isFinite(offset.x) || !Number.isFinite(offset.y)) fail("x and y must be numbers.");
4991
5036
  const layer = loc.layer;
4992
- return mapLayer(layers, id, () => ({ ...layer, offset: { x: Math.round(offset.x), y: Math.round(offset.y) } }));
5037
+ const next = { ...layer, offset: { x: Math.round(offset.x), y: Math.round(offset.y) } };
5038
+ const mask = layer.mask;
5039
+ if (mask && mask.linked === false && !mask.source) {
5040
+ next.mask = { ...mask, surface: translateSurface(mask.surface, layer.offset.x - next.offset.x, layer.offset.y - next.offset.y) };
5041
+ }
5042
+ return mapLayer(layers, id, () => next);
4993
5043
  }
4994
5044
  function setLayerMask(layers, id, mask) {
4995
5045
  const loc = requireLayer(layers, id);
@@ -6175,6 +6225,7 @@ var CHANNEL_USER_MASK = -2;
6175
6225
  var CHANNEL_REAL_USER_MASK = -3;
6176
6226
  var LAYER_FLAG_TRANSPARENCY_PROTECTED = 1;
6177
6227
  var LAYER_FLAG_HIDDEN = 2;
6228
+ var MASK_FLAG_UNLINKED = 1;
6178
6229
  var MASK_FLAG_DISABLED = 2;
6179
6230
  var MASK_FLAG_PARAMETERS = 16;
6180
6231
  var SECTION_OTHER = 0;
@@ -7604,6 +7655,7 @@ function recordMask(ctx, rec, name) {
7604
7655
  if (m.userDensity !== void 0 && m.userDensity < 255) mask.density = percent(m.userDensity);
7605
7656
  if (m.userFeather !== void 0 && m.userFeather > 0) mask.feather = Math.min(1e3, m.userFeather);
7606
7657
  if (flags & MASK_FLAG_DISABLED) mask.enabled = false;
7658
+ if (flags & MASK_FLAG_UNLINKED) mask.linked = false;
7607
7659
  return mask;
7608
7660
  }
7609
7661
  function baseProps(ctx, rec, name, notes) {
@@ -7890,7 +7942,8 @@ function maskChannel(mask, ox, oy, psb) {
7890
7942
  if (mask.source) throw new PsdError("A parametric mask must be baked before export.");
7891
7943
  const bounds = contentBounds(mask.surface);
7892
7944
  const def = mask.defaultValue;
7893
- const m = { rect: EMPTY_RECT, defaultColor: def, flags: mask.enabled === false ? MASK_FLAG_DISABLED : 0 };
7945
+ const flags = (mask.enabled === false ? MASK_FLAG_DISABLED : 0) | (mask.linked === false ? MASK_FLAG_UNLINKED : 0);
7946
+ const m = { rect: EMPTY_RECT, defaultColor: def, flags };
7894
7947
  if ((mask.density ?? 1) < 1) m.userDensity = Math.round(Math.max(0, mask.density ?? 1) * 255);
7895
7948
  if ((mask.feather ?? 0) > 0) m.userFeather = mask.feather;
7896
7949
  if (!bounds) return { channel: emptyChannel(CHANNEL_USER_MASK), mask: m };
@@ -8087,6 +8140,499 @@ function exportPsd(input) {
8087
8140
  return { bytes, psb, records: records.length, warnings };
8088
8141
  }
8089
8142
 
8143
+ // src/image-editor/transform.ts
8144
+ var FREE_TRANSFORM_MODES = ["scale", "rotate", "skew", "distort", "perspective", "warp"];
8145
+ var TRANSFORM_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
8146
+ var ANCHOR_FRACTION = {
8147
+ "top-left": [0, 0],
8148
+ top: [0.5, 0],
8149
+ "top-right": [1, 0],
8150
+ left: [0, 0.5],
8151
+ center: [0.5, 0.5],
8152
+ right: [1, 0.5],
8153
+ "bottom-left": [0, 1],
8154
+ bottom: [0.5, 1],
8155
+ "bottom-right": [1, 1]
8156
+ };
8157
+ function isTransformAnchor(value) {
8158
+ return typeof value === "string" && value in ANCHOR_FRACTION;
8159
+ }
8160
+ function identityTransform(box) {
8161
+ return { box: { ...box }, corners: boxQuad(box) };
8162
+ }
8163
+ function cornersFromMatrix(box, m) {
8164
+ const q2 = boxQuad(box);
8165
+ return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
8166
+ }
8167
+ function matrixFromCorners(box, corners, eps = 1e-6) {
8168
+ const [tl, tr, br, bl] = corners;
8169
+ const size = 1 + Math.max(box.width, box.height);
8170
+ if (Math.abs(tl.x + br.x - tr.x - bl.x) > eps * size || Math.abs(tl.y + br.y - tr.y - bl.y) > eps * size) return null;
8171
+ const h = rectToQuad(box, corners);
8172
+ return h ? affineFromHomography(h, 1e-9) : null;
8173
+ }
8174
+ function transformHomography(geometry) {
8175
+ const h = rectToQuad(geometry.box, geometry.corners);
8176
+ if (!h) return null;
8177
+ return homographyKeepsQuad(h, boxQuad(geometry.box)) ? h : null;
8178
+ }
8179
+ function hasWarp(geometry) {
8180
+ const w = geometry.warp;
8181
+ if (!w) return false;
8182
+ const id = identityWarpMesh(geometry.box, w.us.length - 1, w.vs.length - 1);
8183
+ if (id.points.length !== w.points.length || id.us.some((u, i) => Math.abs(u - w.us[i]) > 1e-12) || id.vs.some((v, i) => Math.abs(v - w.vs[i]) > 1e-12)) return true;
8184
+ return w.points.some((p, i) => Math.abs(p.x - id.points[i].x) > 1e-6 || Math.abs(p.y - id.points[i].y) > 1e-6);
8185
+ }
8186
+ function isIdentityTransform(geometry) {
8187
+ if (hasWarp(geometry)) return false;
8188
+ const q2 = boxQuad(geometry.box);
8189
+ return geometry.corners.every((c, i) => Math.abs(c.x - q2[i].x) < 1e-6 && Math.abs(c.y - q2[i].y) < 1e-6);
8190
+ }
8191
+ function transformMap(geometry) {
8192
+ const h = transformHomography(geometry);
8193
+ if (!h) return null;
8194
+ const { box } = geometry;
8195
+ const warp = hasWarp(geometry) ? geometry.warp : null;
8196
+ if (!warp) return (p) => applyHomography(h, p);
8197
+ return (p) => applyHomography(h, evalWarpMesh(warp, (p.x - box.x) / box.width, (p.y - box.y) / box.height));
8198
+ }
8199
+ function transformedPoint(geometry, fx, fy) {
8200
+ const f = transformMap(geometry);
8201
+ const { box } = geometry;
8202
+ return f ? f({ x: box.x + fx * box.width, y: box.y + fy * box.height }) : null;
8203
+ }
8204
+ function anchorPoint(geometry, anchor) {
8205
+ const [fx, fy] = ANCHOR_FRACTION[anchor];
8206
+ const h = rectToQuad(geometry.box, geometry.corners);
8207
+ const p = { x: geometry.box.x + fx * geometry.box.width, y: geometry.box.y + fy * geometry.box.height };
8208
+ return (h && applyHomography(h, p)) ?? p;
8209
+ }
8210
+ function transformedBox(geometry) {
8211
+ const f = transformMap(geometry);
8212
+ if (!f) return null;
8213
+ const { box } = geometry;
8214
+ const pts = [];
8215
+ const n = hasWarp(geometry) ? 16 : 1;
8216
+ for (let i = 0; i <= n; i++) {
8217
+ const t = i / n;
8218
+ for (const [fx, fy] of [[t, 0], [1, t], [1 - t, 1], [0, 1 - t]]) {
8219
+ const p = f({ x: box.x + fx * box.width, y: box.y + fy * box.height });
8220
+ if (!p) return null;
8221
+ pts.push(p);
8222
+ }
8223
+ }
8224
+ const xs = pts.map((p) => p.x);
8225
+ const ys = pts.map((p) => p.y);
8226
+ const x = Math.min(...xs);
8227
+ const y = Math.min(...ys);
8228
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
8229
+ }
8230
+ function referencePoint(box, reference) {
8231
+ if (reference && typeof reference === "object") return { x: reference.x, y: reference.y };
8232
+ const [fx, fy] = ANCHOR_FRACTION[reference ?? "center"];
8233
+ return { x: box.x + fx * box.width, y: box.y + fy * box.height };
8234
+ }
8235
+ function matrixFromParams(box, params) {
8236
+ const sx = (params.scaleX ?? 100) / 100;
8237
+ const sy = (params.scaleY ?? 100) / 100;
8238
+ const tx = Math.tan((params.skewX ?? 0) * Math.PI / 180);
8239
+ const ty = Math.tan((params.skewY ?? 0) * Math.PI / 180);
8240
+ const ref = referencePoint(box, params.reference);
8241
+ const linear = multiplyMatrices(multiplyMatrices([1, ty, tx, 1, 0, 0], [sx, 0, 0, sy, 0, 0]), rotateMatrix(params.rotate ?? 0));
8242
+ return multiplyMatrices(matrixAbout(linear, ref), translateMatrix(params.dx ?? 0, params.dy ?? 0));
8243
+ }
8244
+ function describeTransform(geometry, reference = "center") {
8245
+ const m = hasWarp(geometry) ? null : matrixFromCorners(geometry.box, geometry.corners);
8246
+ const ref = referencePoint(geometry.box, reference);
8247
+ const bounds = transformedBox(geometry);
8248
+ if (!m) {
8249
+ const h = rectToQuad(geometry.box, geometry.corners);
8250
+ const at2 = (h && applyHomography(h, ref)) ?? ref;
8251
+ return { affine: false, scaleX: 100, scaleY: 100, rotate: 0, skewX: 0, x: at2.x, y: at2.y, bounds, corners: geometry.corners };
8252
+ }
8253
+ const [a, b, c, d] = m;
8254
+ const sxAbs = Math.hypot(a, b);
8255
+ const rot = Math.atan2(b, a) * 180 / Math.PI;
8256
+ const shear = sxAbs ? (a * c + b * d) / sxAbs : 0;
8257
+ const sy = sxAbs ? (a * d - b * c) / sxAbs : 0;
8258
+ const skew = sxAbs ? Math.atan(shear / sxAbs) * 180 / Math.PI : 0;
8259
+ const at = applyMatrix(m, ref);
8260
+ const clean = (v) => Math.abs(v) < 1e-9 ? 0 : v;
8261
+ return {
8262
+ affine: true,
8263
+ scaleX: clean(sxAbs * 100),
8264
+ scaleY: clean(sy * 100),
8265
+ rotate: clean(rot === -180 ? 180 : rot),
8266
+ skewX: clean(skew),
8267
+ x: at.x,
8268
+ y: at.y,
8269
+ bounds,
8270
+ corners: geometry.corners
8271
+ };
8272
+ }
8273
+ function applyParams(geometry, params) {
8274
+ const ref = params.reference && typeof params.reference === "object" ? params.reference : anchorPoint(geometry, params.reference ?? "center");
8275
+ const m = matrixFromParams({ x: ref.x, y: ref.y, width: 0, height: 0 }, { ...params, reference: ref });
8276
+ const c = geometry.corners;
8277
+ return [applyMatrix(m, c[0]), applyMatrix(m, c[1]), applyMatrix(m, c[2]), applyMatrix(m, c[3])];
8278
+ }
8279
+ function flipCorners(geometry, axis) {
8280
+ const c = anchorPoint(geometry, "center");
8281
+ const m = axis === "horizontal" ? [-1, 0, 0, 1, 2 * c.x, 0] : [1, 0, 0, -1, 0, 2 * c.y];
8282
+ const q2 = geometry.corners;
8283
+ return [applyMatrix(m, q2[0]), applyMatrix(m, q2[1]), applyMatrix(m, q2[2]), applyMatrix(m, q2[3])];
8284
+ }
8285
+ function boxPointOf(geometry, p) {
8286
+ const m = matrixFromCorners(geometry.box, geometry.corners);
8287
+ if (m) return applyMatrix(invertMatrix(m), p);
8288
+ return null;
8289
+ }
8290
+ function presetWarp(box, preset) {
8291
+ return warpPresetMesh(preset, box);
8292
+ }
8293
+ function isValidWarp(mesh) {
8294
+ return isValidWarpMesh(mesh);
8295
+ }
8296
+ function geometryHomography(geometry) {
8297
+ const m = matrixFromCorners(geometry.box, geometry.corners);
8298
+ return m ? homographyFromAffine(m) : transformHomography(geometry);
8299
+ }
8300
+
8301
+ // src/image-editor/free-transform.ts
8302
+ var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
8303
+ var fail2 = (message) => {
8304
+ throw new LayerOperationError(message);
8305
+ };
8306
+ var T = (x, y) => [1, 0, x, 0, 1, y, 0, 0, 1];
8307
+ function previewStep(scale) {
8308
+ if (!scale || !(scale > 0) || scale >= 1) return 1;
8309
+ return Math.min(8, 2 ** Math.floor(Math.log2(1 / scale)));
8310
+ }
8311
+ function toDoc(r, o) {
8312
+ return r ? { x: r.x + o.x, y: r.y + o.y, width: r.width, height: r.height } : null;
8313
+ }
8314
+ function selectionSurface(sel) {
8315
+ return surfaceFromPixels({ width: sel.width, height: sel.height, data: sel.data }, 1, 0, 0, 0);
8316
+ }
8317
+ function liftPixels(surface, sel, o) {
8318
+ const sb = sel.bounds;
8319
+ const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };
8320
+ const px2 = readRegion(surface, area);
8321
+ const kept = new Uint8ClampedArray(px2);
8322
+ for (let y = 0; y < area.height; y++) {
8323
+ for (let x = 0; x < area.width; x++) {
8324
+ const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;
8325
+ const i = (y * area.width + x) * 4;
8326
+ if (k <= 0) {
8327
+ px2[i] = px2[i + 1] = px2[i + 2] = px2[i + 3] = 0;
8328
+ continue;
8329
+ }
8330
+ px2[i + 3] = px2[i + 3] * k;
8331
+ if (!px2[i + 3]) px2[i] = px2[i + 1] = px2[i + 2] = 0;
8332
+ kept[i + 3] = kept[i + 3] * (1 - k);
8333
+ if (!kept[i + 3]) kept[i] = kept[i + 1] = kept[i + 2] = 0;
8334
+ }
8335
+ }
8336
+ return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: px2 }, 4, area.x, area.y), rest: writeRegion(surface, area, kept) };
8337
+ }
8338
+ function liftMask(mask, sel, o) {
8339
+ const sb = sel.bounds;
8340
+ const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };
8341
+ const v = readRegion(mask, area);
8342
+ const def = mask.defaultValue;
8343
+ const lifted = new Uint8ClampedArray(area.width * area.height * 4);
8344
+ const rest = new Uint8ClampedArray(v.length);
8345
+ for (let y = 0; y < area.height; y++) {
8346
+ for (let x = 0; x < area.width; x++) {
8347
+ const i = y * area.width + x;
8348
+ const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;
8349
+ const g = v[i];
8350
+ if (k > 0) {
8351
+ lifted[i * 4] = lifted[i * 4 + 1] = lifted[i * 4 + 2] = g;
8352
+ lifted[i * 4 + 3] = k * 255;
8353
+ }
8354
+ rest[i] = g * (1 - k) + def * k;
8355
+ }
8356
+ }
8357
+ return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: lifted }, 4, area.x, area.y), rest: writeRegion(mask, area, rest) };
8358
+ }
8359
+ var FreeTransformSession = class {
8360
+ constructor(host, init = {}) {
8361
+ this.host = host;
8362
+ if (init.interpolation !== void 0 && !isInterpolation(init.interpolation)) fail2(`Unknown interpolation "${String(init.interpolation)}"; use nearest, bilinear or bicubic.`);
8363
+ this.interp = init.interpolation ?? "bicubic";
8364
+ const sel = host.selection();
8365
+ const selected = Boolean(sel?.bounds);
8366
+ let content = init.content ?? "auto";
8367
+ if (!["auto", "layer", "selection", "selection-copy", "selection-outline"].includes(content)) {
8368
+ fail2(`Unknown content "${String(content)}"; use auto, layer, selection, selection-copy or selection-outline.`);
8369
+ }
8370
+ if (content === "auto") content = selected ? "selection" : "layer";
8371
+ this.content = content;
8372
+ if (content !== "layer" && !selected) fail2(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);
8373
+ const layers = host.layers();
8374
+ this.startLayers = layers;
8375
+ this.startSelection = sel;
8376
+ this.startActive = host.activeLayerId();
8377
+ this.before = host.capture();
8378
+ const moves = [];
8379
+ let box = null;
8380
+ if (content === "selection-outline") {
8381
+ this.layerId = null;
8382
+ this.parts.push({ what: "selection", offset: { x: 0, y: 0 }, moving: selectionSurface(sel), base: null, reduced: /* @__PURE__ */ new Map() });
8383
+ moves.push("selection");
8384
+ box = { ...sel.bounds };
8385
+ } else {
8386
+ const id = init.layerId ?? host.activeLayerId();
8387
+ if (!id) fail2("No layer given and none is active; pass layerId (see list_layers).");
8388
+ const layer = requireLayer(layers, id).layer;
8389
+ this.layerId = layer.id;
8390
+ const mask = layer.mask && !layer.mask.source ? layer.mask : void 0;
8391
+ const channel = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : "pixels");
8392
+ if (channel !== "pixels" && channel !== "mask") fail2(`Unknown channel "${String(channel)}"; use pixels or mask.`);
8393
+ const linked = mask?.linked !== false;
8394
+ const maskAlone = channel === "mask" && (!linked || layer.type !== "pixel");
8395
+ if (channel === "mask" && !mask) fail2(layer.mask?.source ? `Layer "${layer.name || id}" has a depth mask, which is generated, not transformed.` : `Layer "${layer.name || id}" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);
8396
+ if (!maskAlone && layer.type !== "pixel") fail2(`Layer "${layer.name || id}" is a ${layer.type} layer; Free Transform works on pixel layers, layer masks (channel mask) and selections.`);
8397
+ if (!layer.visible && content !== "selection-copy") fail2(`Layer "${layer.name || id}" is hidden; show it before you transform it.`);
8398
+ if (content !== "selection-copy") {
8399
+ requireUnlocked(layer, "position", "transform it");
8400
+ if (!maskAlone) requireUnlocked(layer, "pixels", "transform it");
8401
+ }
8402
+ const o = layer.type === "pixel" ? layer.offset : { x: 0, y: 0 };
8403
+ const pixels = layer.type === "pixel" ? host.pixelsOf(layer) : null;
8404
+ if (maskAlone) {
8405
+ if (content === "selection-copy") fail2("Layer via Copy copies pixels; transform a mask with content layer or selection.");
8406
+ if (content === "selection") {
8407
+ const { lifted, rest } = liftMask(mask.surface, sel, o);
8408
+ this.parts.push({ what: "mask", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
8409
+ box = toDoc(contentBounds(lifted), o);
8410
+ } else {
8411
+ this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
8412
+ box = toDoc(contentBounds(mask.surface), o);
8413
+ }
8414
+ moves.push("mask");
8415
+ } else if (content === "layer") {
8416
+ this.parts.push({ what: "pixels", offset: o, moving: pixels, base: null, reduced: /* @__PURE__ */ new Map() });
8417
+ moves.push("pixels");
8418
+ box = toDoc(contentBounds(pixels), o);
8419
+ if (mask && linked) {
8420
+ this.parts.push({ what: "mask", offset: o, moving: mask.surface, base: null, reduced: /* @__PURE__ */ new Map() });
8421
+ moves.push("mask");
8422
+ if (!box) box = toDoc(contentBounds(mask.surface), o);
8423
+ }
8424
+ } else if (content === "selection") {
8425
+ const { lifted, rest } = liftPixels(pixels, sel, o);
8426
+ this.parts.push({ what: "pixels", offset: o, moving: lifted, base: rest, reduced: /* @__PURE__ */ new Map() });
8427
+ moves.push("pixels");
8428
+ box = toDoc(contentBounds(lifted), o);
8429
+ if (mask && linked) {
8430
+ const m = liftMask(mask.surface, sel, o);
8431
+ this.parts.push({ what: "mask", offset: o, moving: m.lifted, base: m.rest, reduced: /* @__PURE__ */ new Map() });
8432
+ moves.push("mask");
8433
+ }
8434
+ } else {
8435
+ const { lifted } = liftPixels(pixels, sel, o);
8436
+ box = toDoc(contentBounds(lifted), o);
8437
+ if (box) {
8438
+ const copy = {
8439
+ type: "pixel",
8440
+ id: newLayerId(),
8441
+ name: `${layer.name || "Layer"} copy`,
8442
+ visible: true,
8443
+ opacity: 1,
8444
+ blend: "normal",
8445
+ surface: lifted,
8446
+ offset: { ...o }
8447
+ };
8448
+ this.copyId = copy.id;
8449
+ const loc = findLayer(layers, layer.id);
8450
+ this.startLayers = insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1);
8451
+ this.parts.push({ what: "pixels", offset: o, moving: lifted, base: null, reduced: /* @__PURE__ */ new Map() });
8452
+ moves.push("pixels");
8453
+ }
8454
+ }
8455
+ if (content !== "layer") {
8456
+ this.parts.push({ what: "selection", offset: { x: 0, y: 0 }, moving: selectionSurface(sel), base: null, reduced: /* @__PURE__ */ new Map() });
8457
+ moves.push("selection");
8458
+ }
8459
+ }
8460
+ if (!box && moves[0] === "mask") fail2("The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).");
8461
+ if (!box) fail2(content === "layer" ? "The layer has no pixels to transform." : "The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).");
8462
+ this.box = box;
8463
+ this.moves = moves;
8464
+ this.geometryValue = identityTransform(box);
8465
+ this.shown = this.startLayers;
8466
+ if (this.copyId) host.show(this.startLayers, sel, this.copyId);
8467
+ this.shown = host.layers();
8468
+ }
8469
+ host;
8470
+ content;
8471
+ /** The layer being transformed (`null` for `selection-outline`). */
8472
+ layerId;
8473
+ /** The new layer of a `selection-copy`. */
8474
+ copyId = null;
8475
+ /** The frame (document pixels). */
8476
+ box;
8477
+ /** What moves. */
8478
+ moves;
8479
+ geometryValue;
8480
+ interp;
8481
+ parts = [];
8482
+ before;
8483
+ startLayers;
8484
+ startSelection;
8485
+ startActive;
8486
+ /** The stack this session last showed: anything else means the document changed under it. */
8487
+ shown;
8488
+ closed = false;
8489
+ /** The current geometry. */
8490
+ get geometry() {
8491
+ return this.geometryValue;
8492
+ }
8493
+ get interpolation() {
8494
+ return this.interp;
8495
+ }
8496
+ get isOpen() {
8497
+ return !this.closed;
8498
+ }
8499
+ check() {
8500
+ if (this.closed) fail2("This transform has already been committed or cancelled; begin a new one.");
8501
+ if (this.host.layers() !== this.shown) {
8502
+ this.closed = true;
8503
+ fail2("The document changed while the transform was open, so it was abandoned; begin a new one.");
8504
+ }
8505
+ }
8506
+ /**
8507
+ * Changes the geometry (`corners`, `warp`) or the interpolation and, unless `preview` is `false`,
8508
+ * shows the result live (no history). Returns the geometry.
8509
+ */
8510
+ update(changes, preview = {}) {
8511
+ this.check();
8512
+ if (changes.interpolation !== void 0) {
8513
+ if (!isInterpolation(changes.interpolation)) fail2(`Unknown interpolation "${String(changes.interpolation)}"; use nearest, bilinear or bicubic.`);
8514
+ this.interp = changes.interpolation;
8515
+ }
8516
+ const next = {
8517
+ box: this.box,
8518
+ corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })),
8519
+ warp: changes.warp === void 0 ? this.geometryValue.warp ?? null : changes.warp
8520
+ };
8521
+ if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail2("corners must be four finite points: top-left, top-right, bottom-right, bottom-left.");
8522
+ if (!transformMap(next)) fail2("These corners fold the frame over itself or through the horizon; keep the quad convex.");
8523
+ this.geometryValue = next;
8524
+ if (preview !== false) this.render(preview);
8525
+ return next;
8526
+ }
8527
+ /** Resamples every part for the current geometry and shows it (or returns it, for commit). */
8528
+ resample(final, preview = {}) {
8529
+ const g = this.geometryValue;
8530
+ const identity = isIdentityTransform(g);
8531
+ const warp = hasWarp(g);
8532
+ const h = warp ? null : geometryHomography(g);
8533
+ const f = warp ? transformMap(g) : null;
8534
+ const step = final ? 1 : previewStep(preview.scale);
8535
+ const interpolation = final ? this.interp : this.interp === "nearest" ? "nearest" : "bilinear";
8536
+ let layers = this.startLayers;
8537
+ let selection = this.startSelection;
8538
+ const targetId = this.copyId ?? this.layerId;
8539
+ for (const part of this.parts) {
8540
+ const o = part.offset;
8541
+ let moved;
8542
+ if (identity) moved = part.moving;
8543
+ else {
8544
+ let src = part.moving;
8545
+ let k = 1;
8546
+ if (step > 1) {
8547
+ k = step;
8548
+ let r = part.reduced.get(k);
8549
+ if (!r) {
8550
+ r = reduceSurface(part.moving, k);
8551
+ part.reduced.set(k, r);
8552
+ }
8553
+ src = r;
8554
+ }
8555
+ const opts = { interpolation, step: step > 1 ? step : void 0 };
8556
+ if (preview.clip && !final) opts.clip = { x: Math.floor(preview.clip.x - o.x), y: Math.floor(preview.clip.y - o.y), width: Math.ceil(preview.clip.width) + 1, height: Math.ceil(preview.clip.height) + 1 };
8557
+ try {
8558
+ if (f) {
8559
+ moved = warpSurface(src, (p) => {
8560
+ const q2 = f({ x: p.x * k + o.x, y: p.y * k + o.y });
8561
+ return q2 && { x: q2.x - o.x, y: q2.y - o.y };
8562
+ }, opts);
8563
+ } else {
8564
+ const hl = multiplyHomographies(multiplyHomographies(multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], T(o.x, o.y)), h), T(-o.x, -o.y));
8565
+ moved = transformSurface(src, hl, opts);
8566
+ }
8567
+ } catch (e) {
8568
+ if (e instanceof TransformTooLargeError) fail2(e.message);
8569
+ throw e;
8570
+ }
8571
+ }
8572
+ const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;
8573
+ if (part.what === "selection") {
8574
+ const { width, height } = this.host.canvas();
8575
+ const data = new Uint8Array(readRegion(result, { x: 0, y: 0, width, height }).buffer);
8576
+ const next = createSelectionMask(width, height, data);
8577
+ selection = next.bounds ? next : null;
8578
+ } else if (part.what === "pixels") {
8579
+ layers = mapLayer(layers, targetId, (l) => {
8580
+ const next = { ...l, surface: result };
8581
+ delete next.source;
8582
+ return next;
8583
+ });
8584
+ } else {
8585
+ layers = mapLayer(layers, targetId, (l) => ({ ...l, mask: { ...l.mask, surface: result } }));
8586
+ }
8587
+ }
8588
+ return { layers, selection };
8589
+ }
8590
+ render(preview) {
8591
+ const { layers, selection } = this.resample(false, preview);
8592
+ this.host.show(layers, selection, this.copyId ?? this.startActive);
8593
+ this.shown = this.host.layers();
8594
+ }
8595
+ /** Finishes the transform with full-quality resampling, as one undo step. */
8596
+ commit() {
8597
+ this.check();
8598
+ const g = this.geometryValue;
8599
+ const changed = !isIdentityTransform(g) || Boolean(this.copyId);
8600
+ if (!changed) {
8601
+ this.closed = true;
8602
+ this.host.restore(this.before);
8603
+ return this.result(false, this.startLayers, this.startSelection);
8604
+ }
8605
+ const { layers, selection } = this.resample(true);
8606
+ this.closed = true;
8607
+ this.host.show(layers, selection, this.copyId ?? this.startActive);
8608
+ this.host.record(this.before);
8609
+ return this.result(true, layers, selection);
8610
+ }
8611
+ /** Abandons the transform and puts everything back (no history). */
8612
+ cancel() {
8613
+ if (this.closed) return;
8614
+ this.closed = true;
8615
+ this.host.restore(this.before);
8616
+ }
8617
+ result(changed, layers, selection) {
8618
+ let bounds = null;
8619
+ const id = this.copyId ?? this.layerId;
8620
+ const layer = id ? findLayer(layers, id)?.layer : void 0;
8621
+ if (this.moves.includes("pixels") && layer?.type === "pixel") bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);
8622
+ else if (this.moves.includes("mask") && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === "pixel" ? layer.offset : { x: 0, y: 0 });
8623
+ else if (this.moves.includes("selection")) bounds = selection?.bounds ? { ...selection.bounds } : null;
8624
+ return {
8625
+ layerId: this.copyId ?? this.layerId,
8626
+ content: this.content,
8627
+ moved: [...this.moves],
8628
+ box: { ...this.box },
8629
+ corners: this.geometryValue.corners,
8630
+ bounds,
8631
+ changed
8632
+ };
8633
+ }
8634
+ };
8635
+
8090
8636
  // src/image-editor/controller.ts
8091
8637
  var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
8092
8638
  function paintCoverageInto(base, coverage, apply, rects) {
@@ -8763,17 +9309,17 @@ var ImageEditorController = class {
8763
9309
  */
8764
9310
  sourceSurface() {
8765
9311
  const { width, height } = this.naturalSize;
8766
- const fromPixels = (px2) => {
8767
- let s = this.sourceSurfaces.get(px2);
9312
+ const fromPixels = (px3) => {
9313
+ let s = this.sourceSurfaces.get(px3);
8768
9314
  if (!s) {
8769
- s = surfaceFromPixels(px2, 4);
8770
- this.sourceSurfaces.set(px2, s);
9315
+ s = surfaceFromPixels(px3, 4);
9316
+ this.sourceSurfaces.set(px3, s);
8771
9317
  }
8772
9318
  return s;
8773
9319
  };
8774
- const px = this.samplePixels;
8775
- const fits = Boolean(px && px.width === width && px.height === height);
8776
- if (this.samplePixelsOverride && fits) return fromPixels(px);
9320
+ const px2 = this.samplePixels;
9321
+ const fits = Boolean(px2 && px2.width === width && px2.height === height);
9322
+ if (this.samplePixelsOverride && fits) return fromPixels(px2);
8777
9323
  const el = this.imageElement;
8778
9324
  if (el) {
8779
9325
  const cached = this.sourceSurfaces.get(el);
@@ -8787,7 +9333,7 @@ var ImageEditorController = class {
8787
9333
  return s;
8788
9334
  }
8789
9335
  }
8790
- return fits ? fromPixels(px) : null;
9336
+ return fits ? fromPixels(px2) : null;
8791
9337
  }
8792
9338
  opaqueCache = /* @__PURE__ */ new WeakMap();
8793
9339
  /** Whether every source pixel is opaque (some legacy renders depend on it). */
@@ -8846,11 +9392,11 @@ var ImageEditorController = class {
8846
9392
  const { width, height } = this.naturalSize;
8847
9393
  let surface = emptySurface(4);
8848
9394
  if (init.pixels) {
8849
- const px = init.pixels;
8850
- if (!(px.width > 0 && px.height > 0) || px.data.length < px.width * px.height * 4) {
9395
+ const px2 = init.pixels;
9396
+ if (!(px2.width > 0 && px2.height > 0) || px2.data.length < px2.width * px2.height * 4) {
8851
9397
  throw new LayerOperationError("pixels must be RGBA with width \xD7 height \xD7 4 bytes.");
8852
9398
  }
8853
- surface = surfaceFromPixels(px, 4);
9399
+ surface = surfaceFromPixels(px2, 4);
8854
9400
  } else if (init.color !== void 0) {
8855
9401
  const c = parseColor(init.color);
8856
9402
  surface = solidSurface(4, { x: 0, y: 0, width, height }, packRgba(c[0], c[1], c[2], c[3]));
@@ -9038,7 +9584,7 @@ var ImageEditorController = class {
9038
9584
  if (this.state.activeLayerId === id) this.state.activeTarget = "pixels";
9039
9585
  });
9040
9586
  }
9041
- /** Changes any layer mask's density, feather or enabled flag (pixels kept). Returns the mask. */
9587
+ /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */
9042
9588
  setLayerMaskProps(id, props, commitToHistory = true) {
9043
9589
  const layer = requireLayer(this.state.layers, id).layer;
9044
9590
  if (!layer.mask) throw new LayerOperationError(`Layer "${layer.name || id}" has no mask; add one with add_layer_mask.`);
@@ -9098,14 +9644,14 @@ var ImageEditorController = class {
9098
9644
  const surface = this.surfaceOf(layer);
9099
9645
  const sb = sel.bounds;
9100
9646
  const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };
9101
- const px = readRegion(surface, src);
9102
- const kept = options.cut ? new Uint8ClampedArray(px) : null;
9647
+ const px2 = readRegion(surface, src);
9648
+ const kept = options.cut ? new Uint8ClampedArray(px2) : null;
9103
9649
  for (let y = 0; y < sb.height; y++) {
9104
9650
  for (let x = 0; x < sb.width; x++) {
9105
9651
  const c = sel.data[(sb.y + y) * sel.width + sb.x + x];
9106
9652
  const i = (y * sb.width + x) * 4 + 3;
9107
9653
  if (kept) kept[i] = Math.round(kept[i] * (1 - c / 255));
9108
- px[i] = Math.round(px[i] * (c / 255));
9654
+ px2[i] = Math.round(px2[i] * (c / 255));
9109
9655
  }
9110
9656
  }
9111
9657
  const copy = {
@@ -9115,7 +9661,7 @@ var ImageEditorController = class {
9115
9661
  visible: true,
9116
9662
  opacity: 1,
9117
9663
  blend: "normal",
9118
- surface: surfaceFromPixels({ width: src.width, height: src.height, data: px }, 4, src.x, src.y),
9664
+ surface: surfaceFromPixels({ width: src.width, height: src.height, data: px2 }, 4, src.x, src.y),
9119
9665
  offset: { ...layer.offset }
9120
9666
  };
9121
9667
  let next = insertLayer(this.state.layers, copy, loc.parent?.id ?? null, loc.index + 1);
@@ -9130,6 +9676,134 @@ var ImageEditorController = class {
9130
9676
  });
9131
9677
  return copy.id;
9132
9678
  }
9679
+ // --- Free Transform ---
9680
+ openTransform = null;
9681
+ /** The Free Transform in progress (see `beginFreeTransform`), or `null`. */
9682
+ getFreeTransform() {
9683
+ if (this.openTransform && !this.openTransform.isOpen) this.openTransform = null;
9684
+ return this.openTransform;
9685
+ }
9686
+ transformHost() {
9687
+ return {
9688
+ layers: () => this.state.layers,
9689
+ activeLayerId: () => this.state.activeLayerId,
9690
+ activeTarget: () => this.state.activeTarget,
9691
+ selection: () => this.state.selection?.bounds ? this.state.selection : null,
9692
+ canvas: () => this.naturalSize,
9693
+ pixelsOf: (layer) => this.surfaceOf(layer),
9694
+ capture: () => this.captureHistoryEntry(),
9695
+ show: (layers, selection, active) => {
9696
+ this.state.layers = layers;
9697
+ this.state.selection = selection;
9698
+ if (active !== this.state.activeLayerId) {
9699
+ this.state.activeLayerId = active;
9700
+ this.state.activeTarget = "pixels";
9701
+ }
9702
+ this.notify();
9703
+ },
9704
+ record: (before) => {
9705
+ if (this.isApplyingHistory) return;
9706
+ this.undoStack.push(before);
9707
+ if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();
9708
+ this.redoStack = [];
9709
+ this.trimHistoryBytes();
9710
+ this.notify();
9711
+ },
9712
+ restore: (before) => {
9713
+ const e = before;
9714
+ this.state.layers = e.layers;
9715
+ this.state.selection = e.selection;
9716
+ this.state.activeLayerId = e.activeLayerId;
9717
+ this.state.activeTarget = e.activeTarget;
9718
+ this.notify();
9719
+ }
9720
+ };
9721
+ }
9722
+ /**
9723
+ * Starts a Free Transform (Edit › Free Transform, Transform Selection, or a transform of a Layer
9724
+ * via Copy): of the layer's pixels with its linked mask, an unlinked mask alone, the selected
9725
+ * pixels or just the selection outline (see `FreeTransformInit`). Change the geometry with
9726
+ * `session.update` (live preview, no history), then `commit()` (one undo step) or `cancel()`.
9727
+ * Refused while another transform is open.
9728
+ */
9729
+ beginFreeTransform(init = {}) {
9730
+ if (this.getFreeTransform()) throw new LayerOperationError("A Free Transform is already open; commit or cancel it first.");
9731
+ const session = new FreeTransformSession(this.transformHost(), init);
9732
+ this.openTransform = session;
9733
+ this.notify();
9734
+ return session;
9735
+ }
9736
+ /**
9737
+ * Transforms in one step (agents, scripts): begins a Free Transform for `spec`, sets its corners
9738
+ * from exactly one of `matrix` (document-space affine), `corners` (where the frame's corners go:
9739
+ * distort / perspective) or `params` (scale, rotate, skew and move about a reference point), and
9740
+ * commits it as one undo step. Returns what changed and the new bounds.
9741
+ */
9742
+ transformLayer(spec) {
9743
+ const given = [spec.matrix, spec.corners, spec.params].filter((v) => v !== void 0).length;
9744
+ if (given > 1) throw new LayerOperationError("Give only one of matrix, corners and params.");
9745
+ if (!given && spec.warp === void 0) throw new LayerOperationError("Give matrix, corners or params (or a warp).");
9746
+ if (spec.matrix && !(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) {
9747
+ throw new LayerOperationError("matrix must be six finite numbers [a, b, c, d, e, f] (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f).");
9748
+ }
9749
+ if (spec.matrix) {
9750
+ const [a, b, c, d] = spec.matrix;
9751
+ if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError("matrix is singular (it collapses the layer to a line); its determinant a\xB7d \u2212 b\xB7c must not be 0.");
9752
+ }
9753
+ const session = this.beginFreeTransform(spec);
9754
+ try {
9755
+ const g = identityTransform(session.box);
9756
+ const corners = spec.matrix ? cornersFromMatrix(session.box, spec.matrix) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;
9757
+ let warp = null;
9758
+ if (spec.warp) {
9759
+ if ("style" in spec.warp) {
9760
+ if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style "${String(spec.warp.style)}". Styles: ${WARP_STYLES.join(", ")}.`);
9761
+ warp = presetWarp(session.box, spec.warp);
9762
+ } else {
9763
+ if (!isValidWarp(spec.warp)) throw new LayerOperationError("mesh must have us and vs (knots from 0 to 1, increasing) and (3\xB7columns + 1) \xD7 (3\xB7rows + 1) finite points, row by row.");
9764
+ warp = spec.warp;
9765
+ }
9766
+ }
9767
+ session.update({ corners, warp }, false);
9768
+ return session.commit();
9769
+ } catch (e) {
9770
+ session.cancel();
9771
+ throw e;
9772
+ }
9773
+ }
9774
+ /** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */
9775
+ flipLayer(axis, init = {}) {
9776
+ if (axis !== "horizontal" && axis !== "vertical") throw new LayerOperationError(`axis must be horizontal or vertical, got "${String(axis)}".`);
9777
+ const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
9778
+ try {
9779
+ session.update({ corners: flipCorners(identityTransform(session.box), axis) }, false);
9780
+ return session.commit();
9781
+ } catch (e) {
9782
+ session.cancel();
9783
+ throw e;
9784
+ }
9785
+ }
9786
+ /**
9787
+ * Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's
9788
+ * centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).
9789
+ */
9790
+ rotateLayer90(turn, init = {}) {
9791
+ if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);
9792
+ const session = this.beginFreeTransform({ interpolation: "nearest", ...init });
9793
+ try {
9794
+ const b = session.box;
9795
+ const cx = b.x + b.width / 2;
9796
+ const cy = b.y + b.height / 2;
9797
+ const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);
9798
+ const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);
9799
+ const m = turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];
9800
+ session.update({ corners: cornersFromMatrix(b, m) }, false);
9801
+ return session.commit();
9802
+ } catch (e) {
9803
+ session.cancel();
9804
+ throw e;
9805
+ }
9806
+ }
9133
9807
  /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */
9134
9808
  addLayerFilter(id, filter) {
9135
9809
  const f = normalizeFilter(filter);
@@ -10377,9 +11051,9 @@ var ImageEditorController = class {
10377
11051
  return;
10378
11052
  }
10379
11053
  const { width, height } = this.naturalSize;
10380
- let px = await this.decodeImage(newDataUrl);
10381
- if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);
10382
- const data = new Uint8ClampedArray(px.data);
11054
+ let px2 = await this.decodeImage(newDataUrl);
11055
+ if (px2.width !== width || px2.height !== height) px2 = resizePixels(px2, width, height);
11056
+ const data = new Uint8ClampedArray(px2.data);
10383
11057
  const sel = this.state.selection?.bounds ? this.state.selection : null;
10384
11058
  if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round(data[i] * sel.data[p] / 255);
10385
11059
  return this.addPixelLayer({ name: "Inpaint", pixels: { width, height, data } });
@@ -10400,10 +11074,10 @@ var ImageEditorController = class {
10400
11074
  if (!target) throw new LayerOperationError("There is no pixel layer to mask.");
10401
11075
  requireUnlocked(target, "all", "mask it");
10402
11076
  const { width, height } = this.naturalSize;
10403
- let px = await this.decodeImage(newDataUrl);
10404
- if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);
11077
+ let px2 = await this.decodeImage(newDataUrl);
11078
+ if (px2.width !== width || px2.height !== height) px2 = resizePixels(px2, width, height);
10405
11079
  const alpha = new Uint8ClampedArray(width * height);
10406
- for (let p = 0; p < alpha.length; p++) alpha[p] = px.data[p * 4 + 3];
11080
+ for (let p = 0; p < alpha.length; p++) alpha[p] = px2.data[p * 4 + 3];
10407
11081
  let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);
10408
11082
  if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);
10409
11083
  this.commitLayers(setLayerMask(this.state.layers, target.id, { surface, defaultValue: 0 }));
@@ -10868,6 +11542,10 @@ function withMaskProps(mask, props) {
10868
11542
  if (props.enabled) delete next.enabled;
10869
11543
  else next.enabled = false;
10870
11544
  }
11545
+ if (props.linked !== void 0) {
11546
+ if (props.linked) delete next.linked;
11547
+ else next.linked = false;
11548
+ }
10871
11549
  return next;
10872
11550
  }
10873
11551
  function resizeLegacyMasks(layers, from, to) {
@@ -10913,24 +11591,24 @@ function pixelsOnly(layers) {
10913
11591
  }
10914
11592
  return out;
10915
11593
  }
10916
- function resizePixels(px, width, height) {
10917
- const src = px.data;
11594
+ function resizePixels(px2, width, height) {
11595
+ const src = px2.data;
10918
11596
  const out = new Uint8ClampedArray(width * height * 4);
10919
- const kx = px.width / width;
10920
- const ky = px.height / height;
11597
+ const kx = px2.width / width;
11598
+ const ky = px2.height / height;
10921
11599
  for (let y = 0; y < height; y++) {
10922
- const fy = Math.min(px.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));
11600
+ const fy = Math.min(px2.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));
10923
11601
  const y0 = Math.floor(fy);
10924
- const y1 = Math.min(px.height - 1, y0 + 1);
11602
+ const y1 = Math.min(px2.height - 1, y0 + 1);
10925
11603
  const ty = fy - y0;
10926
11604
  for (let x = 0; x < width; x++) {
10927
- const fx = Math.min(px.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));
11605
+ const fx = Math.min(px2.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));
10928
11606
  const x0 = Math.floor(fx);
10929
- const x1 = Math.min(px.width - 1, x0 + 1);
11607
+ const x1 = Math.min(px2.width - 1, x0 + 1);
10930
11608
  const tx = fx - x0;
10931
11609
  for (let c = 0; c < 4; c++) {
10932
- const a = src[(y0 * px.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px.width + x1) * 4 + c] * tx;
10933
- const b = src[(y1 * px.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px.width + x1) * 4 + c] * tx;
11610
+ const a = src[(y0 * px2.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px2.width + x1) * 4 + c] * tx;
11611
+ const b = src[(y1 * px2.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px2.width + x1) * 4 + c] * tx;
10934
11612
  out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;
10935
11613
  }
10936
11614
  }
@@ -11425,8 +12103,8 @@ function describeSelection(mask) {
11425
12103
  if (!mask || !mask.bounds) return "No selection";
11426
12104
  let sum = 0;
11427
12105
  for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
11428
- const pct2 = sum / 255 / mask.data.length * 100;
11429
- const shown = pct2 >= 10 ? Math.round(pct2) : Math.round(pct2 * 10) / 10;
12106
+ const pct3 = sum / 255 / mask.data.length * 100;
12107
+ const shown = pct3 >= 10 ? Math.round(pct3) : Math.round(pct3 * 10) / 10;
11430
12108
  return `${mask.bounds.width} \xD7 ${mask.bounds.height} px selected (${shown}% of the image)`;
11431
12109
  }
11432
12110
 
@@ -11746,14 +12424,14 @@ function rebin(bins, count) {
11746
12424
  return out;
11747
12425
  }
11748
12426
  function histogramSummary(h, bins) {
11749
- const pct2 = (n) => h.samples ? round(n / h.samples * 100) : 0;
12427
+ const pct3 = (n) => h.samples ? round(n / h.samples * 100) : 0;
11750
12428
  const channels = { red: h.red, green: h.green, blue: h.blue, luminance: h.luminance };
11751
12429
  return {
11752
12430
  samples: h.samples,
11753
12431
  transparent: h.transparent,
11754
12432
  clipping: {
11755
- shadowsPercent: { red: pct2(h.shadows[0]), green: pct2(h.shadows[1]), blue: pct2(h.shadows[2]) },
11756
- highlightsPercent: { red: pct2(h.highlights[0]), green: pct2(h.highlights[1]), blue: pct2(h.highlights[2]) }
12433
+ shadowsPercent: { red: pct3(h.shadows[0]), green: pct3(h.shadows[1]), blue: pct3(h.shadows[2]) },
12434
+ highlightsPercent: { red: pct3(h.highlights[0]), green: pct3(h.highlights[1]), blue: pct3(h.highlights[2]) }
11757
12435
  },
11758
12436
  stats: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, binStats(v, h.samples)])),
11759
12437
  ...bins ? { bins: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, rebin(v, bins)])) } : {}
@@ -11985,6 +12663,7 @@ function maskSummary(mask) {
11985
12663
  const out = {
11986
12664
  kind: mask.source ? "depth" : "raster",
11987
12665
  enabled: mask.enabled !== false,
12666
+ linked: mask.linked !== false,
11988
12667
  density: mask.density ?? 1,
11989
12668
  feather: mask.feather ?? 0
11990
12669
  };
@@ -12361,23 +13040,29 @@ function createImageLayerTools(controller) {
12361
13040
  const maskProps = {
12362
13041
  name: "set_layer_mask_properties",
12363
13042
  title: "Set layer mask properties",
12364
- description: "Changes a layer mask's density (0..1), feather (px) or enabled flag; its pixels are kept. One undo step. Returns the mask summary.",
13043
+ description: "Changes a layer mask's density (0..1), feather (px), enabled flag or linked flag; its pixels are kept. An unlinked mask (linked false) stays in place when the layer is moved, transformed or warped; transform it alone with transform_layer target mask. One undo step. Returns the mask summary.",
12365
13044
  inputSchema: {
12366
13045
  type: "object",
12367
13046
  properties: {
12368
13047
  layerId: LAYER_ID2,
12369
13048
  density: { type: "number", minimum: 0, maximum: 1, description: "Density 0..1." },
12370
13049
  feather: { type: "number", minimum: 0, maximum: 1e3, description: "Feather \u03C3 in px." },
12371
- enabled: { type: "boolean", description: "false ignores the mask but keeps it." }
13050
+ enabled: { type: "boolean", description: "false ignores the mask but keeps it." },
13051
+ linked: { type: "boolean", description: "false unlinks the mask from the layer pixels (it stays put when they move or transform); true links it again." }
12372
13052
  },
12373
13053
  required: ["layerId"],
12374
13054
  additionalProperties: false
12375
13055
  },
12376
13056
  annotations: { title: "Set layer mask properties", ...WRITE_IDEMPOTENT2 },
12377
- handler: ({ layerId, density, feather, enabled }) => {
13057
+ handler: ({ layerId, density, feather, enabled, linked }) => {
12378
13058
  const id = exists(layerId);
12379
- if ([density, feather, enabled].every((v) => v === void 0)) throw new LayerOperationError("Give density, feather or enabled.");
12380
- const mask = controller.setLayerMaskProps(id, { density, feather, enabled });
13059
+ if ([density, feather, enabled, linked].every((v) => v === void 0)) throw new LayerOperationError("Give density, feather, enabled or linked.");
13060
+ const mask = controller.setLayerMaskProps(id, {
13061
+ density,
13062
+ feather,
13063
+ enabled,
13064
+ linked
13065
+ });
12381
13066
  return { layerId: id, mask: maskSummary(mask) };
12382
13067
  }
12383
13068
  };
@@ -12620,7 +13305,7 @@ var DEFS = {
12620
13305
  };
12621
13306
  var nullable = (schema, description) => ({ anyOf: [schema, { type: "null" }], description });
12622
13307
  function settingsSchema(forUpdate) {
12623
- const opt = (schema, description) => forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description };
13308
+ const opt2 = (schema, description) => forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description };
12624
13309
  return {
12625
13310
  type: "object",
12626
13311
  description: forUpdate ? "Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect." : "Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.",
@@ -12635,7 +13320,7 @@ function settingsSchema(forUpdate) {
12635
13320
  spread: { type: "number", minimum: 0, maximum: 1, description: "drop-shadow, outer-glow: 0..1 of the size made solid (default 0)." },
12636
13321
  choke: { type: "number", minimum: 0, maximum: 1, description: "inner-shadow, inner-glow: 0..1 of the size made solid (default 0)." },
12637
13322
  size: { type: "number", minimum: 0, maximum: 250, description: "Shadows, glows, satin, bevel-emboss: blur / size in px (default 5; satin 14); stroke: width in px (default 3)." },
12638
- contour: opt({ $ref: "#/$defs/contour" }, "Shadows, glows, satin: coverage contour (default linear)."),
13323
+ contour: opt2({ $ref: "#/$defs/contour" }, "Shadows, glows, satin: coverage contour (default linear)."),
12639
13324
  antiAlias: { type: "boolean", description: "Shadows, glows, satin: smooth the contour (default false; satin true)." },
12640
13325
  noise: { type: "number", minimum: 0, maximum: 1, description: "Shadows, glows: speckle 0..1 (default 0)." },
12641
13326
  knocksOut: { type: "boolean", description: "drop-shadow: the layer hides the shadow beneath its see-through fill (default true)." },
@@ -12649,14 +13334,14 @@ function settingsSchema(forUpdate) {
12649
13334
  direction: { type: "string", enum: ["up", "down"], description: "bevel-emboss: raised (up, default) or sunk (down)." },
12650
13335
  soften: { type: "number", minimum: 0, maximum: 16, description: "bevel-emboss: blur of the height in px (default 0)." },
12651
13336
  altitude: { type: "number", minimum: 0, maximum: 90, description: "bevel-emboss: light altitude in degrees (default 30)." },
12652
- glossContour: opt({ $ref: "#/$defs/contour" }, "bevel-emboss: gloss contour over the shading (default linear)."),
13337
+ glossContour: opt2({ $ref: "#/$defs/contour" }, "bevel-emboss: gloss contour over the shading (default linear)."),
12653
13338
  highlightColor: { $ref: "#/$defs/color", description: "bevel-emboss: highlight colour (default #ffffff)." },
12654
13339
  highlightBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: highlight mode (default screen)." },
12655
13340
  highlightOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: highlight opacity (default 0.75)." },
12656
13341
  shadowColor: { $ref: "#/$defs/color", description: "bevel-emboss: shadow colour (default #000000)." },
12657
13342
  shadowBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: shadow mode (default multiply)." },
12658
13343
  shadowOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: shadow opacity (default 0.75)." },
12659
- contourElement: opt(
13344
+ contourElement: opt2(
12660
13345
  {
12661
13346
  type: "object",
12662
13347
  properties: {
@@ -12669,7 +13354,7 @@ function settingsSchema(forUpdate) {
12669
13354
  },
12670
13355
  "bevel-emboss: the Contour element, shaping the bevel's height profile (default off)."
12671
13356
  ),
12672
- texture: opt(
13357
+ texture: opt2(
12673
13358
  {
12674
13359
  type: "object",
12675
13360
  properties: {
@@ -13238,6 +13923,248 @@ function createImagePsdTools(controller) {
13238
13923
  return [inspect, open, save];
13239
13924
  }
13240
13925
 
13926
+ // src/image-editor/transform-tools.ts
13927
+ var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
13928
+ var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
13929
+ var round4 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
13930
+ var roundPoint = (p) => ({ x: round4(p.x), y: round4(p.y) });
13931
+ var POINT = {
13932
+ type: "object",
13933
+ properties: { x: { type: "number", description: "Document pixels from the left." }, y: { type: "number", description: "Document pixels from the top." } },
13934
+ required: ["x", "y"],
13935
+ additionalProperties: false
13936
+ };
13937
+ var TARGET2 = {
13938
+ layerId: { type: "string", description: "Layer to transform (from list_layers; default the active layer)." },
13939
+ content: {
13940
+ type: "string",
13941
+ enum: FREE_TRANSFORM_CONTENTS.filter((c) => c !== "selection-outline"),
13942
+ description: "What moves: layer (all its pixels with a linked mask), selection (only the selected pixels, committed back into the layer; the outline moves too), selection-copy (Layer via Copy: the selected pixels on a new layer above, then transformed; the original stays) or auto (default: selection when something is selected, else layer)."
13943
+ },
13944
+ channel: {
13945
+ type: "string",
13946
+ enum: ["pixels", "mask"],
13947
+ description: "pixels (default) moves the pixels and a linked mask; mask moves an unlinked mask alone (a linked mask always moves with its pixels)."
13948
+ },
13949
+ interpolation: { type: "string", enum: INTERPOLATIONS, description: "Resampling: nearest (hard pixels), bilinear or bicubic (default, Photoshop\u2019s Bicubic)." }
13950
+ };
13951
+ var GEOMETRY = {
13952
+ scaleX: { type: "number", description: "Width in percent of the frame (default 100; negative flips horizontally)." },
13953
+ scaleY: { type: "number", description: "Height in percent of the frame (default 100; negative flips vertically)." },
13954
+ rotate: { type: "number", description: "Rotation in degrees, clockwise (default 0)." },
13955
+ skewX: { type: "number", minimum: -89, maximum: 89, description: "Horizontal skew in degrees: vertical edges lean right for positive angles (default 0)." },
13956
+ skewY: { type: "number", minimum: -89, maximum: 89, description: "Vertical skew in degrees: horizontal edges lean down for positive angles (default 0)." },
13957
+ dx: { type: "number", description: "Move right after scaling and rotating, in document pixels (default 0)." },
13958
+ dy: { type: "number", description: "Move down after scaling and rotating, in document pixels (default 0)." },
13959
+ reference: {
13960
+ description: "Fixed point of scale, skew and rotate: an anchor of the frame (top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right; default center) or a document point {x, y}.",
13961
+ anyOf: [{ type: "string", enum: TRANSFORM_ANCHORS }, POINT]
13962
+ },
13963
+ matrix: {
13964
+ type: "array",
13965
+ items: { type: "number" },
13966
+ minItems: 6,
13967
+ maxItems: 6,
13968
+ description: "Instead of the numeric fields: an affine matrix [a, b, c, d, e, f] in document pixels (x' = a\xB7x + c\xB7y + e, y' = b\xB7x + d\xB7y + f)."
13969
+ },
13970
+ corners: {
13971
+ type: "array",
13972
+ items: POINT,
13973
+ minItems: 4,
13974
+ maxItems: 4,
13975
+ description: "Instead of the numeric fields: where the frame\u2019s corners go, [top-left, top-right, bottom-right, bottom-left] in document pixels (Distort; for Perspective move a pair of corners symmetrically). The quad must stay convex. Get the frame and its corners from get_transform_frame."
13976
+ }
13977
+ };
13978
+ var NUMERIC = ["scaleX", "scaleY", "rotate", "skewX", "skewY", "dx", "dy", "reference"];
13979
+ function geometryFrom(args) {
13980
+ const numeric = NUMERIC.filter((k) => args[k] !== void 0);
13981
+ const ways = [numeric.length > 0, args.matrix !== void 0, args.corners !== void 0].filter(Boolean).length;
13982
+ if (ways === 0) throw new LayerOperationError("Give the transform one way: numeric fields (scaleX, scaleY, rotate, skewX, skewY, dx, dy, reference), matrix, or corners.");
13983
+ if (ways > 1) throw new LayerOperationError("Give the transform only one way: numeric fields, matrix or corners, not a mix.");
13984
+ if (args.matrix !== void 0) return { matrix: args.matrix };
13985
+ if (args.corners !== void 0) return { corners: args.corners };
13986
+ const params = {};
13987
+ for (const k of NUMERIC) if (args[k] !== void 0) params[k] = args[k];
13988
+ if (params.scaleX === 0 || params.scaleY === 0) throw new LayerOperationError("scaleX and scaleY must not be 0 (that collapses the layer to a line).");
13989
+ if (typeof params.reference === "string" && !isTransformAnchor(params.reference)) throw new LayerOperationError(`Unknown reference "${params.reference}". Use ${TRANSFORM_ANCHORS.join(", ")} or {x, y}.`);
13990
+ return { params };
13991
+ }
13992
+ function targetFrom(args) {
13993
+ const init = {};
13994
+ if (typeof args.layerId === "string") init.layerId = args.layerId;
13995
+ if (args.content !== void 0) init.content = args.content;
13996
+ init.channel = args.channel ?? "pixels";
13997
+ if (args.interpolation !== void 0) init.interpolation = args.interpolation;
13998
+ return init;
13999
+ }
14000
+ function summary2(res) {
14001
+ return {
14002
+ layerId: res.layerId,
14003
+ content: res.content,
14004
+ moved: res.moved,
14005
+ changed: res.changed,
14006
+ frame: { ...res.box },
14007
+ corners: res.corners.map(roundPoint),
14008
+ bounds: res.bounds
14009
+ };
14010
+ }
14011
+ function createImageTransformTools(controller) {
14012
+ const getFrame = {
14013
+ name: "get_transform_frame",
14014
+ title: "Get the Free Transform frame",
14015
+ description: "Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.",
14016
+ inputSchema: {
14017
+ type: "object",
14018
+ properties: {
14019
+ layerId: TARGET2.layerId,
14020
+ content: { type: "string", enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET2.content.description} selection-outline frames the selection outline alone.` },
14021
+ channel: TARGET2.channel,
14022
+ meshColumns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns of an identity warp mesh to return (omit for none)." },
14023
+ meshRows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows of an identity warp mesh to return (default meshColumns)." }
14024
+ },
14025
+ additionalProperties: false
14026
+ },
14027
+ annotations: { title: "Get the Free Transform frame", ...READ6 },
14028
+ handler: (args) => {
14029
+ const init = targetFrom(args);
14030
+ if (init.content === "selection-copy") init.content = "selection";
14031
+ const s = controller.beginFreeTransform(init);
14032
+ const out = {
14033
+ layerId: s.layerId,
14034
+ content: args.content === "selection-copy" ? "selection-copy" : s.content,
14035
+ moved: [...s.moves],
14036
+ frame: { ...s.box },
14037
+ corners: identityTransform(s.box).corners.map(roundPoint),
14038
+ center: roundPoint({ x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 })
14039
+ };
14040
+ s.cancel();
14041
+ if (args.meshColumns !== void 0) {
14042
+ const cols = args.meshColumns;
14043
+ const rows = args.meshRows ?? cols;
14044
+ const mesh = identityWarpMesh(s.box, cols, rows);
14045
+ out.mesh = { columns: cols, rows, pointsPerRow: warpMeshColumns(mesh), pointRows: warpMeshRows(mesh), points: mesh.points.map(roundPoint) };
14046
+ }
14047
+ return out;
14048
+ }
14049
+ };
14050
+ const transformLayer = {
14051
+ name: "transform_layer",
14052
+ title: "Free Transform a layer",
14053
+ description: "Edit \u203A Free Transform: scales, rotates, skews, moves, distorts or puts in perspective a pixel layer (with its linked mask), the selected pixels, a copy of them on a new layer, or an unlinked mask. Give the transform one way: the numeric fields (as Photoshop\u2019s options bar, about a reference point), a matrix, or the frame\u2019s new corners (distort / perspective; read the frame with get_transform_frame). Resamples once, tile by tile; one undo step. Returns layerId (the new layer for selection-copy), what moved, the frame, the corners and the new document bounds.",
14054
+ inputSchema: {
14055
+ type: "object",
14056
+ properties: { ...TARGET2, ...GEOMETRY },
14057
+ additionalProperties: false
14058
+ },
14059
+ annotations: { title: "Free Transform a layer", ...WRITE5 },
14060
+ handler: (args) => summary2(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) }))
14061
+ };
14062
+ const warpLayer = {
14063
+ name: "warp_layer",
14064
+ title: "Warp a layer",
14065
+ description: "Edit \u203A Transform \u203A Warp: bends a pixel layer (with its linked mask), the selected pixels, a copy of them, or an unlinked mask through a preset (style with bend and distortions in percent) or a B\xE9zier mesh over the frame (get the frame and an identity mesh to edit from get_transform_frame with meshColumns). Give style or mesh, not both. One undo step. Returns what moved and the new document bounds.",
14066
+ inputSchema: {
14067
+ type: "object",
14068
+ properties: {
14069
+ ...TARGET2,
14070
+ style: { type: "string", enum: WARP_STYLES, description: "Preset warp (Photoshop\u2019s menu): arc, arcLower, arcUpper, arch, bulge, shellLower, shellUpper, flag, wave, fish, rise, fisheye, inflate, squeeze, twist." },
14071
+ bend: { type: "number", minimum: -100, maximum: 100, description: "Preset bend in percent, \u2212100..100 (default 50)." },
14072
+ horizontal: { type: "number", minimum: -100, maximum: 100, description: "Preset horizontal distortion in percent, \u2212100..100 (default 0)." },
14073
+ vertical: { type: "number", minimum: -100, maximum: 100, description: "Preset vertical distortion in percent, \u2212100..100 (default 0)." },
14074
+ orientation: { type: "string", enum: ["horizontal", "vertical"], description: "Bend along the horizontal (default) or vertical axis." },
14075
+ mesh: {
14076
+ type: "object",
14077
+ description: "A custom warp: patch columns and rows, and the (3\xB7columns + 1) \xD7 (3\xB7rows + 1) control points row by row in document pixels (anchors where both indices are multiples of 3, B\xE9zier handles between). Start from get_transform_frame\u2019s identity mesh.",
14078
+ properties: {
14079
+ columns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns." },
14080
+ rows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows." },
14081
+ points: { type: "array", items: POINT, minItems: 16, description: "Control points, row by row, document pixels." }
14082
+ },
14083
+ required: ["columns", "rows", "points"],
14084
+ additionalProperties: false
14085
+ }
14086
+ },
14087
+ additionalProperties: false
14088
+ },
14089
+ annotations: { title: "Warp a layer", ...WRITE5 },
14090
+ handler: (args) => {
14091
+ const hasPreset = ["style", "bend", "horizontal", "vertical", "orientation"].some((k) => args[k] !== void 0);
14092
+ if (hasPreset && args.mesh !== void 0) throw new LayerOperationError("Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.");
14093
+ if (!hasPreset && args.mesh === void 0) throw new LayerOperationError("Give a preset style or a mesh.");
14094
+ let warp;
14095
+ if (hasPreset) {
14096
+ if (args.style === void 0) throw new LayerOperationError(`style is required with a preset. Styles: ${WARP_STYLES.join(", ")}.`);
14097
+ warp = {
14098
+ style: args.style,
14099
+ bend: args.bend ?? 50,
14100
+ horizontal: args.horizontal,
14101
+ vertical: args.vertical,
14102
+ orientation: args.orientation
14103
+ };
14104
+ } else {
14105
+ const m = args.mesh;
14106
+ const want = (3 * m.columns + 1) * (3 * m.rows + 1);
14107
+ if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} \xD7 ${m.rows} mesh needs (3\xB7${m.columns} + 1) \xD7 (3\xB7${m.rows} + 1) = ${want} points, got ${m.points.length}.`);
14108
+ const knots = (n) => Array.from({ length: n + 1 }, (_, i) => i / n);
14109
+ warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };
14110
+ }
14111
+ return summary2(controller.transformLayer({ ...targetFrom(args), warp }));
14112
+ }
14113
+ };
14114
+ const transformSelection = {
14115
+ name: "transform_selection",
14116
+ title: "Transform the selection outline",
14117
+ description: "Select \u203A Transform Selection: scales, rotates, skews, moves or distorts only the selection outline; no pixels change. Give the transform one way: the numeric fields (about a reference point of the selection\u2019s bounds), a matrix, or the new corners of its bounds. Needs a selection. One undo step. Returns the new selection bounds.",
14118
+ inputSchema: {
14119
+ type: "object",
14120
+ properties: { interpolation: { ...TARGET2.interpolation, description: "Resampling of soft edges: nearest, bilinear or bicubic (default)." }, ...GEOMETRY },
14121
+ additionalProperties: false
14122
+ },
14123
+ annotations: { title: "Transform the selection outline", ...WRITE5 },
14124
+ handler: (args) => {
14125
+ const res = controller.transformLayer({ content: "selection-outline", interpolation: args.interpolation, ...geometryFrom(args) });
14126
+ return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };
14127
+ }
14128
+ };
14129
+ const flipLayer = {
14130
+ name: "flip_layer",
14131
+ title: "Flip a layer",
14132
+ description: "Edit \u203A Transform \u203A Flip Horizontal or Vertical: mirrors a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (no resampling). To flip the whole image use flip_image. One undo step. Returns the new bounds.",
14133
+ inputSchema: {
14134
+ type: "object",
14135
+ properties: {
14136
+ axis: { type: "string", enum: ["horizontal", "vertical"], description: "horizontal mirrors left \u2194 right, vertical top \u2194 bottom." },
14137
+ layerId: TARGET2.layerId,
14138
+ content: TARGET2.content,
14139
+ channel: TARGET2.channel
14140
+ },
14141
+ required: ["axis"],
14142
+ additionalProperties: false
14143
+ },
14144
+ annotations: { title: "Flip a layer", ...WRITE5 },
14145
+ handler: (args) => summary2(controller.flipLayer(args.axis, targetFrom(args)))
14146
+ };
14147
+ const rotate90 = {
14148
+ name: "rotate_layer_90",
14149
+ title: "Rotate a layer by quarter turns",
14150
+ description: "Edit \u203A Transform \u203A Rotate 90\xB0 / 180\xB0: turns a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (pixels land on whole pixels; no resampling). To rotate the whole image use set_rotation. One undo step. Returns the new bounds.",
14151
+ inputSchema: {
14152
+ type: "object",
14153
+ properties: {
14154
+ turn: { type: "integer", enum: [90, -90, 180], description: "Degrees: 90 clockwise, -90 counter-clockwise, or 180." },
14155
+ layerId: TARGET2.layerId,
14156
+ content: TARGET2.content,
14157
+ channel: TARGET2.channel
14158
+ },
14159
+ required: ["turn"],
14160
+ additionalProperties: false
14161
+ },
14162
+ annotations: { title: "Rotate a layer by quarter turns", ...WRITE5 },
14163
+ handler: (args) => summary2(controller.rotateLayer90(args.turn, targetFrom(args)))
14164
+ };
14165
+ return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];
14166
+ }
14167
+
13241
14168
  // src/image-editor/tools.ts
13242
14169
  var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
13243
14170
  function adjustmentsSummary(adjustments) {
@@ -13475,6 +14402,7 @@ function createImageEditorToolProvider(controller, options = {}) {
13475
14402
  ...createImageSelectionTools(controller),
13476
14403
  ...createImageEditTools(controller),
13477
14404
  ...createImageLayerTools(controller),
14405
+ ...createImageTransformTools(controller),
13478
14406
  ...createImageEffectTools(controller),
13479
14407
  ...createImagePaintTools(controller),
13480
14408
  ...createPigmentTools(),
@@ -13686,8 +14614,8 @@ function applyRegionMosaic(imageData, regions, blockSize = 12) {
13686
14614
  const count = bw * bh;
13687
14615
  for (let py = by; py < by + bh; py++) {
13688
14616
  const rowOffset = py * width;
13689
- for (let px = bx; px < bx + bw; px++) {
13690
- const idx = (rowOffset + px) * 4;
14617
+ for (let px2 = bx; px2 < bx + bw; px2++) {
14618
+ const idx = (rowOffset + px2) * 4;
13691
14619
  rSum += data[idx];
13692
14620
  gSum += data[idx + 1];
13693
14621
  bSum += data[idx + 2];
@@ -13700,8 +14628,8 @@ function applyRegionMosaic(imageData, regions, blockSize = 12) {
13700
14628
  const aAvg = Math.round(aSum / count);
13701
14629
  for (let py = by; py < by + bh; py++) {
13702
14630
  const rowOffset = py * width;
13703
- for (let px = bx; px < bx + bw; px++) {
13704
- const idx = (rowOffset + px) * 4;
14631
+ for (let px2 = bx; px2 < bx + bw; px2++) {
14632
+ const idx = (rowOffset + px2) * 4;
13705
14633
  data[idx] = rAvg;
13706
14634
  data[idx + 1] = gAvg;
13707
14635
  data[idx + 2] = bAvg;
@@ -13919,6 +14847,179 @@ async function runLayerMenuAction(controller, action) {
13919
14847
  }
13920
14848
  }
13921
14849
 
14850
+ // src/image-editor/layers/effects-ui.ts
14851
+ function layerEffectsPanel(layer) {
14852
+ const empty = { readOnly: true, enabled: true, scale: 1, rows: [] };
14853
+ if (!layer) return { ...empty, available: false, reason: "Select a layer to add effects." };
14854
+ if (layer.type === "adjustment") return { ...empty, available: false, reason: "Adjustment layers cannot have effects." };
14855
+ const fx = layer.effects;
14856
+ const readOnly = Boolean(layer.locks?.all);
14857
+ return {
14858
+ available: true,
14859
+ readOnly,
14860
+ reason: readOnly ? "The layer is locked; unlock it to change its effects." : void 0,
14861
+ enabled: fx?.enabled ?? true,
14862
+ scale: fx?.scale ?? 1,
14863
+ rows: (fx?.items ?? []).map((e) => ({ id: e.id, kind: e.kind, label: layerEffectLabel(e.kind), enabled: e.enabled }))
14864
+ };
14865
+ }
14866
+ function layerEffectsBadge(layer) {
14867
+ if (!layer.effects?.items.length) return null;
14868
+ return hasActiveEffects(layer.effects) ? "on" : "off";
14869
+ }
14870
+ var LAYER_EFFECT_MENU = LAYER_EFFECT_KINDS.map((kind) => ({ kind, label: layerEffectLabel(kind) }));
14871
+ var BLENDS = BLEND_MODES.filter((m) => m.id !== "pigment").map((m) => ({ value: m.id, label: m.label }));
14872
+ var opt = (...pairs) => pairs.map(([value, label]) => ({ value, label }));
14873
+ function patternOptions(patterns) {
14874
+ return [...patterns.map((p) => ({ value: p.id, label: p.name || p.id })), ...builtinPatternIds().map((id) => ({ value: id, label: builtinPattern(id).name }))];
14875
+ }
14876
+ function getPath(o, path) {
14877
+ let v = o;
14878
+ for (const p of path.split(".")) {
14879
+ if (v === null || v === void 0) return void 0;
14880
+ if (Array.isArray(v)) v = p === "last" ? v[v.length - 1] : v[Number(p)];
14881
+ else v = v[p];
14882
+ }
14883
+ return v;
14884
+ }
14885
+ function setPath(o, parts, value) {
14886
+ if (!parts.length) return value;
14887
+ const [p, ...rest] = parts;
14888
+ if (Array.isArray(o)) {
14889
+ const i = p === "last" ? o.length - 1 : Number(p);
14890
+ const out = [...o];
14891
+ out[i] = setPath(o[i], rest, value);
14892
+ return out;
14893
+ }
14894
+ const r = o ?? {};
14895
+ return { ...r, [p]: setPath(r[p], rest, value) };
14896
+ }
14897
+ function fillControls(f, allowPattern, patterns) {
14898
+ const types = allowPattern ? opt(["color", "Color"], ["gradient", "Gradient"], ["pattern", "Pattern"]) : opt(["color", "Color"], ["gradient", "Gradient"]);
14899
+ const out = [{ key: "fill.type", label: "Fill", type: "select", value: f.type, options: types }];
14900
+ if (f.type === "color") out.push({ key: "fill.color", label: "Color", type: "color", value: f.color });
14901
+ else if (f.type === "gradient") out.push(...gradientControls("fill.gradient", f.gradient));
14902
+ else {
14903
+ out.push(
14904
+ { key: "fill.patternId", label: "Pattern", type: "select", value: f.patternId, options: patternOptions(patterns) },
14905
+ { key: "fill.scale", label: "Scale", type: "range", min: 0.01, max: 10, step: 0.01, value: f.scale, percent: true, unit: "%" }
14906
+ );
14907
+ }
14908
+ return out;
14909
+ }
14910
+ function gradientControls(prefix, g) {
14911
+ return [
14912
+ { key: `${prefix}.stops.0.color`, label: "Start colour", type: "color", value: g.stops[0]?.color ?? "#000000" },
14913
+ { key: `${prefix}.stops.last.color`, label: "End colour", type: "color", value: g.stops[g.stops.length - 1]?.color ?? "#ffffff" },
14914
+ { key: `${prefix}.style`, label: "Style", type: "select", value: g.style, options: opt(["linear", "Linear"], ["radial", "Radial"], ["angle", "Angle"], ["reflected", "Reflected"], ["diamond", "Diamond"]) },
14915
+ { key: `${prefix}.angle`, label: "Angle", type: "range", min: -180, max: 180, step: 1, value: g.angle, unit: "\xB0" },
14916
+ { key: `${prefix}.scale`, label: "Scale", type: "range", min: 0.1, max: 1.5, step: 0.01, value: g.scale, percent: true, unit: "%" },
14917
+ { key: `${prefix}.reverse`, label: "Reverse", type: "checkbox", value: g.reverse },
14918
+ { key: `${prefix}.align`, label: "Align with layer", type: "checkbox", value: g.align }
14919
+ ];
14920
+ }
14921
+ var px = (key, label, value, max = 250) => ({ key, label, type: "range", min: 0, max, step: 1, value, unit: "px" });
14922
+ var pct2 = (key, label, value, min = 0, max = 1) => ({ key, label, type: "range", min, max, step: 0.01, value, percent: true, unit: "%" });
14923
+ var deg = (key, label, value, min = -180, max = 180) => ({ key, label, type: "range", min, max, step: 1, value, unit: "\xB0" });
14924
+ function layerEffectControls(e, patterns = []) {
14925
+ const blended = e.kind === "bevel-emboss" ? [] : [
14926
+ { key: "blend", label: "Blend mode", type: "select", value: e.blend, options: BLENDS },
14927
+ pct2("opacity", "Opacity", e.opacity)
14928
+ ];
14929
+ switch (e.kind) {
14930
+ case "drop-shadow":
14931
+ case "inner-shadow":
14932
+ return [
14933
+ ...blended,
14934
+ { key: "color", label: "Color", type: "color", value: e.color },
14935
+ { key: "useGlobalLight", label: "Use global light", type: "checkbox", value: e.useGlobalLight },
14936
+ deg("angle", "Angle", e.angle),
14937
+ px("distance", "Distance", e.distance, 500),
14938
+ e.kind === "drop-shadow" ? pct2("spread", "Spread", e.spread) : pct2("choke", "Choke", e.choke),
14939
+ px("size", "Size", e.size),
14940
+ pct2("noise", "Noise", e.noise),
14941
+ ...e.kind === "drop-shadow" ? [{ key: "knocksOut", label: "Layer knocks out shadow", type: "checkbox", value: e.knocksOut }] : []
14942
+ ];
14943
+ case "outer-glow":
14944
+ case "inner-glow":
14945
+ return [
14946
+ ...blended,
14947
+ ...fillControls(e.fill, false, patterns),
14948
+ { key: "technique", label: "Technique", type: "select", value: e.technique, options: opt(["softer", "Softer"], ["precise", "Precise"]) },
14949
+ ...e.kind === "inner-glow" ? [{ key: "source", label: "Source", type: "select", value: e.source, options: opt(["edge", "Edge"], ["center", "Center"]) }] : [],
14950
+ e.kind === "outer-glow" ? pct2("spread", "Spread", e.spread) : pct2("choke", "Choke", e.choke),
14951
+ px("size", "Size", e.size),
14952
+ pct2("range", "Range", e.range, 0.01, 1),
14953
+ pct2("noise", "Noise", e.noise)
14954
+ ];
14955
+ case "bevel-emboss":
14956
+ return [
14957
+ { key: "style", label: "Style", type: "select", value: e.style, options: opt(["inner-bevel", "Inner Bevel"], ["outer-bevel", "Outer Bevel"], ["emboss", "Emboss"], ["pillow-emboss", "Pillow Emboss"], ["stroke-emboss", "Stroke Emboss"]) },
14958
+ { key: "technique", label: "Technique", type: "select", value: e.technique, options: opt(["smooth", "Smooth"], ["chisel-hard", "Chisel Hard"], ["chisel-soft", "Chisel Soft"]) },
14959
+ pct2("depth", "Depth", e.depth, 0, 10),
14960
+ { key: "direction", label: "Direction", type: "select", value: e.direction, options: opt(["up", "Up"], ["down", "Down"]) },
14961
+ px("size", "Size", e.size),
14962
+ px("soften", "Soften", e.soften, 16),
14963
+ { key: "useGlobalLight", label: "Use global light", type: "checkbox", value: e.useGlobalLight },
14964
+ deg("angle", "Angle", e.angle),
14965
+ deg("altitude", "Altitude", e.altitude, 0, 90),
14966
+ { key: "highlightColor", label: "Highlight", type: "color", value: e.highlightColor },
14967
+ { key: "highlightBlend", label: "Highlight mode", type: "select", value: e.highlightBlend, options: BLENDS },
14968
+ pct2("highlightOpacity", "Highlight opacity", e.highlightOpacity),
14969
+ { key: "shadowColor", label: "Shadow", type: "color", value: e.shadowColor },
14970
+ { key: "shadowBlend", label: "Shadow mode", type: "select", value: e.shadowBlend, options: BLENDS },
14971
+ pct2("shadowOpacity", "Shadow opacity", e.shadowOpacity)
14972
+ ];
14973
+ case "satin":
14974
+ return [
14975
+ ...blended,
14976
+ { key: "color", label: "Color", type: "color", value: e.color },
14977
+ deg("angle", "Angle", e.angle),
14978
+ px("distance", "Distance", e.distance),
14979
+ px("size", "Size", e.size),
14980
+ { key: "invert", label: "Invert", type: "checkbox", value: e.invert }
14981
+ ];
14982
+ case "color-overlay":
14983
+ return [...blended, { key: "color", label: "Color", type: "color", value: e.color }];
14984
+ case "gradient-overlay":
14985
+ return [...blended, ...gradientControls("gradient", e.gradient)];
14986
+ case "pattern-overlay":
14987
+ return [
14988
+ ...blended,
14989
+ { key: "patternId", label: "Pattern", type: "select", value: e.patternId, options: patternOptions(patterns) },
14990
+ pct2("scale", "Scale", e.scale, 0.01, 10),
14991
+ deg("angle", "Angle", e.angle ?? 0),
14992
+ { key: "link", label: "Link with layer", type: "checkbox", value: e.link ?? true }
14993
+ ];
14994
+ case "stroke":
14995
+ return [
14996
+ ...blended,
14997
+ px("size", "Size", e.size),
14998
+ { key: "position", label: "Position", type: "select", value: e.position, options: opt(["outside", "Outside"], ["inside", "Inside"], ["center", "Center"]) },
14999
+ ...fillControls(e.fill, true, patterns)
15000
+ ];
15001
+ }
15002
+ }
15003
+ function fillOfType(current, type, patterns) {
15004
+ if (type === "gradient") return { type: "gradient", gradient: defaultEffectGradient() };
15005
+ if (type === "pattern") return { type: "pattern", patternId: patterns[0]?.id ?? "checker", scale: 1 };
15006
+ return { type: "color", color: current.type === "color" ? current.color : "#000000" };
15007
+ }
15008
+ function effectControlChange(effect, key, value, patterns = []) {
15009
+ const [top, ...rest] = key.split(".");
15010
+ if (top === "fill" && rest[0] === "type") return { fill: fillOfType(effect.fill, String(value), patterns) };
15011
+ if (top === "patternId" && !rest.length) {
15012
+ const name = patternOptions(patterns).find((o) => o.value === value)?.label;
15013
+ return name ? { patternId: value, name } : { patternId: value };
15014
+ }
15015
+ if (!rest.length) return { [top]: value };
15016
+ if (top === "fill" && rest[0] === "patternId") {
15017
+ const name = patternOptions(patterns).find((o) => o.value === value)?.label;
15018
+ return { fill: { ...effect.fill, patternId: value, ...name ? { name } : {} } };
15019
+ }
15020
+ return { [top]: setPath(getPath(effect, top), rest, value) };
15021
+ }
15022
+
13922
15023
  // src/image-editor/psd/ui.ts
13923
15024
  var PSD_FILE_ACCEPT = ".psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop";
13924
15025
  function blobBytes(file) {
@@ -13979,6 +15080,9 @@ export {
13979
15080
  DabWalker,
13980
15081
  EMPTY_DISTANCE,
13981
15082
  EMPTY_MIXER_LOAD,
15083
+ FREE_TRANSFORM_CONTENTS,
15084
+ FREE_TRANSFORM_MODES,
15085
+ FreeTransformSession,
13982
15086
  GUIDED_FILTER_EPS,
13983
15087
  HISTOGRAM_BINS,
13984
15088
  HUE_RANGE_LABELS,
@@ -13986,11 +15090,13 @@ export {
13986
15090
  IMAGE_ADJUSTMENT_CONTROLS,
13987
15091
  IMAGE_EDITOR_SCHEMA_VERSION,
13988
15092
  IMAGE_EDITOR_STATE_VERSION,
15093
+ INTERPOLATIONS,
13989
15094
  ImageEditorController,
13990
15095
  ImageEditorRenderer,
13991
15096
  LATEST_PROCESS_VERSION,
13992
15097
  LAYERED_SCHEMA_VERSION,
13993
15098
  LAYER_EFFECT_KINDS,
15099
+ LAYER_EFFECT_MENU,
13994
15100
  LEGACY_PROCESS_VERSION,
13995
15101
  LUMA_709,
13996
15102
  LayerCompositor,
@@ -13999,6 +15105,7 @@ export {
13999
15105
  MAX_BRUSH_SIZE,
14000
15106
  MAX_EFFECT_REACH,
14001
15107
  MAX_KELVIN,
15108
+ MAX_TRANSFORM_PIXELS,
14002
15109
  MIN_KELVIN,
14003
15110
  MIRED_PER_TEMPERATURE_UNIT,
14004
15111
  MIXER_DRAIN,
@@ -14034,8 +15141,12 @@ export {
14034
15141
  TINT_SCALE,
14035
15142
  TONE_LUT_SIZE,
14036
15143
  TONE_RANGE_PRESETS,
15144
+ TRANSFORM_ANCHORS,
14037
15145
  TiledSurface,
15146
+ TransformTooLargeError,
14038
15147
  VIBRANCE_GREY,
15148
+ WARP_STYLES,
15149
+ WARP_STYLE_LABELS,
14039
15150
  XYZ_D50_TO_SRGB,
14040
15151
  ZlibError,
14041
15152
  activeAdjustmentLayers,
@@ -14047,16 +15158,19 @@ export {
14047
15158
  adjustmentNodeAsLayer,
14048
15159
  adjustmentNodeFromLegacy,
14049
15160
  adler32,
15161
+ anchorPoint,
14050
15162
  annotationLayerFor,
14051
15163
  annotationLayerName,
14052
15164
  applyAdjustmentLayers,
14053
15165
  applyAdjustments,
14054
15166
  applyBoxBlur,
14055
15167
  applyDepthMask,
15168
+ applyHomography,
14056
15169
  applyImageProjectionToContext,
14057
15170
  applyLegacyAdjustments,
14058
15171
  applyMatrix3,
14059
15172
  applyOutputStage,
15173
+ applyParams,
14060
15174
  applyProcess2Adjustments,
14061
15175
  applyRegionBlur,
14062
15176
  applyRegionMosaic,
@@ -14074,6 +15188,8 @@ export {
14074
15188
  blurFilter,
14075
15189
  borderCoverage,
14076
15190
  boxMean,
15191
+ boxPointOf,
15192
+ boxQuad,
14077
15193
  boxToCurvePoint,
14078
15194
  bradfordAdaptation,
14079
15195
  buildLayerMaskPlanes,
@@ -14093,12 +15209,14 @@ export {
14093
15209
  colorRange,
14094
15210
  combineSelections,
14095
15211
  compileAdjustmentLayer,
15212
+ compositeSurfaceOver,
14096
15213
  computeHistogram,
14097
15214
  computeSelectionBounds,
14098
15215
  contentBounds,
14099
15216
  contractCoverage,
14100
15217
  controlPosition,
14101
15218
  controlTrackBackground,
15219
+ cornersFromMatrix,
14102
15220
  coverageTileCoord,
14103
15221
  coverageToByte,
14104
15222
  coveringRect,
@@ -14111,6 +15229,7 @@ export {
14111
15229
  createImagePaintTools,
14112
15230
  createImagePsdTools,
14113
15231
  createImageSelectionTools,
15232
+ createImageTransformTools,
14114
15233
  createPigment,
14115
15234
  createPigmentTools,
14116
15235
  createSelectionMask,
@@ -14137,6 +15256,7 @@ export {
14137
15256
  describeLayer,
14138
15257
  describeLayerEffects,
14139
15258
  describeSelection,
15259
+ describeTransform,
14140
15260
  deserializeEffects,
14141
15261
  deserializeLayer,
14142
15262
  deserializeLayers,
@@ -14147,11 +15267,13 @@ export {
14147
15267
  developTarget,
14148
15268
  distanceTransform,
14149
15269
  downloadPsd,
15270
+ dragWarpMeshPoint,
14150
15271
  dropPlacement,
14151
15272
  dropZone,
14152
15273
  duplicateLayer,
14153
15274
  edgeWidth,
14154
15275
  effectColorHex,
15276
+ effectControlChange,
14155
15277
  effectReach,
14156
15278
  effectsFingerprint,
14157
15279
  effectsMargin,
@@ -14164,6 +15286,7 @@ export {
14164
15286
  encodeRle,
14165
15287
  encodeSelectionRle,
14166
15288
  encodeSurface,
15289
+ evalWarpMesh,
14167
15290
  evaluateAdjustmentLayers,
14168
15291
  expandCoverage,
14169
15292
  exportPsd,
@@ -14174,9 +15297,11 @@ export {
14174
15297
  findLayerControl,
14175
15298
  finishTile,
14176
15299
  flattenLayers,
15300
+ flipCorners,
14177
15301
  formatControlValue,
14178
15302
  fullSelection,
14179
15303
  gaussianBlur,
15304
+ geometryHomography,
14180
15305
  getAnnotationBounds,
14181
15306
  getControlPath,
14182
15307
  getCropCursor,
@@ -14191,6 +15316,7 @@ export {
14191
15316
  handlePaintShortcut,
14192
15317
  hasActiveEffects,
14193
15318
  hasResampledLegacyMask,
15319
+ hasWarp,
14194
15320
  hashMaskContent,
14195
15321
  hashNoise,
14196
15322
  hexToRgb,
@@ -14198,6 +15324,8 @@ export {
14198
15324
  hitTestAnnotation,
14199
15325
  hitTestCrop,
14200
15326
  hitTestCurvePoint,
15327
+ homographyFromAffine,
15328
+ homographyFromPoints,
14201
15329
  hslToRgb,
14202
15330
  hueRange,
14203
15331
  hueRangeTables,
@@ -14205,6 +15333,8 @@ export {
14205
15333
  hueSaturation,
14206
15334
  identityCurve,
14207
15335
  identityLevels,
15336
+ identityTransform,
15337
+ identityWarpMesh,
14208
15338
  imageEditorStateMigrations,
14209
15339
  imageToCanvasPoint,
14210
15340
  importPsd,
@@ -14212,12 +15342,15 @@ export {
14212
15342
  inflateRaw,
14213
15343
  insertCurvePoint,
14214
15344
  insertLayer,
15345
+ invertHomography,
14215
15346
  invertMatrix3,
14216
15347
  invertSelection,
14217
15348
  isActiveFilter,
14218
15349
  isAdjustmentKind,
14219
15350
  isBlendMode,
14220
15351
  isDefaultDepthMask,
15352
+ isIdentityTransform,
15353
+ isInterpolation,
14221
15354
  isLayerEffectKind,
14222
15355
  isLayerLocked,
14223
15356
  isLegacyShape,
@@ -14229,11 +15362,17 @@ export {
14229
15362
  isSelectionMode,
14230
15363
  isSeparableBlendMode,
14231
15364
  isSpatialFilter,
15365
+ isTransformAnchor,
15366
+ isValidWarp,
14232
15367
  kelvinToTemperature,
15368
+ keysWeight,
14233
15369
  layerBadge,
15370
+ layerEffectControls,
14234
15371
  layerEffectFields,
14235
15372
  layerEffectLabel,
15373
+ layerEffectsBadge,
14236
15374
  layerEffectsOf,
15375
+ layerEffectsPanel,
14237
15376
  layerLabel,
14238
15377
  layerMaskCoverage,
14239
15378
  layerMaskFromSelection,
@@ -14258,6 +15397,8 @@ export {
14258
15397
  mapCropHandle,
14259
15398
  mapLayer,
14260
15399
  maskSummary,
15400
+ matrixFromCorners,
15401
+ matrixFromParams,
14261
15402
  mergeLayerEffect,
14262
15403
  migrateSerializedImageEditorState,
14263
15404
  migrateToLayeredState,
@@ -14308,6 +15449,8 @@ export {
14308
15449
  polygonCoverage,
14309
15450
  polygonSelection,
14310
15451
  posterizeValue,
15452
+ presetWarp,
15453
+ previewStep,
14311
15454
  projectImagePoint,
14312
15455
  projectImageRect,
14313
15456
  projectMaskCoverage,
@@ -14316,6 +15459,7 @@ export {
14316
15459
  psdFileName,
14317
15460
  psdPatternToEffectPattern,
14318
15461
  psdToLayers,
15462
+ pullWarpMesh,
14319
15463
  quickMaskPixels,
14320
15464
  rangeLightness,
14321
15465
  rasterMaskFromLegacy,
@@ -14325,6 +15469,9 @@ export {
14325
15469
  readPsd,
14326
15470
  readRegion,
14327
15471
  rectSelection,
15472
+ rectToQuad,
15473
+ reduceSurface,
15474
+ referencePoint,
14328
15475
  refineCoverage,
14329
15476
  refineEdgeHalo,
14330
15477
  refineSelection,
@@ -14340,6 +15487,7 @@ export {
14340
15487
  requireUnlocked,
14341
15488
  resizePixels,
14342
15489
  resizeSelection,
15490
+ resizeWarpMesh,
14343
15491
  resolveExternalAssets,
14344
15492
  resolveMosaicRegionBounds,
14345
15493
  rgbToGray,
@@ -14395,6 +15543,12 @@ export {
14395
15543
  toneCurve,
14396
15544
  toneLuts,
14397
15545
  toneRange,
15546
+ transformHomography,
15547
+ transformMap,
15548
+ transformSurface,
15549
+ transformedBounds,
15550
+ transformedBox,
15551
+ transformedPoint,
14398
15552
  translateSurface,
14399
15553
  ungroupLayer,
14400
15554
  uniformTile,
@@ -14411,6 +15565,11 @@ export {
14411
15565
  vibranceLinear,
14412
15566
  vibrancePixel,
14413
15567
  walkDabs,
15568
+ warpMeshEditPoints,
15569
+ warpMeshGridCurves,
15570
+ warpMeshParamAt,
15571
+ warpPresetMesh,
15572
+ warpSurface,
14414
15573
  wbMatrix,
14415
15574
  whiteBalanceFromSample,
14416
15575
  whiteBalanceMatrix,