@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.
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/dist/animation-editor/index.cjs +55 -53
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +3 -2
- package/dist/animation-editor/index.d.ts +3 -2
- package/dist/animation-editor/index.js +11 -9
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/chunk-DWGZUSES.cjs +694 -0
- package/dist/chunk-DWGZUSES.cjs.map +1 -0
- package/dist/chunk-GX332W2C.js +181 -0
- package/dist/chunk-GX332W2C.js.map +1 -0
- package/dist/{chunk-7MD7C6XQ.cjs → chunk-JF5PXN7W.cjs} +96 -744
- package/dist/chunk-JF5PXN7W.cjs.map +1 -0
- package/dist/chunk-KKREERP2.js +694 -0
- package/dist/chunk-KKREERP2.js.map +1 -0
- package/dist/chunk-MUUXNRU4.cjs +181 -0
- package/dist/chunk-MUUXNRU4.cjs.map +1 -0
- package/dist/{chunk-OEVSVA6V.js → chunk-WIR377F6.js} +788 -22
- package/dist/chunk-WIR377F6.js.map +1 -0
- package/dist/{chunk-4AB5BHYK.cjs → chunk-WT6ZAWJP.cjs} +788 -22
- package/dist/chunk-WT6ZAWJP.cjs.map +1 -0
- package/dist/{chunk-4JLB2USO.js → chunk-YGZNGDIT.js} +45 -693
- package/dist/chunk-YGZNGDIT.js.map +1 -0
- package/dist/image-editor/index.cjs +1280 -299
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +304 -5
- package/dist/image-editor/index.d.ts +304 -5
- package/dist/image-editor/index.js +991 -10
- package/dist/image-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +23 -2
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +98 -2
- package/dist/raster/index.d.ts +98 -2
- package/dist/raster/index.js +23 -2
- package/dist/{svg-ymShkNtu.d.cts → svg-C9r284x8.d.cts} +6 -9
- package/dist/{svg-Dh17jCyA.d.ts → svg-MVKBqwD4.d.ts} +6 -9
- package/dist/types-CwMPoRMU.d.cts +40 -0
- package/dist/types-CwMPoRMU.d.ts +40 -0
- package/dist/vector/index.cjs +490 -554
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +154 -292
- package/dist/vector/index.d.ts +154 -292
- package/dist/vector/index.js +91 -155
- package/dist/vector/index.js.map +1 -1
- package/dist/warp-B8rAguzs.d.ts +165 -0
- package/dist/warp-nYs2DPrx.d.cts +165 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +49 -3
- package/dist/chunk-4AB5BHYK.cjs.map +0 -1
- package/dist/chunk-4JLB2USO.js.map +0 -1
- package/dist/chunk-7MD7C6XQ.cjs.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12380
|
-
const mask = controller.setLayerMaskProps(id, {
|
|
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,
|