@pixerate/editor 0.97.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-OEVSVA6V.js → chunk-WIR377F6.js} +788 -22
  19. package/dist/chunk-WIR377F6.js.map +1 -0
  20. package/dist/{chunk-4AB5BHYK.cjs → chunk-WT6ZAWJP.cjs} +788 -22
  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 +1280 -299
  25. package/dist/image-editor/index.cjs.map +1 -1
  26. package/dist/image-editor/index.d.cts +304 -5
  27. package/dist/image-editor/index.d.ts +304 -5
  28. package/dist/image-editor/index.js +991 -10
  29. package/dist/image-editor/index.js.map +1 -1
  30. package/dist/raster/index.cjs +23 -2
  31. package/dist/raster/index.cjs.map +1 -1
  32. package/dist/raster/index.d.cts +98 -2
  33. package/dist/raster/index.d.ts +98 -2
  34. package/dist/raster/index.js +23 -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-4AB5BHYK.cjs.map +0 -1
  50. package/dist/chunk-4JLB2USO.js.map +0 -1
  51. package/dist/chunk-7MD7C6XQ.cjs.map +0 -1
  52. package/dist/chunk-OEVSVA6V.js.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-OEVSVA6V.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;
@@ -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) {
@@ -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) {
@@ -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.`);
@@ -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);
@@ -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) {
@@ -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
  };
@@ -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(),
@@ -14152,6 +15080,9 @@ export {
14152
15080
  DabWalker,
14153
15081
  EMPTY_DISTANCE,
14154
15082
  EMPTY_MIXER_LOAD,
15083
+ FREE_TRANSFORM_CONTENTS,
15084
+ FREE_TRANSFORM_MODES,
15085
+ FreeTransformSession,
14155
15086
  GUIDED_FILTER_EPS,
14156
15087
  HISTOGRAM_BINS,
14157
15088
  HUE_RANGE_LABELS,
@@ -14159,6 +15090,7 @@ export {
14159
15090
  IMAGE_ADJUSTMENT_CONTROLS,
14160
15091
  IMAGE_EDITOR_SCHEMA_VERSION,
14161
15092
  IMAGE_EDITOR_STATE_VERSION,
15093
+ INTERPOLATIONS,
14162
15094
  ImageEditorController,
14163
15095
  ImageEditorRenderer,
14164
15096
  LATEST_PROCESS_VERSION,
@@ -14173,6 +15105,7 @@ export {
14173
15105
  MAX_BRUSH_SIZE,
14174
15106
  MAX_EFFECT_REACH,
14175
15107
  MAX_KELVIN,
15108
+ MAX_TRANSFORM_PIXELS,
14176
15109
  MIN_KELVIN,
14177
15110
  MIRED_PER_TEMPERATURE_UNIT,
14178
15111
  MIXER_DRAIN,
@@ -14208,8 +15141,12 @@ export {
14208
15141
  TINT_SCALE,
14209
15142
  TONE_LUT_SIZE,
14210
15143
  TONE_RANGE_PRESETS,
15144
+ TRANSFORM_ANCHORS,
14211
15145
  TiledSurface,
15146
+ TransformTooLargeError,
14212
15147
  VIBRANCE_GREY,
15148
+ WARP_STYLES,
15149
+ WARP_STYLE_LABELS,
14213
15150
  XYZ_D50_TO_SRGB,
14214
15151
  ZlibError,
14215
15152
  activeAdjustmentLayers,
@@ -14221,16 +15158,19 @@ export {
14221
15158
  adjustmentNodeAsLayer,
14222
15159
  adjustmentNodeFromLegacy,
14223
15160
  adler32,
15161
+ anchorPoint,
14224
15162
  annotationLayerFor,
14225
15163
  annotationLayerName,
14226
15164
  applyAdjustmentLayers,
14227
15165
  applyAdjustments,
14228
15166
  applyBoxBlur,
14229
15167
  applyDepthMask,
15168
+ applyHomography,
14230
15169
  applyImageProjectionToContext,
14231
15170
  applyLegacyAdjustments,
14232
15171
  applyMatrix3,
14233
15172
  applyOutputStage,
15173
+ applyParams,
14234
15174
  applyProcess2Adjustments,
14235
15175
  applyRegionBlur,
14236
15176
  applyRegionMosaic,
@@ -14248,6 +15188,8 @@ export {
14248
15188
  blurFilter,
14249
15189
  borderCoverage,
14250
15190
  boxMean,
15191
+ boxPointOf,
15192
+ boxQuad,
14251
15193
  boxToCurvePoint,
14252
15194
  bradfordAdaptation,
14253
15195
  buildLayerMaskPlanes,
@@ -14267,12 +15209,14 @@ export {
14267
15209
  colorRange,
14268
15210
  combineSelections,
14269
15211
  compileAdjustmentLayer,
15212
+ compositeSurfaceOver,
14270
15213
  computeHistogram,
14271
15214
  computeSelectionBounds,
14272
15215
  contentBounds,
14273
15216
  contractCoverage,
14274
15217
  controlPosition,
14275
15218
  controlTrackBackground,
15219
+ cornersFromMatrix,
14276
15220
  coverageTileCoord,
14277
15221
  coverageToByte,
14278
15222
  coveringRect,
@@ -14285,6 +15229,7 @@ export {
14285
15229
  createImagePaintTools,
14286
15230
  createImagePsdTools,
14287
15231
  createImageSelectionTools,
15232
+ createImageTransformTools,
14288
15233
  createPigment,
14289
15234
  createPigmentTools,
14290
15235
  createSelectionMask,
@@ -14311,6 +15256,7 @@ export {
14311
15256
  describeLayer,
14312
15257
  describeLayerEffects,
14313
15258
  describeSelection,
15259
+ describeTransform,
14314
15260
  deserializeEffects,
14315
15261
  deserializeLayer,
14316
15262
  deserializeLayers,
@@ -14321,6 +15267,7 @@ export {
14321
15267
  developTarget,
14322
15268
  distanceTransform,
14323
15269
  downloadPsd,
15270
+ dragWarpMeshPoint,
14324
15271
  dropPlacement,
14325
15272
  dropZone,
14326
15273
  duplicateLayer,
@@ -14339,6 +15286,7 @@ export {
14339
15286
  encodeRle,
14340
15287
  encodeSelectionRle,
14341
15288
  encodeSurface,
15289
+ evalWarpMesh,
14342
15290
  evaluateAdjustmentLayers,
14343
15291
  expandCoverage,
14344
15292
  exportPsd,
@@ -14349,9 +15297,11 @@ export {
14349
15297
  findLayerControl,
14350
15298
  finishTile,
14351
15299
  flattenLayers,
15300
+ flipCorners,
14352
15301
  formatControlValue,
14353
15302
  fullSelection,
14354
15303
  gaussianBlur,
15304
+ geometryHomography,
14355
15305
  getAnnotationBounds,
14356
15306
  getControlPath,
14357
15307
  getCropCursor,
@@ -14366,6 +15316,7 @@ export {
14366
15316
  handlePaintShortcut,
14367
15317
  hasActiveEffects,
14368
15318
  hasResampledLegacyMask,
15319
+ hasWarp,
14369
15320
  hashMaskContent,
14370
15321
  hashNoise,
14371
15322
  hexToRgb,
@@ -14373,6 +15324,8 @@ export {
14373
15324
  hitTestAnnotation,
14374
15325
  hitTestCrop,
14375
15326
  hitTestCurvePoint,
15327
+ homographyFromAffine,
15328
+ homographyFromPoints,
14376
15329
  hslToRgb,
14377
15330
  hueRange,
14378
15331
  hueRangeTables,
@@ -14380,6 +15333,8 @@ export {
14380
15333
  hueSaturation,
14381
15334
  identityCurve,
14382
15335
  identityLevels,
15336
+ identityTransform,
15337
+ identityWarpMesh,
14383
15338
  imageEditorStateMigrations,
14384
15339
  imageToCanvasPoint,
14385
15340
  importPsd,
@@ -14387,12 +15342,15 @@ export {
14387
15342
  inflateRaw,
14388
15343
  insertCurvePoint,
14389
15344
  insertLayer,
15345
+ invertHomography,
14390
15346
  invertMatrix3,
14391
15347
  invertSelection,
14392
15348
  isActiveFilter,
14393
15349
  isAdjustmentKind,
14394
15350
  isBlendMode,
14395
15351
  isDefaultDepthMask,
15352
+ isIdentityTransform,
15353
+ isInterpolation,
14396
15354
  isLayerEffectKind,
14397
15355
  isLayerLocked,
14398
15356
  isLegacyShape,
@@ -14404,7 +15362,10 @@ export {
14404
15362
  isSelectionMode,
14405
15363
  isSeparableBlendMode,
14406
15364
  isSpatialFilter,
15365
+ isTransformAnchor,
15366
+ isValidWarp,
14407
15367
  kelvinToTemperature,
15368
+ keysWeight,
14408
15369
  layerBadge,
14409
15370
  layerEffectControls,
14410
15371
  layerEffectFields,
@@ -14436,6 +15397,8 @@ export {
14436
15397
  mapCropHandle,
14437
15398
  mapLayer,
14438
15399
  maskSummary,
15400
+ matrixFromCorners,
15401
+ matrixFromParams,
14439
15402
  mergeLayerEffect,
14440
15403
  migrateSerializedImageEditorState,
14441
15404
  migrateToLayeredState,
@@ -14486,6 +15449,8 @@ export {
14486
15449
  polygonCoverage,
14487
15450
  polygonSelection,
14488
15451
  posterizeValue,
15452
+ presetWarp,
15453
+ previewStep,
14489
15454
  projectImagePoint,
14490
15455
  projectImageRect,
14491
15456
  projectMaskCoverage,
@@ -14494,6 +15459,7 @@ export {
14494
15459
  psdFileName,
14495
15460
  psdPatternToEffectPattern,
14496
15461
  psdToLayers,
15462
+ pullWarpMesh,
14497
15463
  quickMaskPixels,
14498
15464
  rangeLightness,
14499
15465
  rasterMaskFromLegacy,
@@ -14503,6 +15469,9 @@ export {
14503
15469
  readPsd,
14504
15470
  readRegion,
14505
15471
  rectSelection,
15472
+ rectToQuad,
15473
+ reduceSurface,
15474
+ referencePoint,
14506
15475
  refineCoverage,
14507
15476
  refineEdgeHalo,
14508
15477
  refineSelection,
@@ -14518,6 +15487,7 @@ export {
14518
15487
  requireUnlocked,
14519
15488
  resizePixels,
14520
15489
  resizeSelection,
15490
+ resizeWarpMesh,
14521
15491
  resolveExternalAssets,
14522
15492
  resolveMosaicRegionBounds,
14523
15493
  rgbToGray,
@@ -14573,6 +15543,12 @@ export {
14573
15543
  toneCurve,
14574
15544
  toneLuts,
14575
15545
  toneRange,
15546
+ transformHomography,
15547
+ transformMap,
15548
+ transformSurface,
15549
+ transformedBounds,
15550
+ transformedBox,
15551
+ transformedPoint,
14576
15552
  translateSurface,
14577
15553
  ungroupLayer,
14578
15554
  uniformTile,
@@ -14589,6 +15565,11 @@ export {
14589
15565
  vibranceLinear,
14590
15566
  vibrancePixel,
14591
15567
  walkDabs,
15568
+ warpMeshEditPoints,
15569
+ warpMeshGridCurves,
15570
+ warpMeshParamAt,
15571
+ warpPresetMesh,
15572
+ warpSurface,
14592
15573
  wbMatrix,
14593
15574
  whiteBalanceFromSample,
14594
15575
  whiteBalanceMatrix,