@pixerate/editor 0.111.0 → 0.113.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 +6 -0
- package/dist/chunk-4KLT7ZYS.cjs +781 -0
- package/dist/chunk-4KLT7ZYS.cjs.map +1 -0
- package/dist/{chunk-HHZ6ZJTW.js → chunk-CZOXTEMN.js} +1772 -456
- package/dist/chunk-CZOXTEMN.js.map +1 -0
- package/dist/chunk-LPYOIVLA.js +781 -0
- package/dist/chunk-LPYOIVLA.js.map +1 -0
- package/dist/{chunk-PHUMLSCW.cjs → chunk-YGIMK4NX.cjs} +1810 -494
- package/dist/chunk-YGIMK4NX.cjs.map +1 -0
- package/dist/{export-DoDhCszr.d.cts → export-CEXEzqBf.d.cts} +150 -18
- package/dist/{export-DBNVwb6h.d.ts → export-DIkoK8-r.d.ts} +150 -18
- package/dist/image-editor/index.cjs +1922 -788
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +233 -9
- package/dist/image-editor/index.d.ts +233 -9
- package/dist/image-editor/index.js +1208 -74
- package/dist/image-editor/index.js.map +1 -1
- package/dist/paint/index.cjs +61 -2
- package/dist/paint/index.cjs.map +1 -1
- package/dist/paint/index.d.cts +29 -2
- package/dist/paint/index.d.ts +29 -2
- package/dist/paint/index.js +60 -1
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.d.cts +3 -3
- package/dist/paint-editor/index.d.ts +3 -3
- package/dist/paint-editor/index.js +1 -1
- package/dist/{blending-DXl06NSx.d.ts → retouch-BXmeSCaB.d.ts} +218 -1
- package/dist/{blending-aOE6Y3UE.d.cts → retouch-CEIKR22W.d.cts} +218 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +30 -6
- package/skills/pixerate-image-editor/references/retouching.md +36 -0
- package/skills/pixerate-vector/SKILL.md +102 -5
- package/dist/chunk-FTBZPI77.cjs +0 -1
- package/dist/chunk-FTBZPI77.cjs.map +0 -1
- package/dist/chunk-HHZ6ZJTW.js.map +0 -1
- package/dist/chunk-PHUMLSCW.cjs.map +0 -1
- package/dist/chunk-VKK5VS2T.js +0 -1
- package/dist/chunk-VKK5VS2T.js.map +0 -1
|
@@ -4,7 +4,37 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
createCaptureTool
|
|
6
6
|
} from "../chunk-REOJJIY3.js";
|
|
7
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_RETOUCH,
|
|
9
|
+
RETOUCH_SAMPLES,
|
|
10
|
+
RegionPixelSource,
|
|
11
|
+
RetouchDabStroke,
|
|
12
|
+
SPOT_HEAL_TYPES,
|
|
13
|
+
TONE_RANGES,
|
|
14
|
+
TilePixelSource,
|
|
15
|
+
applyRetouchToTile,
|
|
16
|
+
bestOffset,
|
|
17
|
+
dilateMask,
|
|
18
|
+
dodgeBurn,
|
|
19
|
+
dodgeBurnCurve,
|
|
20
|
+
healPixels,
|
|
21
|
+
luma,
|
|
22
|
+
maskBounds,
|
|
23
|
+
membraneFill,
|
|
24
|
+
normalizeRetouch,
|
|
25
|
+
perDabStrength,
|
|
26
|
+
readSourceRegion,
|
|
27
|
+
rgbaViewOfGrey,
|
|
28
|
+
saturationOf,
|
|
29
|
+
seamlessClone,
|
|
30
|
+
smudgeEffect,
|
|
31
|
+
solveMembrane,
|
|
32
|
+
sponge,
|
|
33
|
+
spongeEffect,
|
|
34
|
+
spotHealPixels,
|
|
35
|
+
spotHealWindow,
|
|
36
|
+
toneOf
|
|
37
|
+
} from "../chunk-LPYOIVLA.js";
|
|
8
38
|
import {
|
|
9
39
|
ADJUSTMENT_KEYS,
|
|
10
40
|
ADJUSTMENT_KINDS,
|
|
@@ -369,7 +399,7 @@ import {
|
|
|
369
399
|
writePsd,
|
|
370
400
|
xyToTempTint,
|
|
371
401
|
xyToUv
|
|
372
|
-
} from "../chunk-
|
|
402
|
+
} from "../chunk-CZOXTEMN.js";
|
|
373
403
|
import {
|
|
374
404
|
DEFAULT_GLOBAL_LIGHT,
|
|
375
405
|
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
@@ -1065,6 +1095,45 @@ var ImageEditorRenderer = class {
|
|
|
1065
1095
|
}
|
|
1066
1096
|
ctx.restore();
|
|
1067
1097
|
}
|
|
1098
|
+
if (options.textEdit) {
|
|
1099
|
+
const t = options.textEdit;
|
|
1100
|
+
ctx.save();
|
|
1101
|
+
try {
|
|
1102
|
+
const scr = (p) => projectImagePoint(p, projection);
|
|
1103
|
+
const poly = (pts) => {
|
|
1104
|
+
ctx.beginPath();
|
|
1105
|
+
pts.map(scr).forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
|
|
1106
|
+
ctx.closePath();
|
|
1107
|
+
};
|
|
1108
|
+
if (t.frame) {
|
|
1109
|
+
poly(t.frame);
|
|
1110
|
+
ctx.setLineDash([4, 3]);
|
|
1111
|
+
ctx.lineWidth = 1;
|
|
1112
|
+
ctx.strokeStyle = "rgba(99,102,241,0.9)";
|
|
1113
|
+
ctx.stroke();
|
|
1114
|
+
ctx.setLineDash([]);
|
|
1115
|
+
}
|
|
1116
|
+
ctx.fillStyle = "rgba(99,102,241,0.35)";
|
|
1117
|
+
for (const q of t.selection) {
|
|
1118
|
+
poly(q);
|
|
1119
|
+
ctx.fill();
|
|
1120
|
+
}
|
|
1121
|
+
if (t.caret) {
|
|
1122
|
+
const [a, b] = t.caret.map(scr);
|
|
1123
|
+
ctx.beginPath();
|
|
1124
|
+
ctx.moveTo(a.x, a.y);
|
|
1125
|
+
ctx.lineTo(b.x, b.y);
|
|
1126
|
+
ctx.lineWidth = 3;
|
|
1127
|
+
ctx.strokeStyle = "#ffffff";
|
|
1128
|
+
ctx.stroke();
|
|
1129
|
+
ctx.lineWidth = 1.5;
|
|
1130
|
+
ctx.strokeStyle = "#111827";
|
|
1131
|
+
ctx.stroke();
|
|
1132
|
+
}
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1135
|
+
ctx.restore();
|
|
1136
|
+
}
|
|
1068
1137
|
if (options.freeTransform) {
|
|
1069
1138
|
const t = options.freeTransform;
|
|
1070
1139
|
ctx.save();
|
|
@@ -2581,7 +2650,7 @@ function createOutlineTextShaper(fonts, options = {}) {
|
|
|
2581
2650
|
return { face: e.face, family, key: family.toLowerCase(), weight, italic, postScript };
|
|
2582
2651
|
});
|
|
2583
2652
|
const fallbackKey = (options.fallback ?? entries[0].family).toLowerCase();
|
|
2584
|
-
const
|
|
2653
|
+
const pick3 = (font) => {
|
|
2585
2654
|
if (font.postScriptName) {
|
|
2586
2655
|
const exact = entries.find((e) => e.postScript === font.postScriptName);
|
|
2587
2656
|
if (exact) return { entry: exact, available: true };
|
|
@@ -2609,22 +2678,22 @@ function createOutlineTextShaper(fonts, options = {}) {
|
|
|
2609
2678
|
return {
|
|
2610
2679
|
id: options.id ?? `outline:${entries.map((e) => `${e.family}/${e.weight}${e.italic ? "i" : ""}`).join(",")}`,
|
|
2611
2680
|
resolve(font) {
|
|
2612
|
-
const { entry, available } =
|
|
2681
|
+
const { entry, available } = pick3(font);
|
|
2613
2682
|
return { family: entry.family, available };
|
|
2614
2683
|
},
|
|
2615
2684
|
metrics(font) {
|
|
2616
|
-
const { face } =
|
|
2685
|
+
const { face } = pick3(font).entry;
|
|
2617
2686
|
const k = font.size / face.unitsPerEm;
|
|
2618
2687
|
return { ascent: face.ascender * k, descent: -face.descender * k };
|
|
2619
2688
|
},
|
|
2620
2689
|
measure(text, font) {
|
|
2621
|
-
const { face } =
|
|
2690
|
+
const { face } = pick3(font).entry;
|
|
2622
2691
|
const out = [0];
|
|
2623
2692
|
for (let i = 1; i <= text.length; i++) out.push(face.getAdvanceWidth(text.slice(0, i), font.size, { kerning: true }));
|
|
2624
2693
|
return out;
|
|
2625
2694
|
},
|
|
2626
2695
|
outline(text, font, x, y) {
|
|
2627
|
-
return toPath(
|
|
2696
|
+
return toPath(pick3(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);
|
|
2628
2697
|
},
|
|
2629
2698
|
families() {
|
|
2630
2699
|
return [...new Set(entries.map((e) => e.family))];
|
|
@@ -3114,6 +3183,9 @@ var FreeTransformSession = class {
|
|
|
3114
3183
|
|
|
3115
3184
|
// src/image-editor/controller.ts
|
|
3116
3185
|
var IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;
|
|
3186
|
+
var RETOUCH_TOOLS = ["clone", "heal", "spot-heal", "smudge", "dodge", "burn", "sponge"];
|
|
3187
|
+
var isRetouchTool = (tool) => RETOUCH_TOOLS.includes(tool);
|
|
3188
|
+
var STROKE_TOOLS = ["brush", "eraser", "mixer", ...RETOUCH_TOOLS];
|
|
3117
3189
|
function paintCoverageInto(base, coverage, apply, rects) {
|
|
3118
3190
|
const tiles = new Map(base.tiles);
|
|
3119
3191
|
const done = /* @__PURE__ */ new Set();
|
|
@@ -3147,6 +3219,18 @@ var PaintStrokeSession = class {
|
|
|
3147
3219
|
if (stroke instanceof StrokeCoverage) {
|
|
3148
3220
|
this.stroke = stroke;
|
|
3149
3221
|
this.mixer = null;
|
|
3222
|
+
} else if ("kind" in stroke && stroke.kind === "coverage") {
|
|
3223
|
+
this.stroke = stroke.coverage;
|
|
3224
|
+
this.mixer = null;
|
|
3225
|
+
this.retouch = stroke;
|
|
3226
|
+
} else if ("kind" in stroke) {
|
|
3227
|
+
this.stroke = null;
|
|
3228
|
+
this.retouch = stroke;
|
|
3229
|
+
if (this.base.channels === 1) {
|
|
3230
|
+
const view = rgbaViewOfGrey(this.mixerPixels());
|
|
3231
|
+
this.mixer = stroke.create(view.target);
|
|
3232
|
+
this.flushView = view.flush;
|
|
3233
|
+
} else this.mixer = stroke.create(this.mixerPixels());
|
|
3150
3234
|
} else {
|
|
3151
3235
|
this.stroke = null;
|
|
3152
3236
|
this.mixer = stroke.create(this.mixerPixels());
|
|
@@ -3163,10 +3247,20 @@ var PaintStrokeSession = class {
|
|
|
3163
3247
|
stroke;
|
|
3164
3248
|
mixer;
|
|
3165
3249
|
mixerInit = null;
|
|
3250
|
+
retouch = null;
|
|
3251
|
+
/** Writes a mask's RGBA view back (sequential retouching on a mask). */
|
|
3252
|
+
flushView = null;
|
|
3253
|
+
started = false;
|
|
3166
3254
|
/** Mixer tiles written since the last update: they get new tile objects so the compositor sees them change. */
|
|
3167
3255
|
fresh = /* @__PURE__ */ new Set();
|
|
3168
3256
|
last = null;
|
|
3169
3257
|
closed = false;
|
|
3258
|
+
/** The new tile of a coverage stroke over the pre-stroke tile `src`. */
|
|
3259
|
+
compositeTile(src, tx, ty, options = {}, paint) {
|
|
3260
|
+
const r = this.retouch;
|
|
3261
|
+
if (r && r.kind === "coverage") return applyRetouchToTile(this.stroke.coverage, src, this.base.channels, tx, ty, paint ?? r.paint(), r.apply, options);
|
|
3262
|
+
return applyStrokeToTile(this.stroke.coverage, src, this.base.channels, tx, ty, this.apply, options);
|
|
3263
|
+
}
|
|
3170
3264
|
/** The live pixels a mixer stroke reads and writes; base tiles are copied on first write. */
|
|
3171
3265
|
mixerPixels() {
|
|
3172
3266
|
const a = this.apply;
|
|
@@ -3193,8 +3287,14 @@ var PaintStrokeSession = class {
|
|
|
3193
3287
|
/** Adds input points (document pixels); the stack shows the stroke so far. */
|
|
3194
3288
|
push(points) {
|
|
3195
3289
|
if (this.closed || !points.length) return;
|
|
3290
|
+
if (!this.started) {
|
|
3291
|
+
this.started = true;
|
|
3292
|
+
if (this.retouch?.kind === "coverage") this.retouch.start(points[0]);
|
|
3293
|
+
}
|
|
3196
3294
|
this.last = points[points.length - 1];
|
|
3197
|
-
this.
|
|
3295
|
+
const rects = this.mixer ? this.mixer.push(points) : this.stroke.push(points);
|
|
3296
|
+
this.flushView?.();
|
|
3297
|
+
this.update(rects);
|
|
3198
3298
|
}
|
|
3199
3299
|
update(rects) {
|
|
3200
3300
|
if (!rects.length) return;
|
|
@@ -3218,7 +3318,7 @@ var PaintStrokeSession = class {
|
|
|
3218
3318
|
const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);
|
|
3219
3319
|
const prev = this.touched.has(key) ? this.live.get(key)?.data : void 0;
|
|
3220
3320
|
this.touched.add(key);
|
|
3221
|
-
const data =
|
|
3321
|
+
const data = this.compositeTile(src, tx, ty, prev ? { into: prev, region: r } : {});
|
|
3222
3322
|
this.live.set(key, { data, uniform: null });
|
|
3223
3323
|
}
|
|
3224
3324
|
this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurface(ch, new Map(this.live), this.base.defaultValue), rects);
|
|
@@ -3227,9 +3327,22 @@ var PaintStrokeSession = class {
|
|
|
3227
3327
|
end() {
|
|
3228
3328
|
if (this.closed) throw new LayerOperationError("This stroke has already ended.");
|
|
3229
3329
|
const last = this.last ?? void 0;
|
|
3230
|
-
this.
|
|
3330
|
+
const rects = this.mixer ? this.mixer.finish(last) : this.stroke.finish(last);
|
|
3331
|
+
this.flushView?.();
|
|
3332
|
+
this.update(rects);
|
|
3231
3333
|
this.closed = true;
|
|
3232
3334
|
const ch = this.base.channels;
|
|
3335
|
+
const r = this.retouch;
|
|
3336
|
+
if (r?.kind === "coverage" && r.finalPaint) {
|
|
3337
|
+
const final = r.finalPaint(this.stroke.coverage);
|
|
3338
|
+
if (final) {
|
|
3339
|
+
for (const key of this.touched) {
|
|
3340
|
+
const { tx, ty } = tileCoord(key);
|
|
3341
|
+
const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);
|
|
3342
|
+
this.live.set(key, { data: this.compositeTile(src, tx, ty, {}, final), uniform: null });
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3233
3346
|
const tiles = new Map(this.base.tiles);
|
|
3234
3347
|
let changed = false;
|
|
3235
3348
|
for (const key of this.touched) {
|
|
@@ -3241,11 +3354,12 @@ var PaintStrokeSession = class {
|
|
|
3241
3354
|
if (!sameTile(prev, tile)) changed = true;
|
|
3242
3355
|
}
|
|
3243
3356
|
const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;
|
|
3244
|
-
const mixerLoad = this.
|
|
3357
|
+
const mixerLoad = this.mixerInit ? this.mixerInit.end(this.mixer.load) : void 0;
|
|
3245
3358
|
this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);
|
|
3246
3359
|
const b = this.mixer ? this.mixer.bounds : this.stroke.coverage.bounds;
|
|
3247
3360
|
const result = { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: (this.mixer ?? this.stroke).dabCount };
|
|
3248
3361
|
if (mixerLoad) result.mixerLoad = mixerLoad;
|
|
3362
|
+
if (r?.result) Object.assign(result, r.result());
|
|
3249
3363
|
return result;
|
|
3250
3364
|
}
|
|
3251
3365
|
/** Abandons the stroke and restores the layer. */
|
|
@@ -3384,7 +3498,7 @@ var ImageEditorController = class {
|
|
|
3384
3498
|
activeLayerId: null,
|
|
3385
3499
|
activeTarget: "pixels",
|
|
3386
3500
|
outputOpacity: 1,
|
|
3387
|
-
paint: { brush: { ...DEFAULT_BRUSH }, foreground: "#000000", background: "#ffffff", mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD },
|
|
3501
|
+
paint: { brush: { ...DEFAULT_BRUSH }, foreground: "#000000", background: "#ffffff", mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD, retouch: { ...DEFAULT_RETOUCH }, cloneSource: null },
|
|
3388
3502
|
fit: options.fit ?? "margin"
|
|
3389
3503
|
};
|
|
3390
3504
|
if (options.image instanceof Object && "width" in options.image) {
|
|
@@ -4979,12 +5093,16 @@ var ImageEditorController = class {
|
|
|
4979
5093
|
/** Brush settings, colours and tool options of the paint tools (view state, no undo). */
|
|
4980
5094
|
setPaintOptions(options) {
|
|
4981
5095
|
const cur = this.state.paint;
|
|
5096
|
+
const retouch = options.retouch ? normalizeRetouch(options.retouch, cur.retouch) : cur.retouch;
|
|
5097
|
+
const cloneSource = cur.cloneSource && retouch.aligned !== cur.retouch.aligned ? { ...cur.cloneSource, offset: null } : cur.cloneSource;
|
|
4982
5098
|
const next = {
|
|
4983
5099
|
brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,
|
|
4984
5100
|
foreground: options.foreground !== void 0 ? toHex(parseColor(options.foreground)) : cur.foreground,
|
|
4985
5101
|
background: options.background !== void 0 ? toHex(parseColor(options.background)) : cur.background,
|
|
4986
5102
|
mixer: options.mixer ? normalizeMixer({ ...cur.mixer, ...options.mixer }) : cur.mixer,
|
|
4987
|
-
mixerLoad: cur.mixerLoad
|
|
5103
|
+
mixerLoad: cur.mixerLoad,
|
|
5104
|
+
retouch,
|
|
5105
|
+
cloneSource
|
|
4988
5106
|
};
|
|
4989
5107
|
this.state.paint = next;
|
|
4990
5108
|
this.notify();
|
|
@@ -5014,6 +5132,17 @@ var ImageEditorController = class {
|
|
|
5014
5132
|
this.notify();
|
|
5015
5133
|
return EMPTY_MIXER_LOAD;
|
|
5016
5134
|
}
|
|
5135
|
+
/**
|
|
5136
|
+
* Sets (Alt-click) or clears the Clone Stamp and Healing Brush source point, in document
|
|
5137
|
+
* pixels. The next stroke fixes the offset from its first point. View state, no undo.
|
|
5138
|
+
*/
|
|
5139
|
+
setCloneSource(point) {
|
|
5140
|
+
if (point && (!Number.isFinite(point.x) || !Number.isFinite(point.y))) throw new LayerOperationError("The clone source needs finite x and y (document pixels).");
|
|
5141
|
+
const source = point ? { x: Math.round(point.x), y: Math.round(point.y), offset: null } : null;
|
|
5142
|
+
this.state.paint = { ...this.state.paint, cloneSource: source };
|
|
5143
|
+
this.notify();
|
|
5144
|
+
return source;
|
|
5145
|
+
}
|
|
5017
5146
|
/** D: black foreground, white background. */
|
|
5018
5147
|
resetPaintColors() {
|
|
5019
5148
|
this.setPaintOptions({ foreground: "#000000", background: "#ffffff" });
|
|
@@ -5068,8 +5197,8 @@ var ImageEditorController = class {
|
|
|
5068
5197
|
*/
|
|
5069
5198
|
beginStroke(init = {}) {
|
|
5070
5199
|
const tool = init.tool ?? "brush";
|
|
5071
|
-
if (tool
|
|
5072
|
-
const target = this.paintTarget(init.layerId, init.channel, tool === "eraser" ? "erase" : "paint");
|
|
5200
|
+
if (!STROKE_TOOLS.includes(tool)) throw new LayerOperationError(`Unknown paint tool "${String(tool)}"; use one of ${STROKE_TOOLS.join(", ")}.`);
|
|
5201
|
+
const target = this.paintTarget(init.layerId, init.channel, tool === "eraser" ? "erase" : isRetouchTool(tool) ? "retouch" : "paint");
|
|
5073
5202
|
if (tool === "mixer" && target.channel !== "pixels") {
|
|
5074
5203
|
throw new LayerOperationError("The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.");
|
|
5075
5204
|
}
|
|
@@ -5091,8 +5220,101 @@ var ImageEditorController = class {
|
|
|
5091
5220
|
offset: target.offset
|
|
5092
5221
|
};
|
|
5093
5222
|
if (tool === "mixer") return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply);
|
|
5223
|
+
if (isRetouchTool(tool)) return new PaintStrokeSession(this, target, this.retouchStroke(tool, init, brush, target, apply), apply);
|
|
5094
5224
|
return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);
|
|
5095
5225
|
}
|
|
5226
|
+
/** A retouching stroke's start: its settings, what it samples and how it composites. */
|
|
5227
|
+
retouchStroke(tool, init, brush, target, apply) {
|
|
5228
|
+
const settings = normalizeRetouch(init.retouch ?? {}, this.state.paint.retouch);
|
|
5229
|
+
const mask = target.channel !== "pixels";
|
|
5230
|
+
if (tool === "sponge" && mask) throw new LayerOperationError("The Sponge changes saturation, and a mask has none; target pixels (channel pixels), or use dodge or burn on the mask.");
|
|
5231
|
+
if (tool === "smudge") return { kind: "dabs", create: (pixels) => new RetouchDabStroke(brush, pixels, smudgeEffect(brush, settings.strength)) };
|
|
5232
|
+
if (tool === "sponge") return { kind: "dabs", create: (pixels) => new RetouchDabStroke(brush, pixels, spongeEffect(brush, settings.spongeMode === "saturate", settings.vibrance)) };
|
|
5233
|
+
const retouchApply = {
|
|
5234
|
+
opacity: tool === "clone" || tool === "heal" ? brush.opacity : 1,
|
|
5235
|
+
blend: tool === "clone" && !mask ? brush.blend : "normal",
|
|
5236
|
+
lockTransparency: target.lockTransparency,
|
|
5237
|
+
selection: apply.selection,
|
|
5238
|
+
offset: target.offset
|
|
5239
|
+
};
|
|
5240
|
+
const coverage = new StrokeCoverage(brush);
|
|
5241
|
+
if (tool === "dodge" || tool === "burn") {
|
|
5242
|
+
const paint2 = { kind: "tone", tone: toneOf(tool, settings) };
|
|
5243
|
+
return { kind: "coverage", coverage, apply: retouchApply, start: () => {
|
|
5244
|
+
}, paint: () => paint2 };
|
|
5245
|
+
}
|
|
5246
|
+
const sample = mask ? "current" : settings.sample;
|
|
5247
|
+
const own = this.surfaceSource(target.surface, target.offset);
|
|
5248
|
+
const source = sample === "current" ? own : this.compositeSource(sample === "all" ? null : target.id);
|
|
5249
|
+
const pad = (r, m) => ({ x: r.x - m, y: r.y - m, width: r.width + 2 * m, height: r.height + 2 * m });
|
|
5250
|
+
const covered = (cov, r) => {
|
|
5251
|
+
const m = new Uint8Array(r.width * r.height);
|
|
5252
|
+
for (let y = 0; y < r.height; y++) for (let x = 0; x < r.width; x++) m[y * r.width + x] = cov.get(r.x + x, r.y + y) > 0 ? 1 : 0;
|
|
5253
|
+
return m;
|
|
5254
|
+
};
|
|
5255
|
+
if (tool === "spot-heal") {
|
|
5256
|
+
const { width, height } = this.naturalSize;
|
|
5257
|
+
let patch = null;
|
|
5258
|
+
const trail = { kind: "color", color: [0, 0, 0, 96] };
|
|
5259
|
+
return {
|
|
5260
|
+
kind: "coverage",
|
|
5261
|
+
coverage,
|
|
5262
|
+
apply: retouchApply,
|
|
5263
|
+
start: () => {
|
|
5264
|
+
},
|
|
5265
|
+
paint: () => trail,
|
|
5266
|
+
finalPaint: (cov) => {
|
|
5267
|
+
if (!cov.bounds) return null;
|
|
5268
|
+
const win = spotHealWindow(cov.bounds, { width, height });
|
|
5269
|
+
if (win.width <= 0 || win.height <= 0) return null;
|
|
5270
|
+
const img = readSourceRegion(source, win);
|
|
5271
|
+
const hole = covered(cov, win);
|
|
5272
|
+
const healed = spotHealPixels(win.width, win.height, img, hole, brush.size, settings.spotType, (off) => {
|
|
5273
|
+
patch = off ? { x: off[0], y: off[1] } : null;
|
|
5274
|
+
});
|
|
5275
|
+
return { kind: "source", source: new RegionPixelSource(win, healed), dx: 0, dy: 0 };
|
|
5276
|
+
},
|
|
5277
|
+
result: () => ({ spotHeal: { offset: patch } })
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
5280
|
+
if (init.source) this.setCloneSource(init.source);
|
|
5281
|
+
const cs = this.state.paint.cloneSource;
|
|
5282
|
+
if (!cs) throw new LayerOperationError(`The ${tool === "clone" ? "Clone Stamp" : "Healing Brush"} needs a source point; Alt-click the canvas, call set_clone_source, or pass source { x, y }.`);
|
|
5283
|
+
let offset = { x: 0, y: 0 };
|
|
5284
|
+
let first = { x: 0, y: 0 };
|
|
5285
|
+
const paint = () => ({ kind: "source", source, dx: offset.x, dy: offset.y });
|
|
5286
|
+
return {
|
|
5287
|
+
kind: "coverage",
|
|
5288
|
+
coverage,
|
|
5289
|
+
apply: retouchApply,
|
|
5290
|
+
start: (p) => {
|
|
5291
|
+
first = { x: Math.round(p.x), y: Math.round(p.y) };
|
|
5292
|
+
offset = settings.aligned && cs.offset ? { ...cs.offset } : { x: cs.x - first.x, y: cs.y - first.y };
|
|
5293
|
+
if (settings.aligned) this.state.paint = { ...this.state.paint, cloneSource: { x: cs.x, y: cs.y, offset: { ...offset } } };
|
|
5294
|
+
},
|
|
5295
|
+
paint,
|
|
5296
|
+
finalPaint: tool === "heal" ? (cov) => {
|
|
5297
|
+
if (!cov.bounds) return null;
|
|
5298
|
+
const g = pad(cov.bounds, 2);
|
|
5299
|
+
const src = readSourceRegion(source, g, offset.x, offset.y);
|
|
5300
|
+
const dst = readSourceRegion(source, g);
|
|
5301
|
+
return { kind: "source", source: new RegionPixelSource(g, healPixels(g.width, g.height, src, dst, covered(cov, g))), dx: 0, dy: 0 };
|
|
5302
|
+
} : void 0,
|
|
5303
|
+
result: () => ({ clone: { offset: { ...offset }, source: { x: first.x + offset.x, y: first.y + offset.y }, aligned: settings.aligned } })
|
|
5304
|
+
};
|
|
5305
|
+
}
|
|
5306
|
+
/** A pixel source over a surface (layer pixels or a mask) in layer space at `offset`. */
|
|
5307
|
+
surfaceSource(surface, offset) {
|
|
5308
|
+
return new TilePixelSource((tx, ty) => surface.tiles.get(tileKey(tx, ty))?.data ?? (surface.defaultValue ? defaultTile(surface) : null), surface.channels, offset);
|
|
5309
|
+
}
|
|
5310
|
+
/** A pixel source over the composite of the current layers (through `throughId` and below, or all of them), as it is now. */
|
|
5311
|
+
compositeSource(throughId) {
|
|
5312
|
+
const stack = withoutAnnotations(this.state.layers);
|
|
5313
|
+
const layers = throughId ? layersThrough(stack, throughId) : stack;
|
|
5314
|
+
const compositor = new LayerCompositor();
|
|
5315
|
+
const sources = this.compositorSources();
|
|
5316
|
+
return new TilePixelSource((tx, ty) => compositor.tile(layers, tx, ty, sources), 4);
|
|
5317
|
+
}
|
|
5096
5318
|
/** A Mixer Brush stroke's start: the settings, and the load (refilled when asked or set to). */
|
|
5097
5319
|
mixerStroke(init, brush) {
|
|
5098
5320
|
const settings = normalizeMixer({ ...this.state.paint.mixer, ...init.mixer ?? {} });
|
|
@@ -6475,7 +6697,7 @@ var ImageEditorController = class {
|
|
|
6475
6697
|
s.sourceUrl = null;
|
|
6476
6698
|
s.imageDimensions = { width: result.width, height: result.height };
|
|
6477
6699
|
if (!this.samplePixelsOverride) this.samplePixels = null;
|
|
6478
|
-
s.layers = result
|
|
6700
|
+
s.layers = this.checkPsdFonts(result);
|
|
6479
6701
|
s.crop = null;
|
|
6480
6702
|
s.transform = { ...DEFAULT_TRANSFORM_VALUES };
|
|
6481
6703
|
s.outputOpacity = 1;
|
|
@@ -6500,6 +6722,30 @@ var ImageEditorController = class {
|
|
|
6500
6722
|
summary: result.summary
|
|
6501
6723
|
};
|
|
6502
6724
|
}
|
|
6725
|
+
/**
|
|
6726
|
+
* Type layers whose fonts the shaper lacks: they keep Photoshop's pixels, are marked with their
|
|
6727
|
+
* `missingFonts`, and a warning names them (an edit draws them with a fallback).
|
|
6728
|
+
*/
|
|
6729
|
+
checkPsdFonts(result) {
|
|
6730
|
+
const shaper = this.getTextShaper();
|
|
6731
|
+
const types = flattenLayers(result.layers).filter((l) => l.type === "type");
|
|
6732
|
+
if (!types.length) return result.layers;
|
|
6733
|
+
let layers = result.layers;
|
|
6734
|
+
const missing = /* @__PURE__ */ new Map();
|
|
6735
|
+
for (const l of types) {
|
|
6736
|
+
const fonts = shaper ? layoutText(l, shaper).missingFonts : [...new Set(l.runs.map((r) => r.style.fontFamily))];
|
|
6737
|
+
if (!fonts.length) continue;
|
|
6738
|
+
layers = mapLayer(layers, l.id, () => ({ ...l, missingFonts: fonts }));
|
|
6739
|
+
for (const f of fonts) missing.set(f, [...missing.get(f) ?? [], l.name]);
|
|
6740
|
+
}
|
|
6741
|
+
for (const [font, names] of missing) {
|
|
6742
|
+
result.warnings.push(
|
|
6743
|
+
shaper ? `The font "${font}" is missing here: the type shows Photoshop's pixels until it is edited, then a fallback font (${names.length === 1 ? "layer" : "layers"} ${names.map((n) => `"${n}"`).join(", ")}).` : `Type layers show Photoshop's pixels; nothing here can draw text, so they cannot be edited (${names.length === 1 ? "layer" : "layers"} ${names.map((n) => `"${n}"`).join(", ")}).`
|
|
6744
|
+
);
|
|
6745
|
+
if (!shaper) break;
|
|
6746
|
+
}
|
|
6747
|
+
return layers;
|
|
6748
|
+
}
|
|
6503
6749
|
/**
|
|
6504
6750
|
* The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment
|
|
6505
6751
|
* layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked
|
|
@@ -6578,8 +6824,8 @@ var ImageEditorController = class {
|
|
|
6578
6824
|
return out2;
|
|
6579
6825
|
}
|
|
6580
6826
|
if (layer.type === "shape" || layer.type === "type") {
|
|
6581
|
-
|
|
6582
|
-
const out2 =
|
|
6827
|
+
if (layer.type === "type" && layer.missingFonts?.length) add2(`Type layers drawn with a fallback for missing fonts (${layer.missingFonts.join(", ")}) keep the font names, so Photoshop may draw them differently`, layer.name);
|
|
6828
|
+
const out2 = { ...layer };
|
|
6583
6829
|
const mask2 = bakeMask(layer);
|
|
6584
6830
|
if (mask2) out2.mask = mask2;
|
|
6585
6831
|
else delete out2.mask;
|
|
@@ -6970,6 +7216,27 @@ function collectStateTiles(layers, into) {
|
|
|
6970
7216
|
}
|
|
6971
7217
|
}
|
|
6972
7218
|
}
|
|
7219
|
+
function layersThrough(layers, id) {
|
|
7220
|
+
const walk = (list) => {
|
|
7221
|
+
const out = [];
|
|
7222
|
+
for (const l of list) {
|
|
7223
|
+
if (l.id === id) {
|
|
7224
|
+
out.push(l);
|
|
7225
|
+
return { out, found: true };
|
|
7226
|
+
}
|
|
7227
|
+
if (l.type === "group") {
|
|
7228
|
+
const sub2 = walk(l.children);
|
|
7229
|
+
if (sub2.found) {
|
|
7230
|
+
out.push({ ...l, children: sub2.out });
|
|
7231
|
+
return { out, found: true };
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
out.push(l);
|
|
7235
|
+
}
|
|
7236
|
+
return { out, found: false };
|
|
7237
|
+
};
|
|
7238
|
+
return walk(layers).out;
|
|
7239
|
+
}
|
|
6973
7240
|
function pixelsOnly(layers) {
|
|
6974
7241
|
const out = [];
|
|
6975
7242
|
for (const l of layers) {
|
|
@@ -8992,7 +9259,7 @@ function createImageEffectTools(controller) {
|
|
|
8992
9259
|
const add2 = {
|
|
8993
9260
|
name: "add_layer_effect",
|
|
8994
9261
|
title: "Add a layer effect",
|
|
8995
|
-
description: "Adds a live effect to a pixel, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
|
|
9262
|
+
description: "Adds a live effect to a pixel, shape, type, smart object, annotation or group layer with Photoshop's defaults, changed by settings: drop-shadow, inner-shadow, outer-glow, inner-glow, bevel-emboss, satin, color-overlay, gradient-overlay, pattern-overlay or stroke. Several of one kind are allowed (the first listed is on top). One undo step. Returns the new effect's id and the layer's effects.",
|
|
8996
9263
|
inputSchema: {
|
|
8997
9264
|
type: "object",
|
|
8998
9265
|
$defs: DEFS,
|
|
@@ -9112,9 +9379,73 @@ var PAINT_TOOLS = [
|
|
|
9112
9379
|
{ id: "move", label: "Move", shortcut: "V" },
|
|
9113
9380
|
{ id: "brush", label: "Brush", shortcut: "B" },
|
|
9114
9381
|
{ id: "mixer", label: "Mixer", shortcut: "Shift+B" },
|
|
9115
|
-
{ id: "eraser", label: "Eraser", shortcut: "E" }
|
|
9382
|
+
{ id: "eraser", label: "Eraser", shortcut: "E" },
|
|
9383
|
+
{ id: "clone", label: "Clone", shortcut: "S" },
|
|
9384
|
+
{ id: "heal", label: "Heal", shortcut: "Shift+J" },
|
|
9385
|
+
{ id: "spot-heal", label: "Spot Heal", shortcut: "J" },
|
|
9386
|
+
{ id: "smudge", label: "Smudge", shortcut: "" },
|
|
9387
|
+
{ id: "dodge", label: "Dodge", shortcut: "O" },
|
|
9388
|
+
{ id: "burn", label: "Burn", shortcut: "Shift+O" },
|
|
9389
|
+
{ id: "sponge", label: "Sponge", shortcut: "Shift+O" }
|
|
9390
|
+
];
|
|
9391
|
+
var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
|
|
9392
|
+
var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge";
|
|
9393
|
+
var RETOUCH_OPTION_CONTROLS = [
|
|
9394
|
+
{ id: "exposure", label: "Exposure", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["dodge", "burn"] },
|
|
9395
|
+
{ id: "strength", label: "Strength", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["smudge"] }
|
|
9116
9396
|
];
|
|
9117
|
-
var
|
|
9397
|
+
var RETOUCH_CHOICES = [
|
|
9398
|
+
{
|
|
9399
|
+
id: "sample",
|
|
9400
|
+
label: "Sample",
|
|
9401
|
+
tools: ["clone", "heal", "spot-heal"],
|
|
9402
|
+
options: [
|
|
9403
|
+
{ id: "current", label: "Current", title: "Sample the layer being retouched" },
|
|
9404
|
+
{ id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
|
|
9405
|
+
{ id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
|
|
9406
|
+
]
|
|
9407
|
+
},
|
|
9408
|
+
{
|
|
9409
|
+
id: "spotType",
|
|
9410
|
+
label: "Type",
|
|
9411
|
+
tools: ["spot-heal"],
|
|
9412
|
+
options: [
|
|
9413
|
+
{ id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
|
|
9414
|
+
{ id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
|
|
9415
|
+
]
|
|
9416
|
+
},
|
|
9417
|
+
{
|
|
9418
|
+
id: "range",
|
|
9419
|
+
label: "Range",
|
|
9420
|
+
tools: ["dodge", "burn"],
|
|
9421
|
+
options: [
|
|
9422
|
+
{ id: "shadows", label: "Shadows", title: "Change the dark tones most" },
|
|
9423
|
+
{ id: "midtones", label: "Midtones", title: "Change the middle tones most" },
|
|
9424
|
+
{ id: "highlights", label: "Highlights", title: "Change the light tones most" }
|
|
9425
|
+
]
|
|
9426
|
+
},
|
|
9427
|
+
{
|
|
9428
|
+
id: "spongeMode",
|
|
9429
|
+
label: "Mode",
|
|
9430
|
+
tools: ["sponge"],
|
|
9431
|
+
options: [
|
|
9432
|
+
{ id: "desaturate", label: "Desaturate", title: "Take colour out" },
|
|
9433
|
+
{ id: "saturate", label: "Saturate", title: "Add colour" }
|
|
9434
|
+
]
|
|
9435
|
+
}
|
|
9436
|
+
];
|
|
9437
|
+
var RETOUCH_TOGGLES = [
|
|
9438
|
+
{ id: "aligned", label: "Aligned", tools: ["clone", "heal"] },
|
|
9439
|
+
{ id: "protectTones", label: "Protect tones", tools: ["dodge", "burn"] },
|
|
9440
|
+
{ id: "vibrance", label: "Vibrance", tools: ["sponge"] }
|
|
9441
|
+
];
|
|
9442
|
+
function setRetouchChoice(controller, id, value) {
|
|
9443
|
+
controller.setPaintOptions({ retouch: { [id]: value } });
|
|
9444
|
+
}
|
|
9445
|
+
function cloneSourceLabel(controller) {
|
|
9446
|
+
const cs = controller.getState().paint.cloneSource;
|
|
9447
|
+
return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ""}` : "Alt-click to set the source";
|
|
9448
|
+
}
|
|
9118
9449
|
function rgbUnitToHex(c) {
|
|
9119
9450
|
return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
9120
9451
|
}
|
|
@@ -9140,6 +9471,18 @@ function handlePaintShortcut(controller, e) {
|
|
|
9140
9471
|
case "v":
|
|
9141
9472
|
controller.setTool("move");
|
|
9142
9473
|
return true;
|
|
9474
|
+
case "s":
|
|
9475
|
+
controller.setTool("clone");
|
|
9476
|
+
return true;
|
|
9477
|
+
case "j":
|
|
9478
|
+
controller.setTool(e.shiftKey || key === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
|
|
9479
|
+
return true;
|
|
9480
|
+
case "o": {
|
|
9481
|
+
const cur = controller.getState().activeTool;
|
|
9482
|
+
const next = e.shiftKey || key === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
|
|
9483
|
+
controller.setTool(next);
|
|
9484
|
+
return true;
|
|
9485
|
+
}
|
|
9143
9486
|
case "d":
|
|
9144
9487
|
controller.resetPaintColors();
|
|
9145
9488
|
return true;
|
|
@@ -9195,9 +9538,13 @@ var PaintGesture = class {
|
|
|
9195
9538
|
this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin, moved: false };
|
|
9196
9539
|
return null;
|
|
9197
9540
|
}
|
|
9198
|
-
if (tool === "
|
|
9541
|
+
if ((tool === "clone" || tool === "heal") && sample.altKey) {
|
|
9542
|
+
this.controller.setCloneSource({ x: sample.x, y: sample.y });
|
|
9543
|
+
return null;
|
|
9544
|
+
}
|
|
9545
|
+
if (isPaintTool(tool)) {
|
|
9199
9546
|
this.stroke = this.controller.beginStroke({ tool });
|
|
9200
|
-
this.stroke.push([sample]);
|
|
9547
|
+
this.stroke.push([{ x: sample.x, y: sample.y, pressure: sample.pressure }]);
|
|
9201
9548
|
return null;
|
|
9202
9549
|
}
|
|
9203
9550
|
} catch (err) {
|
|
@@ -9428,8 +9775,272 @@ function createImagePaintTools(controller) {
|
|
|
9428
9775
|
return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool];
|
|
9429
9776
|
}
|
|
9430
9777
|
|
|
9431
|
-
// src/image-editor/
|
|
9778
|
+
// src/image-editor/retouch-tools.ts
|
|
9432
9779
|
var READ5 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
9780
|
+
var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
9781
|
+
var POINTS = {
|
|
9782
|
+
type: "array",
|
|
9783
|
+
minItems: 1,
|
|
9784
|
+
maxItems: 1e4,
|
|
9785
|
+
description: "The stroke path in document pixels; one point is one dab.",
|
|
9786
|
+
items: {
|
|
9787
|
+
type: "object",
|
|
9788
|
+
properties: {
|
|
9789
|
+
x: { type: "number", description: "Document x in px." },
|
|
9790
|
+
y: { type: "number", description: "Document y in px." },
|
|
9791
|
+
pressure: { type: "number", minimum: 0, maximum: 1, description: "Pen pressure (default 1)." }
|
|
9792
|
+
},
|
|
9793
|
+
required: ["x", "y"],
|
|
9794
|
+
additionalProperties: false
|
|
9795
|
+
}
|
|
9796
|
+
};
|
|
9797
|
+
var BRUSH2 = {
|
|
9798
|
+
type: "object",
|
|
9799
|
+
description: "Brush tip; missing fields use the current paint options (default round, hard, 20 px).",
|
|
9800
|
+
properties: {
|
|
9801
|
+
size: { type: "number", minimum: 1, maximum: MAX_BRUSH_SIZE, description: "Diameter in px (default 20)." },
|
|
9802
|
+
hardness: { type: "number", minimum: 0, maximum: 1, description: "Edge hardness 0 (soft) .. 1 (hard, default). Retouching usually wants a soft tip (0..0.5)." },
|
|
9803
|
+
spacing: { type: "number", minimum: 0.01, maximum: 10, description: "Dab spacing as a fraction of the diameter (default 0.1)." },
|
|
9804
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Stroke opacity (Clone Stamp and Healing Brush only; default 1)." },
|
|
9805
|
+
flow: { type: "number", minimum: 0, maximum: 1, description: "Build-up per dab (default 1). For the Sponge, how strongly each pass moves saturation." },
|
|
9806
|
+
smoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Point smoothing 0..0.95 (default 0)." },
|
|
9807
|
+
pressureSize: { type: "boolean", description: "Point pressure scales the size (default true)." },
|
|
9808
|
+
pressureOpacity: { type: "boolean", description: "Point pressure scales the opacity (default false)." }
|
|
9809
|
+
},
|
|
9810
|
+
additionalProperties: false
|
|
9811
|
+
};
|
|
9812
|
+
var TARGET2 = {
|
|
9813
|
+
layerId: { type: "string", description: "Layer to retouch (default the active layer; see list_layers)." },
|
|
9814
|
+
channel: {
|
|
9815
|
+
type: "string",
|
|
9816
|
+
enum: ["pixels", "mask", "filter-mask"],
|
|
9817
|
+
description: "pixels (default for a pixel layer), the layer's mask, or a smart object's filter mask. Masks are grey values and sample only themselves."
|
|
9818
|
+
}
|
|
9819
|
+
};
|
|
9820
|
+
var SAMPLE = {
|
|
9821
|
+
type: "string",
|
|
9822
|
+
enum: [...RETOUCH_SAMPLES],
|
|
9823
|
+
description: "What is sampled: current (default: the target layer), current-below (the composite of this layer and everything under it), or all (the composite of every layer). Sample current-below or all to retouch onto an empty layer above the image."
|
|
9824
|
+
};
|
|
9825
|
+
function readTarget2(args) {
|
|
9826
|
+
return { layerId: args.layerId, channel: args.channel };
|
|
9827
|
+
}
|
|
9828
|
+
function checkSource(controller, source) {
|
|
9829
|
+
if (source === void 0) return void 0;
|
|
9830
|
+
const p = source;
|
|
9831
|
+
const { width, height } = controller.getState().imageDimensions;
|
|
9832
|
+
if (!(p.x >= 0 && p.y >= 0 && p.x < width && p.y < height)) {
|
|
9833
|
+
throw new LayerOperationError(`source (${p.x}, ${p.y}) is outside the ${width} \xD7 ${height} canvas; pick a point on the image to sample.`);
|
|
9834
|
+
}
|
|
9835
|
+
return p;
|
|
9836
|
+
}
|
|
9837
|
+
var SOURCE = {
|
|
9838
|
+
type: "object",
|
|
9839
|
+
description: "Source point in document pixels (an Alt-click). Given, it replaces the clone source and the offset starts afresh at this stroke; missing, the current clone source is used (set_clone_source).",
|
|
9840
|
+
properties: { x: { type: "number", description: "Source x in px." }, y: { type: "number", description: "Source y in px." } },
|
|
9841
|
+
required: ["x", "y"],
|
|
9842
|
+
additionalProperties: false
|
|
9843
|
+
};
|
|
9844
|
+
function createImageRetouchTools(controller) {
|
|
9845
|
+
const stroke = (tool, args, retouch) => controller.paintStroke({
|
|
9846
|
+
tool,
|
|
9847
|
+
points: args.points,
|
|
9848
|
+
brush: args.brush,
|
|
9849
|
+
retouch,
|
|
9850
|
+
source: checkSource(controller, args.source),
|
|
9851
|
+
...readTarget2(args)
|
|
9852
|
+
});
|
|
9853
|
+
const cloneStamp = {
|
|
9854
|
+
name: "clone_stamp",
|
|
9855
|
+
title: "Clone Stamp stroke",
|
|
9856
|
+
description: "Copies pixels from the clone source along a stroke (Photoshop's Clone Stamp): each point paints what lies at point + offset, where the offset is source \u2212 the stroke's first point. With aligned on, later strokes keep that offset; off, every stroke starts again at the source point. Samples the layer, the layers up to it, or all layers, from before the stroke. The selection limits it; a transparency-locked layer keeps its alpha. One undo step. Returns the layerId, channel, changed bounds, dab count and clone {offset, source, aligned}.",
|
|
9857
|
+
inputSchema: {
|
|
9858
|
+
type: "object",
|
|
9859
|
+
properties: {
|
|
9860
|
+
points: POINTS,
|
|
9861
|
+
source: SOURCE,
|
|
9862
|
+
aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
|
|
9863
|
+
sample: SAMPLE,
|
|
9864
|
+
brush: BRUSH2,
|
|
9865
|
+
...TARGET2
|
|
9866
|
+
},
|
|
9867
|
+
required: ["points"],
|
|
9868
|
+
additionalProperties: false
|
|
9869
|
+
},
|
|
9870
|
+
annotations: { title: "Clone Stamp stroke", ...WRITE5 },
|
|
9871
|
+
handler: (args) => stroke("clone", args, pick(args, ["aligned", "sample"]))
|
|
9872
|
+
};
|
|
9873
|
+
const healBrush = {
|
|
9874
|
+
name: "heal_brush",
|
|
9875
|
+
title: "Healing Brush stroke",
|
|
9876
|
+
description: "Heals along a stroke from the clone source (Photoshop's Healing Brush): the source's texture is cloned, then blended so its colour and light match the pixels around the stroke (Poisson seamless cloning, applied when the stroke ends). Use it for blemishes and dust where the area around them is the right tone; use clone_stamp to copy exactly. Same source, offset and aligned rules as clone_stamp. One undo step. Returns the layerId, channel, changed bounds, dab count and clone {offset, source, aligned}.",
|
|
9877
|
+
inputSchema: {
|
|
9878
|
+
type: "object",
|
|
9879
|
+
properties: {
|
|
9880
|
+
points: POINTS,
|
|
9881
|
+
source: SOURCE,
|
|
9882
|
+
aligned: { type: "boolean", description: "Keep the offset for the next strokes (default the current option, true)." },
|
|
9883
|
+
sample: SAMPLE,
|
|
9884
|
+
brush: BRUSH2,
|
|
9885
|
+
...TARGET2
|
|
9886
|
+
},
|
|
9887
|
+
required: ["points"],
|
|
9888
|
+
additionalProperties: false
|
|
9889
|
+
},
|
|
9890
|
+
annotations: { title: "Healing Brush stroke", ...WRITE5 },
|
|
9891
|
+
handler: (args) => stroke("heal", args, pick(args, ["aligned", "sample"]))
|
|
9892
|
+
};
|
|
9893
|
+
const spotHeal = {
|
|
9894
|
+
name: "spot_heal",
|
|
9895
|
+
title: "Spot Healing stroke",
|
|
9896
|
+
description: "Removes what a stroke covers without a source point (Photoshop's Spot Healing Brush): proximity-match copies the best matching patch nearby and blends it in; smooth fills from the edges inwards (for spots on smooth areas). Paint just over the spot with a brush a little larger than it. One undo step. Returns the layerId, channel, changed bounds, dab count and spotHeal {offset} (the patch offset copied, or null for a smooth fill).",
|
|
9897
|
+
inputSchema: {
|
|
9898
|
+
type: "object",
|
|
9899
|
+
properties: {
|
|
9900
|
+
points: POINTS,
|
|
9901
|
+
type: { type: "string", enum: [...SPOT_HEAL_TYPES], description: "proximity-match (default) or smooth." },
|
|
9902
|
+
sample: SAMPLE,
|
|
9903
|
+
brush: BRUSH2,
|
|
9904
|
+
...TARGET2
|
|
9905
|
+
},
|
|
9906
|
+
required: ["points"],
|
|
9907
|
+
additionalProperties: false
|
|
9908
|
+
},
|
|
9909
|
+
annotations: { title: "Spot Healing stroke", ...WRITE5 },
|
|
9910
|
+
handler: (args) => stroke("spot-heal", args, { ...pick(args, ["sample"]), ...args.type !== void 0 ? { spotType: args.type } : {} })
|
|
9911
|
+
};
|
|
9912
|
+
const smudge = {
|
|
9913
|
+
name: "smudge",
|
|
9914
|
+
title: "Smudge stroke",
|
|
9915
|
+
description: "Drags pixels along a stroke like a finger through wet paint (Photoshop's Smudge): each dab lays what the finger carries and picks up what was under it, so colour is pulled in the stroke's direction and fades with distance. One undo step. Returns the layerId, channel, changed bounds and dab count.",
|
|
9916
|
+
inputSchema: {
|
|
9917
|
+
type: "object",
|
|
9918
|
+
properties: {
|
|
9919
|
+
points: POINTS,
|
|
9920
|
+
strength: { type: "number", minimum: 0, maximum: 1, description: "How far colour is dragged, 0..1 (default the current option, 0.5)." },
|
|
9921
|
+
brush: BRUSH2,
|
|
9922
|
+
...TARGET2
|
|
9923
|
+
},
|
|
9924
|
+
required: ["points"],
|
|
9925
|
+
additionalProperties: false
|
|
9926
|
+
},
|
|
9927
|
+
annotations: { title: "Smudge stroke", ...WRITE5 },
|
|
9928
|
+
handler: (args) => stroke("smudge", args, pick(args, ["strength"]))
|
|
9929
|
+
};
|
|
9930
|
+
const toneSchema = (verb) => ({
|
|
9931
|
+
type: "object",
|
|
9932
|
+
properties: {
|
|
9933
|
+
points: POINTS,
|
|
9934
|
+
range: { type: "string", enum: [...TONE_RANGES], description: `Which tones to ${verb} most: shadows, midtones (default the current option, midtones) or highlights.` },
|
|
9935
|
+
exposure: { type: "number", minimum: 0, maximum: 1, description: "Strength 0..1 (default the current option, 0.5). Overlapping dabs in one stroke never go past it; another stroke builds on it." },
|
|
9936
|
+
protectTones: { type: "boolean", description: "Change luma only, keeping hue and saturation (default true)." },
|
|
9937
|
+
brush: BRUSH2,
|
|
9938
|
+
...TARGET2
|
|
9939
|
+
},
|
|
9940
|
+
required: ["points"],
|
|
9941
|
+
additionalProperties: false
|
|
9942
|
+
});
|
|
9943
|
+
const dodge = {
|
|
9944
|
+
name: "dodge",
|
|
9945
|
+
title: "Dodge stroke",
|
|
9946
|
+
description: "Lightens the pixels a stroke covers (Photoshop's Dodge), most in the chosen tonal range. Alpha is kept. One undo step. Returns the layerId, channel, changed bounds and dab count. Inspect the result with capture_view or get_histogram; several light strokes look more natural than one strong one.",
|
|
9947
|
+
inputSchema: toneSchema("lighten"),
|
|
9948
|
+
annotations: { title: "Dodge stroke", ...WRITE5 },
|
|
9949
|
+
handler: (args) => stroke("dodge", args, pick(args, ["range", "exposure", "protectTones"]))
|
|
9950
|
+
};
|
|
9951
|
+
const burn = {
|
|
9952
|
+
name: "burn",
|
|
9953
|
+
title: "Burn stroke",
|
|
9954
|
+
description: "Darkens the pixels a stroke covers (Photoshop's Burn), most in the chosen tonal range. Alpha is kept. One undo step. Returns the layerId, channel, changed bounds and dab count.",
|
|
9955
|
+
inputSchema: toneSchema("darken"),
|
|
9956
|
+
annotations: { title: "Burn stroke", ...WRITE5 },
|
|
9957
|
+
handler: (args) => stroke("burn", args, pick(args, ["range", "exposure", "protectTones"]))
|
|
9958
|
+
};
|
|
9959
|
+
const spongeTool = {
|
|
9960
|
+
name: "sponge",
|
|
9961
|
+
title: "Sponge stroke",
|
|
9962
|
+
description: "Saturates or desaturates the pixels a stroke covers (Photoshop's Sponge); passes compound, and brush.flow sets how much each moves. Pixels only (a mask has no colour). One undo step. Returns the layerId, channel, changed bounds and dab count.",
|
|
9963
|
+
inputSchema: {
|
|
9964
|
+
type: "object",
|
|
9965
|
+
properties: {
|
|
9966
|
+
points: POINTS,
|
|
9967
|
+
mode: { type: "string", enum: ["desaturate", "saturate"], description: "desaturate (default the current option, desaturate) or saturate." },
|
|
9968
|
+
vibrance: { type: "boolean", description: "Damp the change where colours would clip or collapse to grey (default true)." },
|
|
9969
|
+
brush: BRUSH2,
|
|
9970
|
+
layerId: TARGET2.layerId
|
|
9971
|
+
},
|
|
9972
|
+
required: ["points"],
|
|
9973
|
+
additionalProperties: false
|
|
9974
|
+
},
|
|
9975
|
+
annotations: { title: "Sponge stroke", ...WRITE5 },
|
|
9976
|
+
handler: (args) => stroke("sponge", { ...args, channel: "pixels" }, { ...pick(args, ["vibrance"]), ...args.mode !== void 0 ? { spongeMode: args.mode } : {} })
|
|
9977
|
+
};
|
|
9978
|
+
const getOptions = {
|
|
9979
|
+
name: "get_retouch_options",
|
|
9980
|
+
title: "Get retouch options",
|
|
9981
|
+
description: "Reads the retouching tools' options (aligned, sample, spotType, range, exposure, protectTones, spongeMode, vibrance, strength) and the clone source ({x, y, offset} or null; offset is the aligned offset the last clone or heal stroke fixed). Call it before clone_stamp or heal_brush without a source to see where they will sample.",
|
|
9982
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
9983
|
+
annotations: { title: "Get retouch options", ...READ5 },
|
|
9984
|
+
handler: () => {
|
|
9985
|
+
const { retouch, cloneSource } = controller.getState().paint;
|
|
9986
|
+
return { options: { ...retouch }, cloneSource: cloneSource ? { ...cloneSource } : null };
|
|
9987
|
+
}
|
|
9988
|
+
};
|
|
9989
|
+
const setOptions = {
|
|
9990
|
+
name: "set_retouch_options",
|
|
9991
|
+
title: "Set retouch options",
|
|
9992
|
+
description: "Sets the retouching tools' defaults (view state, not an undo step); each stroke tool can also override them per call. Changing aligned clears the aligned offset. Returns the options after the change.",
|
|
9993
|
+
inputSchema: {
|
|
9994
|
+
type: "object",
|
|
9995
|
+
properties: {
|
|
9996
|
+
aligned: { type: "boolean", description: "Clone / heal: keep the offset from stroke to stroke (default true)." },
|
|
9997
|
+
sample: SAMPLE,
|
|
9998
|
+
spotType: { type: "string", enum: [...SPOT_HEAL_TYPES], description: "Spot heal: proximity-match (default) or smooth." },
|
|
9999
|
+
range: { type: "string", enum: [...TONE_RANGES], description: "Dodge / burn: shadows, midtones (default) or highlights." },
|
|
10000
|
+
exposure: { type: "number", minimum: 0, maximum: 1, description: "Dodge / burn strength 0..1 (default 0.5)." },
|
|
10001
|
+
protectTones: { type: "boolean", description: "Dodge / burn: keep hue and saturation (default true)." },
|
|
10002
|
+
spongeMode: { type: "string", enum: ["desaturate", "saturate"], description: "Sponge: desaturate (default) or saturate." },
|
|
10003
|
+
vibrance: { type: "boolean", description: "Sponge: damp clipping (default true)." },
|
|
10004
|
+
strength: { type: "number", minimum: 0, maximum: 1, description: "Smudge strength 0..1 (default 0.5)." }
|
|
10005
|
+
},
|
|
10006
|
+
additionalProperties: false
|
|
10007
|
+
},
|
|
10008
|
+
annotations: { title: "Set retouch options", ...WRITE5, idempotentHint: true },
|
|
10009
|
+
handler: (args) => ({ ...controller.setPaintOptions({ retouch: args }).retouch })
|
|
10010
|
+
};
|
|
10011
|
+
const setSource = {
|
|
10012
|
+
name: "set_clone_source",
|
|
10013
|
+
title: "Set the clone source",
|
|
10014
|
+
description: "Sets the Clone Stamp and Healing Brush source point (Photoshop's Alt-click), in document pixels, or clears it with clear true. The next clone_stamp or heal_brush stroke fixes the offset from its first point. View state, not an undo step. Returns the clone source {x, y, offset} (or null).",
|
|
10015
|
+
inputSchema: {
|
|
10016
|
+
type: "object",
|
|
10017
|
+
properties: {
|
|
10018
|
+
x: { type: "number", description: "Source x in px (required unless clear)." },
|
|
10019
|
+
y: { type: "number", description: "Source y in px (required unless clear)." },
|
|
10020
|
+
clear: { type: "boolean", description: "Remove the source point instead (default false)." }
|
|
10021
|
+
},
|
|
10022
|
+
additionalProperties: false
|
|
10023
|
+
},
|
|
10024
|
+
annotations: { title: "Set the clone source", ...WRITE5, idempotentHint: true },
|
|
10025
|
+
handler: (args) => {
|
|
10026
|
+
if (args.clear === true) {
|
|
10027
|
+
if (args.x !== void 0 || args.y !== void 0) throw new LayerOperationError("Give either clear true or x and y, not both.");
|
|
10028
|
+
return controller.setCloneSource(null);
|
|
10029
|
+
}
|
|
10030
|
+
if (typeof args.x !== "number" || typeof args.y !== "number") throw new LayerOperationError("Pass x and y (document pixels) for the source point, or clear true to remove it.");
|
|
10031
|
+
return controller.setCloneSource(checkSource(controller, { x: args.x, y: args.y }));
|
|
10032
|
+
}
|
|
10033
|
+
};
|
|
10034
|
+
return [cloneStamp, healBrush, spotHeal, smudge, dodge, burn, spongeTool, getOptions, setOptions, setSource];
|
|
10035
|
+
}
|
|
10036
|
+
function pick(args, keys) {
|
|
10037
|
+
const out = {};
|
|
10038
|
+
for (const k of keys) if (args[k] !== void 0) out[k] = args[k];
|
|
10039
|
+
return out;
|
|
10040
|
+
}
|
|
10041
|
+
|
|
10042
|
+
// src/image-editor/psd-tools.ts
|
|
10043
|
+
var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
9433
10044
|
var DESTRUCTIVE4 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
9434
10045
|
var DATA = {
|
|
9435
10046
|
type: "string",
|
|
@@ -9450,7 +10061,7 @@ function createImagePsdTools(controller) {
|
|
|
9450
10061
|
title: "Inspect a PSD",
|
|
9451
10062
|
description: "Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.",
|
|
9452
10063
|
inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
|
|
9453
|
-
annotations: { title: "Inspect a PSD", ...
|
|
10064
|
+
annotations: { title: "Inspect a PSD", ...READ6 },
|
|
9454
10065
|
handler: ({ data }) => {
|
|
9455
10066
|
const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });
|
|
9456
10067
|
return {
|
|
@@ -9466,7 +10077,7 @@ function createImagePsdTools(controller) {
|
|
|
9466
10077
|
const open = {
|
|
9467
10078
|
name: "import_psd",
|
|
9468
10079
|
title: "Open a PSD as the document",
|
|
9469
|
-
description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels);
|
|
10080
|
+
description: "Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; smart objects come in as smart objects (embedded contents, placement and warp, smart filters: Box Blur is drawn, other Photoshop filters are kept but not drawn, filter mask; linked contents as their stored pixels); shape layers come in as live shapes (paths, path operations, fill, stroke, live rectangles, ellipses and lines); type layers come in as live text (runs, paragraphs, frame, transform) showing Photoshop\u2019s pixels until edited, with a warning for fonts missing here; fill layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).",
|
|
9470
10081
|
inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
|
|
9471
10082
|
annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE4 },
|
|
9472
10083
|
handler: ({ data }) => {
|
|
@@ -9485,9 +10096,9 @@ function createImagePsdTools(controller) {
|
|
|
9485
10096
|
const save = {
|
|
9486
10097
|
name: "export_psd",
|
|
9487
10098
|
title: "Export the document as a PSD",
|
|
9488
|
-
description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). Smart objects are written as Photoshop smart objects (contents embedded once per linked set, placement, warp, smart filters and filter mask; a Develop smart filter has no Photoshop filter and is left out of the list). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
|
|
10099
|
+
description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). Smart objects are written as Photoshop smart objects (contents embedded once per linked set, placement, warp, smart filters and filter mask; a Develop smart filter has no Photoshop filter and is left out of the list). Shape layers are written as Photoshop shape layers (path, fill, stroke, live shapes) and type layers as Photoshop type layers (text, runs, paragraphs, frame; blocks opened from a PSD are written back unchanged while the layer is). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor's composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.",
|
|
9489
10100
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
9490
|
-
annotations: { title: "Export the document as a PSD", ...
|
|
10101
|
+
annotations: { title: "Export the document as a PSD", ...READ6 },
|
|
9491
10102
|
handler: async () => {
|
|
9492
10103
|
const res = await controller.toPsd();
|
|
9493
10104
|
return {
|
|
@@ -9505,8 +10116,8 @@ function createImagePsdTools(controller) {
|
|
|
9505
10116
|
}
|
|
9506
10117
|
|
|
9507
10118
|
// src/image-editor/transform-tools.ts
|
|
9508
|
-
var
|
|
9509
|
-
var
|
|
10119
|
+
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
10120
|
+
var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
9510
10121
|
var round4 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
|
|
9511
10122
|
var roundPoint = (p) => ({ x: round4(p.x), y: round4(p.y) });
|
|
9512
10123
|
var POINT = {
|
|
@@ -9515,7 +10126,7 @@ var POINT = {
|
|
|
9515
10126
|
required: ["x", "y"],
|
|
9516
10127
|
additionalProperties: false
|
|
9517
10128
|
};
|
|
9518
|
-
var
|
|
10129
|
+
var TARGET3 = {
|
|
9519
10130
|
layerId: { type: "string", description: "Layer to transform (from list_layers; default the active layer)." },
|
|
9520
10131
|
content: {
|
|
9521
10132
|
type: "string",
|
|
@@ -9597,15 +10208,15 @@ function createImageTransformTools(controller) {
|
|
|
9597
10208
|
inputSchema: {
|
|
9598
10209
|
type: "object",
|
|
9599
10210
|
properties: {
|
|
9600
|
-
layerId:
|
|
9601
|
-
content: { type: "string", enum: FREE_TRANSFORM_CONTENTS, description: `${
|
|
9602
|
-
channel:
|
|
10211
|
+
layerId: TARGET3.layerId,
|
|
10212
|
+
content: { type: "string", enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET3.content.description} selection-outline frames the selection outline alone.` },
|
|
10213
|
+
channel: TARGET3.channel,
|
|
9603
10214
|
meshColumns: { type: "integer", minimum: 1, maximum: 16, description: "Patch columns of an identity warp mesh to return (omit for none)." },
|
|
9604
10215
|
meshRows: { type: "integer", minimum: 1, maximum: 16, description: "Patch rows of an identity warp mesh to return (default meshColumns)." }
|
|
9605
10216
|
},
|
|
9606
10217
|
additionalProperties: false
|
|
9607
10218
|
},
|
|
9608
|
-
annotations: { title: "Get the Free Transform frame", ...
|
|
10219
|
+
annotations: { title: "Get the Free Transform frame", ...READ7 },
|
|
9609
10220
|
handler: (args) => {
|
|
9610
10221
|
const init = targetFrom(args);
|
|
9611
10222
|
if (init.content === "selection-copy") init.content = "selection";
|
|
@@ -9635,10 +10246,10 @@ function createImageTransformTools(controller) {
|
|
|
9635
10246
|
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.",
|
|
9636
10247
|
inputSchema: {
|
|
9637
10248
|
type: "object",
|
|
9638
|
-
properties: { ...
|
|
10249
|
+
properties: { ...TARGET3, ...GEOMETRY },
|
|
9639
10250
|
additionalProperties: false
|
|
9640
10251
|
},
|
|
9641
|
-
annotations: { title: "Free Transform a layer", ...
|
|
10252
|
+
annotations: { title: "Free Transform a layer", ...WRITE6 },
|
|
9642
10253
|
handler: (args) => summary2(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) }))
|
|
9643
10254
|
};
|
|
9644
10255
|
const warpLayer = {
|
|
@@ -9648,7 +10259,7 @@ function createImageTransformTools(controller) {
|
|
|
9648
10259
|
inputSchema: {
|
|
9649
10260
|
type: "object",
|
|
9650
10261
|
properties: {
|
|
9651
|
-
...
|
|
10262
|
+
...TARGET3,
|
|
9652
10263
|
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." },
|
|
9653
10264
|
bend: { type: "number", minimum: -100, maximum: 100, description: "Preset bend in percent, \u2212100..100 (default 50)." },
|
|
9654
10265
|
horizontal: { type: "number", minimum: -100, maximum: 100, description: "Preset horizontal distortion in percent, \u2212100..100 (default 0)." },
|
|
@@ -9668,7 +10279,7 @@ function createImageTransformTools(controller) {
|
|
|
9668
10279
|
},
|
|
9669
10280
|
additionalProperties: false
|
|
9670
10281
|
},
|
|
9671
|
-
annotations: { title: "Warp a layer", ...
|
|
10282
|
+
annotations: { title: "Warp a layer", ...WRITE6 },
|
|
9672
10283
|
handler: (args) => {
|
|
9673
10284
|
const hasPreset = ["style", "bend", "horizontal", "vertical", "orientation"].some((k) => args[k] !== void 0);
|
|
9674
10285
|
if (hasPreset && args.mesh !== void 0) throw new LayerOperationError("Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.");
|
|
@@ -9699,10 +10310,10 @@ function createImageTransformTools(controller) {
|
|
|
9699
10310
|
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.",
|
|
9700
10311
|
inputSchema: {
|
|
9701
10312
|
type: "object",
|
|
9702
|
-
properties: { interpolation: { ...
|
|
10313
|
+
properties: { interpolation: { ...TARGET3.interpolation, description: "Resampling of soft edges: nearest, bilinear or bicubic (default)." }, ...GEOMETRY },
|
|
9703
10314
|
additionalProperties: false
|
|
9704
10315
|
},
|
|
9705
|
-
annotations: { title: "Transform the selection outline", ...
|
|
10316
|
+
annotations: { title: "Transform the selection outline", ...WRITE6 },
|
|
9706
10317
|
handler: (args) => {
|
|
9707
10318
|
const res = controller.transformLayer({ content: "selection-outline", interpolation: args.interpolation, ...geometryFrom(args) });
|
|
9708
10319
|
return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };
|
|
@@ -9716,14 +10327,14 @@ function createImageTransformTools(controller) {
|
|
|
9716
10327
|
type: "object",
|
|
9717
10328
|
properties: {
|
|
9718
10329
|
axis: { type: "string", enum: ["horizontal", "vertical"], description: "horizontal mirrors left \u2194 right, vertical top \u2194 bottom." },
|
|
9719
|
-
layerId:
|
|
9720
|
-
content:
|
|
9721
|
-
channel:
|
|
10330
|
+
layerId: TARGET3.layerId,
|
|
10331
|
+
content: TARGET3.content,
|
|
10332
|
+
channel: TARGET3.channel
|
|
9722
10333
|
},
|
|
9723
10334
|
required: ["axis"],
|
|
9724
10335
|
additionalProperties: false
|
|
9725
10336
|
},
|
|
9726
|
-
annotations: { title: "Flip a layer", ...
|
|
10337
|
+
annotations: { title: "Flip a layer", ...WRITE6 },
|
|
9727
10338
|
handler: (args) => summary2(controller.flipLayer(args.axis, targetFrom(args)))
|
|
9728
10339
|
};
|
|
9729
10340
|
const rotate90 = {
|
|
@@ -9734,22 +10345,22 @@ function createImageTransformTools(controller) {
|
|
|
9734
10345
|
type: "object",
|
|
9735
10346
|
properties: {
|
|
9736
10347
|
turn: { type: "integer", enum: [90, -90, 180], description: "Degrees: 90 clockwise, -90 counter-clockwise, or 180." },
|
|
9737
|
-
layerId:
|
|
9738
|
-
content:
|
|
9739
|
-
channel:
|
|
10348
|
+
layerId: TARGET3.layerId,
|
|
10349
|
+
content: TARGET3.content,
|
|
10350
|
+
channel: TARGET3.channel
|
|
9740
10351
|
},
|
|
9741
10352
|
required: ["turn"],
|
|
9742
10353
|
additionalProperties: false
|
|
9743
10354
|
},
|
|
9744
|
-
annotations: { title: "Rotate a layer by quarter turns", ...
|
|
10355
|
+
annotations: { title: "Rotate a layer by quarter turns", ...WRITE6 },
|
|
9745
10356
|
handler: (args) => summary2(controller.rotateLayer90(args.turn, targetFrom(args)))
|
|
9746
10357
|
};
|
|
9747
10358
|
return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];
|
|
9748
10359
|
}
|
|
9749
10360
|
|
|
9750
10361
|
// src/image-editor/smart-tools.ts
|
|
9751
|
-
var
|
|
9752
|
-
var
|
|
10362
|
+
var READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
10363
|
+
var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
9753
10364
|
var WRITE_IDEMPOTENT4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
9754
10365
|
var DESTRUCTIVE5 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
9755
10366
|
var round5 = (n, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;
|
|
@@ -9807,7 +10418,7 @@ function createImageSmartObjectTools(controller) {
|
|
|
9807
10418
|
title: "Read a smart object",
|
|
9808
10419
|
description: "Returns a smart object\u2019s contents (id shared by linked instances, name, kind raster or document, pixel size, the file type it came from, the instances placed from it), its placement (corners in document pixels, warp mesh in contents pixels, interpolation, and scale / rotate / skew in percent and degrees of the contents\u2019 own size when affine), its smart filters bottom to top (ids, kind, settings, enabled, blend, opacity; kind psd marks a Photoshop filter kept but not drawn) with the filter mask, and the rendered bounds. Changes nothing.",
|
|
9809
10420
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
|
|
9810
|
-
annotations: { title: "Read a smart object", ...
|
|
10421
|
+
annotations: { title: "Read a smart object", ...READ8 },
|
|
9811
10422
|
handler: ({ layerId }) => info(smartId(layerId))
|
|
9812
10423
|
};
|
|
9813
10424
|
const convert = {
|
|
@@ -9822,7 +10433,7 @@ function createImageSmartObjectTools(controller) {
|
|
|
9822
10433
|
},
|
|
9823
10434
|
additionalProperties: false
|
|
9824
10435
|
},
|
|
9825
|
-
annotations: { title: "Convert to Smart Object", ...
|
|
10436
|
+
annotations: { title: "Convert to Smart Object", ...WRITE7 },
|
|
9826
10437
|
handler: ({ layerIds, name }) => {
|
|
9827
10438
|
for (const id2 of layerIds ?? []) requireLayer(controller.getLayers(), id2);
|
|
9828
10439
|
const id = controller.convertToSmartObject(layerIds, { name });
|
|
@@ -9941,7 +10552,7 @@ function createImageSmartObjectTools(controller) {
|
|
|
9941
10552
|
title: "Export a smart object\u2019s contents",
|
|
9942
10553
|
description: "Layer \u203A Smart Objects \u203A Export Contents: the contents as a file, base64: the file they came from while unedited (a PSB, PNG, JPEG\u2026), else a PNG for raster contents or a PSD / PSB of a nested document. Returns name, mimeType, byteLength and base64. Changes nothing.",
|
|
9943
10554
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
|
|
9944
|
-
annotations: { title: "Export a smart object\u2019s contents", ...
|
|
10555
|
+
annotations: { title: "Export a smart object\u2019s contents", ...READ8 },
|
|
9945
10556
|
handler: ({ layerId }) => {
|
|
9946
10557
|
const f = controller.exportSmartObjectContents(smartId(layerId));
|
|
9947
10558
|
return { name: f.name, mimeType: f.mimeType, byteLength: f.bytes.length, base64: bytesToBase64(f.bytes) };
|
|
@@ -9961,7 +10572,7 @@ function createImageSmartObjectTools(controller) {
|
|
|
9961
10572
|
required: ["layerId"],
|
|
9962
10573
|
additionalProperties: false
|
|
9963
10574
|
},
|
|
9964
|
-
annotations: { title: "Copy a smart object", ...
|
|
10575
|
+
annotations: { title: "Copy a smart object", ...WRITE7 },
|
|
9965
10576
|
handler: ({ layerId, linked, name }) => {
|
|
9966
10577
|
const id = controller.copySmartObject(smartId(layerId), linked === true, name);
|
|
9967
10578
|
return { layer: layerSummary(id), contentsId: controller.getSmartObject(id).contents.id };
|
|
@@ -9972,7 +10583,7 @@ function createImageSmartObjectTools(controller) {
|
|
|
9972
10583
|
title: "Edit a smart object\u2019s contents",
|
|
9973
10584
|
description: 'Layer \u203A Smart Objects \u203A Edit Contents: opens the contents as a document of their own (a layer document, or raster contents as one pixel layer "Layer 0") with their own undo history. Every other tool (list_layers, paint, adjustments\u2026) then works inside the contents until close_smart_object_contents saves them back. Contents can nest. Returns the contents\u2019 size and layers.',
|
|
9974
10585
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID4 }, required: ["layerId"], additionalProperties: false },
|
|
9975
|
-
annotations: { title: "Edit a smart object\u2019s contents", ...
|
|
10586
|
+
annotations: { title: "Edit a smart object\u2019s contents", ...WRITE7 },
|
|
9976
10587
|
handler: ({ layerId }) => {
|
|
9977
10588
|
const res = controller.editSmartObjectContents(smartId(layerId));
|
|
9978
10589
|
const s = controller.getState();
|
|
@@ -9988,7 +10599,7 @@ function createImageSmartObjectTools(controller) {
|
|
|
9988
10599
|
properties: { save: { type: "boolean", description: "true (default) saves the edits into the smart object; false discards them." } },
|
|
9989
10600
|
additionalProperties: false
|
|
9990
10601
|
},
|
|
9991
|
-
annotations: { title: "Close a smart object\u2019s contents", ...
|
|
10602
|
+
annotations: { title: "Close a smart object\u2019s contents", ...WRITE7 },
|
|
9992
10603
|
handler: ({ save }) => ({ ...controller.closeSmartObjectContents(save !== false), editing: controller.getEditingContents() })
|
|
9993
10604
|
};
|
|
9994
10605
|
const addFilter = {
|
|
@@ -10006,7 +10617,7 @@ function createImageSmartObjectTools(controller) {
|
|
|
10006
10617
|
required: ["layerId", "kind"],
|
|
10007
10618
|
additionalProperties: false
|
|
10008
10619
|
},
|
|
10009
|
-
annotations: { title: "Add a smart filter", ...
|
|
10620
|
+
annotations: { title: "Add a smart filter", ...WRITE7 },
|
|
10010
10621
|
handler: (args) => {
|
|
10011
10622
|
const id = smartId(args.layerId);
|
|
10012
10623
|
const filter = controller.addSmartFilter(id, { kind: args.kind, ...settingsFrom(args) }, args.index);
|
|
@@ -10095,7 +10706,7 @@ function createImageSmartObjectTools(controller) {
|
|
|
10095
10706
|
required: ["layerId", "from"],
|
|
10096
10707
|
additionalProperties: false
|
|
10097
10708
|
},
|
|
10098
|
-
annotations: { title: "Set the smart filter mask", ...
|
|
10709
|
+
annotations: { title: "Set the smart filter mask", ...WRITE7 },
|
|
10099
10710
|
handler: (args) => {
|
|
10100
10711
|
const id = smartId(args.layerId);
|
|
10101
10712
|
const props = {};
|
|
@@ -10108,8 +10719,8 @@ function createImageSmartObjectTools(controller) {
|
|
|
10108
10719
|
}
|
|
10109
10720
|
|
|
10110
10721
|
// src/image-editor/shape-tools.ts
|
|
10111
|
-
var
|
|
10112
|
-
var
|
|
10722
|
+
var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
10723
|
+
var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
10113
10724
|
var WRITE_IDEMPOTENT5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
10114
10725
|
var DESTRUCTIVE6 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
10115
10726
|
var LAYER_ID5 = { type: "string", description: "Shape layer id from list_layers (type shape) or add_shape_layer." };
|
|
@@ -10362,7 +10973,7 @@ function createImageShapeTools(controller) {
|
|
|
10362
10973
|
title: "Read a shape layer",
|
|
10363
10974
|
description: "Returns a shape layer\u2019s components bottom to top (index, path operation, the live shape it was drawn as or null, its subpaths with absolute anchors and handles, bounds), the fill rule, fill and stroke paints and settings, the path bounds and the rendered bounds (stroke included). Changes nothing.",
|
|
10364
10975
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, required: ["layerId"], additionalProperties: false },
|
|
10365
|
-
annotations: { title: "Read a shape layer", ...
|
|
10976
|
+
annotations: { title: "Read a shape layer", ...READ9 },
|
|
10366
10977
|
handler: ({ layerId }) => controller.getShapeLayer(shapeId(layerId))
|
|
10367
10978
|
};
|
|
10368
10979
|
const add2 = {
|
|
@@ -10393,7 +11004,7 @@ function createImageShapeTools(controller) {
|
|
|
10393
11004
|
},
|
|
10394
11005
|
additionalProperties: false
|
|
10395
11006
|
},
|
|
10396
|
-
annotations: { title: "Add a shape layer", ...
|
|
11007
|
+
annotations: { title: "Add a shape layer", ...WRITE8 },
|
|
10397
11008
|
handler: (args) => {
|
|
10398
11009
|
const init = { ...geometry(args) };
|
|
10399
11010
|
if (args.components !== void 0) init.components = args.components.map(geometry);
|
|
@@ -10488,7 +11099,7 @@ function createImageShapeTools(controller) {
|
|
|
10488
11099
|
required: ["layerId"],
|
|
10489
11100
|
additionalProperties: false
|
|
10490
11101
|
},
|
|
10491
|
-
annotations: { title: "Add a shape component", ...
|
|
11102
|
+
annotations: { title: "Add a shape component", ...WRITE8 },
|
|
10492
11103
|
handler: ({ layerId, index, ...rest }) => controller.addShapeComponent(shapeId(layerId), geometry(rest), index)
|
|
10493
11104
|
};
|
|
10494
11105
|
const removeComponent = {
|
|
@@ -10511,8 +11122,8 @@ function createImageShapeTools(controller) {
|
|
|
10511
11122
|
}
|
|
10512
11123
|
|
|
10513
11124
|
// src/image-editor/type-tools.ts
|
|
10514
|
-
var
|
|
10515
|
-
var
|
|
11125
|
+
var READ10 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
11126
|
+
var WRITE9 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
10516
11127
|
var WRITE_IDEMPOTENT6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
10517
11128
|
var DESTRUCTIVE7 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
10518
11129
|
var LAYER_ID6 = { type: "string", description: "Type layer id from list_layers (type type) or add_text_layer." };
|
|
@@ -10540,7 +11151,7 @@ var PARAGRAPH = {
|
|
|
10540
11151
|
spaceBefore: { type: ["number", "null"], description: "Space above the paragraph in pixels (not before the first)." },
|
|
10541
11152
|
spaceAfter: { type: ["number", "null"], description: "Space below the paragraph in pixels." }
|
|
10542
11153
|
};
|
|
10543
|
-
function
|
|
11154
|
+
function pick2(args, keys) {
|
|
10544
11155
|
const out = {};
|
|
10545
11156
|
for (const k of keys) if (args[k] !== void 0) out[k] = args[k];
|
|
10546
11157
|
return out;
|
|
@@ -10562,7 +11173,7 @@ function createImageTypeTools(controller) {
|
|
|
10562
11173
|
title: "List fonts",
|
|
10563
11174
|
description: "Returns the font families type layers can use here (generic families such as sans-serif always work). Changes nothing.",
|
|
10564
11175
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
10565
|
-
annotations: { title: "List fonts", ...
|
|
11176
|
+
annotations: { title: "List fonts", ...READ10 },
|
|
10566
11177
|
handler: () => ({ families: controller.listFonts(), shaper: controller.getTextShaper()?.id ?? null })
|
|
10567
11178
|
};
|
|
10568
11179
|
const get = {
|
|
@@ -10570,7 +11181,7 @@ function createImageTypeTools(controller) {
|
|
|
10570
11181
|
title: "Read a type layer",
|
|
10571
11182
|
description: "Returns a type layer\u2019s text, its character runs and paragraphs as [start, end) ranges in UTF-16 code units with their styles, the frame (point text, or a paragraph box with its size), the text-to-document transform, the laid-out lines (range, baseline, width), the rendered bounds, missing fonts and whether the pixels are live or stored from a file. Call before editing text you did not write, to get the ranges. Changes nothing.",
|
|
10572
11183
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID6 }, required: ["layerId"], additionalProperties: false },
|
|
10573
|
-
annotations: { title: "Read a type layer", ...
|
|
11184
|
+
annotations: { title: "Read a type layer", ...READ10 },
|
|
10574
11185
|
handler: ({ layerId }) => controller.getTextLayer(typeId(layerId))
|
|
10575
11186
|
};
|
|
10576
11187
|
const add2 = {
|
|
@@ -10604,9 +11215,9 @@ function createImageTypeTools(controller) {
|
|
|
10604
11215
|
required: ["text", "x", "y"],
|
|
10605
11216
|
additionalProperties: false
|
|
10606
11217
|
},
|
|
10607
|
-
annotations: { title: "Add a type layer", ...
|
|
11218
|
+
annotations: { title: "Add a type layer", ...WRITE9 },
|
|
10608
11219
|
handler: (args) => {
|
|
10609
|
-
const init =
|
|
11220
|
+
const init = pick2(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
|
|
10610
11221
|
if (init.blend !== void 0) init.blend = init.blend;
|
|
10611
11222
|
const id = controller.addTextLayer(init);
|
|
10612
11223
|
return { layer: layerSummary(id), text: controller.getTextLayer(id) };
|
|
@@ -10617,7 +11228,7 @@ function createImageTypeTools(controller) {
|
|
|
10617
11228
|
title: "Edit text",
|
|
10618
11229
|
description: "Replaces the text in [start, end) (default all of it) with text; inserted text takes the style of the character at start. Use start = end to insert. Returns the text layer with its new runs.",
|
|
10619
11230
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID6, text: { type: "string", description: "Replacement text (\\n starts a paragraph; empty deletes the range)." }, start: START, end: END }, required: ["layerId", "text"], additionalProperties: false },
|
|
10620
|
-
annotations: { title: "Edit text", ...
|
|
11231
|
+
annotations: { title: "Edit text", ...WRITE9 },
|
|
10621
11232
|
handler: (args) => controller.setText(typeId(args.layerId), args.text, range(args))
|
|
10622
11233
|
};
|
|
10623
11234
|
const setStyle = {
|
|
@@ -10627,7 +11238,7 @@ function createImageTypeTools(controller) {
|
|
|
10627
11238
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...STYLE }, required: ["layerId"], additionalProperties: false },
|
|
10628
11239
|
annotations: { title: "Style a range of text", ...WRITE_IDEMPOTENT6 },
|
|
10629
11240
|
handler: (args) => {
|
|
10630
|
-
const style =
|
|
11241
|
+
const style = pick2(args, Object.keys(STYLE));
|
|
10631
11242
|
if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one style field: ${Object.keys(STYLE).join(", ")}.`);
|
|
10632
11243
|
return controller.setTextStyle(typeId(args.layerId), style, range(args));
|
|
10633
11244
|
}
|
|
@@ -10639,7 +11250,7 @@ function createImageTypeTools(controller) {
|
|
|
10639
11250
|
inputSchema: { type: "object", properties: { layerId: LAYER_ID6, start: START, end: END, ...PARAGRAPH }, required: ["layerId"], additionalProperties: false },
|
|
10640
11251
|
annotations: { title: "Style paragraphs", ...WRITE_IDEMPOTENT6 },
|
|
10641
11252
|
handler: (args) => {
|
|
10642
|
-
const style =
|
|
11253
|
+
const style = pick2(args, Object.keys(PARAGRAPH));
|
|
10643
11254
|
if (!Object.keys(style).length) throw new LayerOperationError(`Give at least one paragraph field: ${Object.keys(PARAGRAPH).join(", ")}.`);
|
|
10644
11255
|
return controller.setParagraphStyle(typeId(args.layerId), style, range(args));
|
|
10645
11256
|
}
|
|
@@ -10923,6 +11534,7 @@ function createImageEditorToolProvider(controller, options = {}) {
|
|
|
10923
11534
|
...createImageTypeTools(controller),
|
|
10924
11535
|
...createImageEffectTools(controller),
|
|
10925
11536
|
...createImagePaintTools(controller),
|
|
11537
|
+
...createImageRetouchTools(controller),
|
|
10926
11538
|
...createPigmentTools(),
|
|
10927
11539
|
...createImagePsdTools(controller)
|
|
10928
11540
|
]
|
|
@@ -11223,6 +11835,470 @@ function applyRegionBlur(imageData, regions, radius = 10) {
|
|
|
11223
11835
|
return imageData;
|
|
11224
11836
|
}
|
|
11225
11837
|
|
|
11838
|
+
// src/image-editor/layers/shape-type-ui.ts
|
|
11839
|
+
var DEFAULT_SHAPE_TOOL_OPTIONS = {
|
|
11840
|
+
kind: "rect",
|
|
11841
|
+
fill: "#4f46e5",
|
|
11842
|
+
stroke: null,
|
|
11843
|
+
strokeWidth: 2,
|
|
11844
|
+
radius: 0,
|
|
11845
|
+
sides: 6,
|
|
11846
|
+
points: 5,
|
|
11847
|
+
innerRatio: 0.5,
|
|
11848
|
+
weight: 4,
|
|
11849
|
+
arrow: false
|
|
11850
|
+
};
|
|
11851
|
+
var SHAPE_KIND_LABELS = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
|
|
11852
|
+
var DEFAULT_TYPE_TOOL_OPTIONS = { fontFamily: "sans-serif", fontSize: 32, fontWeight: 400, italic: false, color: "#111827", align: "left" };
|
|
11853
|
+
function liveShapeForDrag(o, a, b, mods = {}) {
|
|
11854
|
+
if (o.kind === "line") {
|
|
11855
|
+
let x2 = b.x;
|
|
11856
|
+
let y2 = b.y;
|
|
11857
|
+
if (mods.shift) {
|
|
11858
|
+
const ang = Math.round(Math.atan2(b.y - a.y, b.x - a.x) / (Math.PI / 4)) * (Math.PI / 4);
|
|
11859
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
11860
|
+
x2 = a.x + Math.cos(ang) * len;
|
|
11861
|
+
y2 = a.y + Math.sin(ang) * len;
|
|
11862
|
+
}
|
|
11863
|
+
if (Math.hypot(x2 - a.x, y2 - a.y) < 1) return null;
|
|
11864
|
+
const head = o.arrow ? { endArrow: { width: o.weight * 4, length: o.weight * 4 } } : {};
|
|
11865
|
+
return { kind: "line", x1: a.x, y1: a.y, x2, y2, weight: o.weight, ...head };
|
|
11866
|
+
}
|
|
11867
|
+
let w = b.x - a.x;
|
|
11868
|
+
let h = b.y - a.y;
|
|
11869
|
+
if (mods.shift) {
|
|
11870
|
+
const s = Math.max(Math.abs(w), Math.abs(h));
|
|
11871
|
+
w = Math.sign(w || 1) * s;
|
|
11872
|
+
h = Math.sign(h || 1) * s;
|
|
11873
|
+
}
|
|
11874
|
+
let x = a.x;
|
|
11875
|
+
let y = a.y;
|
|
11876
|
+
if (mods.alt) {
|
|
11877
|
+
x = a.x - w;
|
|
11878
|
+
y = a.y - h;
|
|
11879
|
+
w *= 2;
|
|
11880
|
+
h *= 2;
|
|
11881
|
+
}
|
|
11882
|
+
const box = { x: Math.min(x, x + w), y: Math.min(y, y + h), width: Math.abs(w), height: Math.abs(h) };
|
|
11883
|
+
if (box.width < 1 || box.height < 1) return null;
|
|
11884
|
+
switch (o.kind) {
|
|
11885
|
+
case "rect":
|
|
11886
|
+
return { kind: "rect", ...box, ...o.radius > 0 ? { radius: o.radius } : {} };
|
|
11887
|
+
case "ellipse":
|
|
11888
|
+
return { kind: "ellipse", ...box };
|
|
11889
|
+
case "polygon":
|
|
11890
|
+
return { kind: "polygon", ...box, sides: o.sides };
|
|
11891
|
+
case "star":
|
|
11892
|
+
return { kind: "star", ...box, points: o.points, innerRatio: o.innerRatio };
|
|
11893
|
+
}
|
|
11894
|
+
return null;
|
|
11895
|
+
}
|
|
11896
|
+
var ShapeDrawGesture = class {
|
|
11897
|
+
constructor(controller, options) {
|
|
11898
|
+
this.controller = controller;
|
|
11899
|
+
this.options = options;
|
|
11900
|
+
}
|
|
11901
|
+
controller;
|
|
11902
|
+
options;
|
|
11903
|
+
start = null;
|
|
11904
|
+
layerId = null;
|
|
11905
|
+
error = null;
|
|
11906
|
+
get active() {
|
|
11907
|
+
return this.start !== null;
|
|
11908
|
+
}
|
|
11909
|
+
/** The shape layer drawn by the last drag (until the next one starts). */
|
|
11910
|
+
get drawnLayerId() {
|
|
11911
|
+
return this.layerId;
|
|
11912
|
+
}
|
|
11913
|
+
down(p) {
|
|
11914
|
+
this.error = null;
|
|
11915
|
+
this.start = { x: p.x, y: p.y };
|
|
11916
|
+
this.layerId = null;
|
|
11917
|
+
}
|
|
11918
|
+
move(p, mods = {}) {
|
|
11919
|
+
if (!this.start) return;
|
|
11920
|
+
const o = this.options();
|
|
11921
|
+
const shape = liveShapeForDrag(o, this.start, p, mods);
|
|
11922
|
+
if (!shape) return;
|
|
11923
|
+
try {
|
|
11924
|
+
if (!this.layerId) {
|
|
11925
|
+
const isLine = o.kind === "line";
|
|
11926
|
+
this.layerId = this.controller.addShapeLayer({
|
|
11927
|
+
shape,
|
|
11928
|
+
fill: isLine ? o.stroke ?? o.fill ?? "#000000" : o.fill,
|
|
11929
|
+
stroke: !isLine && o.stroke ? { width: o.strokeWidth, paint: o.stroke } : null
|
|
11930
|
+
});
|
|
11931
|
+
} else this.controller.setShapeComponent(this.layerId, 0, { shape }, false);
|
|
11932
|
+
} catch (e) {
|
|
11933
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
11934
|
+
this.start = null;
|
|
11935
|
+
}
|
|
11936
|
+
}
|
|
11937
|
+
up() {
|
|
11938
|
+
this.start = null;
|
|
11939
|
+
return this.layerId;
|
|
11940
|
+
}
|
|
11941
|
+
cancel() {
|
|
11942
|
+
this.start = null;
|
|
11943
|
+
}
|
|
11944
|
+
};
|
|
11945
|
+
function applyShapeOptions(controller, layerId, change) {
|
|
11946
|
+
const info = controller.getShapeLayer(layerId);
|
|
11947
|
+
const style = {};
|
|
11948
|
+
if ("fill" in change) style.fill = change.fill ?? null;
|
|
11949
|
+
if ("stroke" in change || "strokeWidth" in change) {
|
|
11950
|
+
const color = "stroke" in change ? change.stroke : info.stroke?.paint.type === "color" ? info.stroke.paint.color : null;
|
|
11951
|
+
style.stroke = color ? { width: change.strokeWidth ?? info.stroke?.width ?? 2, paint: color } : null;
|
|
11952
|
+
}
|
|
11953
|
+
if (Object.keys(style).length) controller.setShapeStyle(layerId, style);
|
|
11954
|
+
const live = info.components[0]?.live;
|
|
11955
|
+
if (!live) return;
|
|
11956
|
+
const shape = {};
|
|
11957
|
+
if (live.kind === "rect" && change.radius !== void 0) shape.radius = change.radius;
|
|
11958
|
+
if (live.kind === "polygon" && change.sides !== void 0) shape.sides = change.sides;
|
|
11959
|
+
if (live.kind === "star" && change.points !== void 0) shape.points = change.points;
|
|
11960
|
+
if (live.kind === "star" && change.innerRatio !== void 0) shape.innerRatio = change.innerRatio;
|
|
11961
|
+
if (live.kind === "line" && change.weight !== void 0) shape.weight = change.weight;
|
|
11962
|
+
if (live.kind === "line" && change.arrow !== void 0) shape.endArrow = change.arrow ? { width: (live.weight || 1) * 4, length: (live.weight || 1) * 4 } : null;
|
|
11963
|
+
if (Object.keys(shape).length) controller.setShapeComponent(layerId, 0, { shape });
|
|
11964
|
+
}
|
|
11965
|
+
var TextEditSession = class {
|
|
11966
|
+
constructor(controller, layerId, index) {
|
|
11967
|
+
this.controller = controller;
|
|
11968
|
+
this.layerId = layerId;
|
|
11969
|
+
const n = this.text.length;
|
|
11970
|
+
this.anchor = this.focus = Math.max(0, Math.min(n, index ?? n));
|
|
11971
|
+
}
|
|
11972
|
+
controller;
|
|
11973
|
+
layerId;
|
|
11974
|
+
anchor;
|
|
11975
|
+
focus;
|
|
11976
|
+
changed = false;
|
|
11977
|
+
listeners = /* @__PURE__ */ new Set();
|
|
11978
|
+
error = null;
|
|
11979
|
+
get layer() {
|
|
11980
|
+
const l = findLayer(this.controller.getLayers(), this.layerId)?.layer;
|
|
11981
|
+
if (!l || l.type !== "type") throw new Error("The type layer being edited is gone.");
|
|
11982
|
+
return l;
|
|
11983
|
+
}
|
|
11984
|
+
get text() {
|
|
11985
|
+
return this.layer.text;
|
|
11986
|
+
}
|
|
11987
|
+
get start() {
|
|
11988
|
+
return Math.min(this.anchor, this.focus);
|
|
11989
|
+
}
|
|
11990
|
+
get end() {
|
|
11991
|
+
return Math.max(this.anchor, this.focus);
|
|
11992
|
+
}
|
|
11993
|
+
/** Whether the layer still exists (it can be deleted or undone away while editing). */
|
|
11994
|
+
get alive() {
|
|
11995
|
+
return findLayer(this.controller.getLayers(), this.layerId)?.layer.type === "type";
|
|
11996
|
+
}
|
|
11997
|
+
subscribe(fn) {
|
|
11998
|
+
this.listeners.add(fn);
|
|
11999
|
+
return () => this.listeners.delete(fn);
|
|
12000
|
+
}
|
|
12001
|
+
emit() {
|
|
12002
|
+
for (const l of this.listeners) l();
|
|
12003
|
+
}
|
|
12004
|
+
layout() {
|
|
12005
|
+
return this.controller.getTextLayout(this.layerId);
|
|
12006
|
+
}
|
|
12007
|
+
run(fn) {
|
|
12008
|
+
this.error = null;
|
|
12009
|
+
try {
|
|
12010
|
+
const out = fn(!this.changed);
|
|
12011
|
+
this.changed = true;
|
|
12012
|
+
return out;
|
|
12013
|
+
} catch (e) {
|
|
12014
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
12015
|
+
return void 0;
|
|
12016
|
+
} finally {
|
|
12017
|
+
this.emit();
|
|
12018
|
+
}
|
|
12019
|
+
}
|
|
12020
|
+
/** Replaces the selection with `text` (typing and pasting). */
|
|
12021
|
+
insert(text) {
|
|
12022
|
+
if (!text) return;
|
|
12023
|
+
const s = this.start;
|
|
12024
|
+
const e = this.end;
|
|
12025
|
+
const done = this.run((commit) => this.controller.setText(this.layerId, text, { start: s, end: e }, commit));
|
|
12026
|
+
if (done) this.anchor = this.focus = s + text.replace(/\r\n?/g, "\n").length;
|
|
12027
|
+
}
|
|
12028
|
+
/** Backspace: the selection, else the character before the caret. */
|
|
12029
|
+
deleteBackward() {
|
|
12030
|
+
if (this.start === this.end && this.start === 0) return;
|
|
12031
|
+
const s = this.start === this.end ? Math.max(0, this.start - this.surrogate(this.start - 1, -1)) : this.start;
|
|
12032
|
+
const e = this.end;
|
|
12033
|
+
if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
|
|
12034
|
+
}
|
|
12035
|
+
/** Delete: the selection, else the character after the caret. */
|
|
12036
|
+
deleteForward() {
|
|
12037
|
+
const n = this.text.length;
|
|
12038
|
+
if (this.start === this.end && this.end >= n) return;
|
|
12039
|
+
const s = this.start;
|
|
12040
|
+
const e = this.start === this.end ? Math.min(n, this.end + this.surrogate(this.end, 1)) : this.end;
|
|
12041
|
+
if (this.run((commit) => this.controller.setText(this.layerId, "", { start: s, end: e }, commit))) this.anchor = this.focus = s;
|
|
12042
|
+
}
|
|
12043
|
+
/** 2 when the code unit at `i` is half of a surrogate pair (moving `dir`), else 1. */
|
|
12044
|
+
surrogate(i, dir) {
|
|
12045
|
+
const t = this.text;
|
|
12046
|
+
const c = t.charCodeAt(i);
|
|
12047
|
+
if (dir < 0 && c >= 56320 && c <= 57343 && i > 0) return 2;
|
|
12048
|
+
if (dir > 0 && c >= 55296 && c <= 56319 && i + 1 < t.length) return 2;
|
|
12049
|
+
return 1;
|
|
12050
|
+
}
|
|
12051
|
+
/** Moves the caret (Shift extends the selection). */
|
|
12052
|
+
move(to, extend = false) {
|
|
12053
|
+
const n = this.text.length;
|
|
12054
|
+
let f = this.focus;
|
|
12055
|
+
const collapsedJump = !extend && this.start !== this.end;
|
|
12056
|
+
if (to === "left") f = collapsedJump ? this.start : Math.max(0, f - this.surrogate(f - 1, -1));
|
|
12057
|
+
else if (to === "right") f = collapsedJump ? this.end : Math.min(n, f + this.surrogate(f, 1));
|
|
12058
|
+
else if (to === "start") f = 0;
|
|
12059
|
+
else if (to === "finish") f = n;
|
|
12060
|
+
else {
|
|
12061
|
+
const layout = this.layout();
|
|
12062
|
+
if (layout && layout.lines.length) {
|
|
12063
|
+
const c = caretAt(layout, f);
|
|
12064
|
+
const line = layout.lines[c.line];
|
|
12065
|
+
if (to === "home") f = line.start;
|
|
12066
|
+
else if (to === "end") {
|
|
12067
|
+
const last = line.chars[line.chars.length - 1];
|
|
12068
|
+
f = last ? Math.min(n, last.index + 1) : line.start;
|
|
12069
|
+
if (last && this.text[last.index] === " " && c.line < layout.lines.length - 1) f = last.index;
|
|
12070
|
+
} else {
|
|
12071
|
+
const target = layout.lines[c.line + (to === "up" ? -1 : 1)];
|
|
12072
|
+
if (!target) f = to === "up" ? 0 : n;
|
|
12073
|
+
else {
|
|
12074
|
+
const hit = target.chars.find((ch) => c.x < ch.x + ch.advance / 2);
|
|
12075
|
+
const last = target.chars[target.chars.length - 1];
|
|
12076
|
+
f = hit ? hit.index : last ? last.index + 1 : target.start;
|
|
12077
|
+
}
|
|
12078
|
+
}
|
|
12079
|
+
}
|
|
12080
|
+
}
|
|
12081
|
+
this.focus = f;
|
|
12082
|
+
if (!extend) this.anchor = f;
|
|
12083
|
+
this.emit();
|
|
12084
|
+
}
|
|
12085
|
+
selectAll() {
|
|
12086
|
+
this.anchor = 0;
|
|
12087
|
+
this.focus = this.text.length;
|
|
12088
|
+
this.emit();
|
|
12089
|
+
}
|
|
12090
|
+
/** Places the caret at a document point (Shift-click extends). */
|
|
12091
|
+
pointAt(p, extend = false) {
|
|
12092
|
+
this.focus = this.controller.textIndexAt(this.layerId, p);
|
|
12093
|
+
if (!extend) this.anchor = this.focus;
|
|
12094
|
+
this.emit();
|
|
12095
|
+
}
|
|
12096
|
+
/** Handles a key; returns `true` when it was the session's (the caller then prevents the default). */
|
|
12097
|
+
handleKey(e) {
|
|
12098
|
+
const mod2 = e.metaKey || e.ctrlKey;
|
|
12099
|
+
if (mod2 && e.key.toLowerCase() === "a") {
|
|
12100
|
+
this.selectAll();
|
|
12101
|
+
return true;
|
|
12102
|
+
}
|
|
12103
|
+
switch (e.key) {
|
|
12104
|
+
case "Backspace":
|
|
12105
|
+
this.deleteBackward();
|
|
12106
|
+
return true;
|
|
12107
|
+
case "Delete":
|
|
12108
|
+
this.deleteForward();
|
|
12109
|
+
return true;
|
|
12110
|
+
case "Enter":
|
|
12111
|
+
this.insert("\n");
|
|
12112
|
+
return true;
|
|
12113
|
+
case "ArrowLeft":
|
|
12114
|
+
this.move(mod2 ? "home" : "left", e.shiftKey);
|
|
12115
|
+
return true;
|
|
12116
|
+
case "ArrowRight":
|
|
12117
|
+
this.move(mod2 ? "end" : "right", e.shiftKey);
|
|
12118
|
+
return true;
|
|
12119
|
+
case "ArrowUp":
|
|
12120
|
+
this.move(mod2 ? "start" : "up", e.shiftKey);
|
|
12121
|
+
return true;
|
|
12122
|
+
case "ArrowDown":
|
|
12123
|
+
this.move(mod2 ? "finish" : "down", e.shiftKey);
|
|
12124
|
+
return true;
|
|
12125
|
+
case "Home":
|
|
12126
|
+
this.move("home", e.shiftKey);
|
|
12127
|
+
return true;
|
|
12128
|
+
case "End":
|
|
12129
|
+
this.move("end", e.shiftKey);
|
|
12130
|
+
return true;
|
|
12131
|
+
case "Tab":
|
|
12132
|
+
this.insert(" ");
|
|
12133
|
+
return true;
|
|
12134
|
+
}
|
|
12135
|
+
if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
|
|
12136
|
+
this.insert(e.key);
|
|
12137
|
+
return true;
|
|
12138
|
+
}
|
|
12139
|
+
return false;
|
|
12140
|
+
}
|
|
12141
|
+
/** Sets character style fields on the selection, or on the whole text when nothing is selected. */
|
|
12142
|
+
setStyle(style) {
|
|
12143
|
+
const s = this.start;
|
|
12144
|
+
const e = this.end;
|
|
12145
|
+
const range = s === e ? {} : { start: s, end: e };
|
|
12146
|
+
this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
|
|
12147
|
+
}
|
|
12148
|
+
/** Sets paragraph fields on the paragraphs the selection touches. */
|
|
12149
|
+
setParagraph(style) {
|
|
12150
|
+
const s = this.start;
|
|
12151
|
+
const e = this.end;
|
|
12152
|
+
this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
|
|
12153
|
+
}
|
|
12154
|
+
/** The style at the caret (or the selection's start), for the options bar. */
|
|
12155
|
+
styleHere() {
|
|
12156
|
+
const layer = this.layer;
|
|
12157
|
+
const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
|
|
12158
|
+
return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
|
|
12159
|
+
}
|
|
12160
|
+
info() {
|
|
12161
|
+
return this.controller.getTextLayer(this.layerId);
|
|
12162
|
+
}
|
|
12163
|
+
/** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
|
|
12164
|
+
overlay() {
|
|
12165
|
+
const layer = this.layer;
|
|
12166
|
+
const layout = this.layout();
|
|
12167
|
+
const m = textMatrix(layer);
|
|
12168
|
+
const at = (x, y) => applyMatrix(m, { x, y });
|
|
12169
|
+
let frame = null;
|
|
12170
|
+
if (layer.frame.kind === "box") frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];
|
|
12171
|
+
else if (layout) {
|
|
12172
|
+
const b = layout.bounds;
|
|
12173
|
+
frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];
|
|
12174
|
+
}
|
|
12175
|
+
if (!layout) return { caret: null, selection: [], frame };
|
|
12176
|
+
const selection = [];
|
|
12177
|
+
if (this.start !== this.end) {
|
|
12178
|
+
for (const l of layout.lines) {
|
|
12179
|
+
const chars = l.chars.filter((c2) => c2.index >= this.start && c2.index < this.end);
|
|
12180
|
+
if (!chars.length) continue;
|
|
12181
|
+
const x0 = chars[0].x;
|
|
12182
|
+
const x1 = chars[chars.length - 1].x + chars[chars.length - 1].advance;
|
|
12183
|
+
const top = l.baseline - l.ascent;
|
|
12184
|
+
const bottom = l.baseline + l.descent;
|
|
12185
|
+
selection.push([at(x0, top), at(x1, top), at(x1, bottom), at(x0, bottom)]);
|
|
12186
|
+
}
|
|
12187
|
+
}
|
|
12188
|
+
const c = caretAt(layout, this.focus);
|
|
12189
|
+
return { caret: [at(c.x, c.top), at(c.x, c.bottom)], selection, frame };
|
|
12190
|
+
}
|
|
12191
|
+
/** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
|
|
12192
|
+
close() {
|
|
12193
|
+
if (!this.alive) return false;
|
|
12194
|
+
if (!this.text.trim().length) {
|
|
12195
|
+
try {
|
|
12196
|
+
this.controller.removeLayer(this.layerId);
|
|
12197
|
+
} catch {
|
|
12198
|
+
}
|
|
12199
|
+
return false;
|
|
12200
|
+
}
|
|
12201
|
+
if (this.layer.name === "Text" && this.changed) {
|
|
12202
|
+
try {
|
|
12203
|
+
this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
|
|
12204
|
+
} catch {
|
|
12205
|
+
}
|
|
12206
|
+
}
|
|
12207
|
+
return true;
|
|
12208
|
+
}
|
|
12209
|
+
};
|
|
12210
|
+
function typeLayerAt(controller, p, slop = 4) {
|
|
12211
|
+
const layers = [...flattenTypeLayers(controller.getLayers())].reverse();
|
|
12212
|
+
for (const l of layers) {
|
|
12213
|
+
if (!l.visible) continue;
|
|
12214
|
+
const inv = invert(textMatrix(l));
|
|
12215
|
+
const q = applyMatrix(inv, p);
|
|
12216
|
+
let b = null;
|
|
12217
|
+
if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
|
|
12218
|
+
else {
|
|
12219
|
+
const layout = controller.getTextLayout(l.id);
|
|
12220
|
+
if (layout) b = layout.bounds;
|
|
12221
|
+
}
|
|
12222
|
+
if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;
|
|
12223
|
+
}
|
|
12224
|
+
return null;
|
|
12225
|
+
}
|
|
12226
|
+
function flattenTypeLayers(layers) {
|
|
12227
|
+
const out = [];
|
|
12228
|
+
const walk = (list) => {
|
|
12229
|
+
for (const l of list) {
|
|
12230
|
+
if (l.type === "type") out.push(l);
|
|
12231
|
+
else if (l.type === "group") walk(l.children);
|
|
12232
|
+
}
|
|
12233
|
+
};
|
|
12234
|
+
walk(layers);
|
|
12235
|
+
return out;
|
|
12236
|
+
}
|
|
12237
|
+
function invert(m) {
|
|
12238
|
+
const [a, b, c, d, e, f] = m;
|
|
12239
|
+
const det = a * d - b * c || 1;
|
|
12240
|
+
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
12241
|
+
}
|
|
12242
|
+
var TypeToolGesture = class {
|
|
12243
|
+
constructor(controller, options) {
|
|
12244
|
+
this.controller = controller;
|
|
12245
|
+
this.options = options;
|
|
12246
|
+
}
|
|
12247
|
+
controller;
|
|
12248
|
+
options;
|
|
12249
|
+
session = null;
|
|
12250
|
+
dragStart = null;
|
|
12251
|
+
error = null;
|
|
12252
|
+
down(p, mods = {}) {
|
|
12253
|
+
this.error = null;
|
|
12254
|
+
const hit = typeLayerAt(this.controller, p);
|
|
12255
|
+
if (this.session && this.session.alive && hit === this.session.layerId) {
|
|
12256
|
+
this.session.pointAt(p, mods.shift);
|
|
12257
|
+
this.dragStart = null;
|
|
12258
|
+
return;
|
|
12259
|
+
}
|
|
12260
|
+
this.finish();
|
|
12261
|
+
if (hit) {
|
|
12262
|
+
this.controller.setActiveLayer(hit);
|
|
12263
|
+
this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
|
|
12264
|
+
this.dragStart = null;
|
|
12265
|
+
return;
|
|
12266
|
+
}
|
|
12267
|
+
this.dragStart = { x: p.x, y: p.y };
|
|
12268
|
+
}
|
|
12269
|
+
/** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */
|
|
12270
|
+
up(p) {
|
|
12271
|
+
const start = this.dragStart;
|
|
12272
|
+
this.dragStart = null;
|
|
12273
|
+
if (!start) return this.session;
|
|
12274
|
+
const o = this.options();
|
|
12275
|
+
const w = Math.abs(p.x - start.x);
|
|
12276
|
+
const h = Math.abs(p.y - start.y);
|
|
12277
|
+
try {
|
|
12278
|
+
const box = w >= 8 && h >= 8 ? { width: w, height: h } : void 0;
|
|
12279
|
+
const id = this.controller.addTextLayer({
|
|
12280
|
+
text: "",
|
|
12281
|
+
x: box ? Math.min(start.x, p.x) : start.x,
|
|
12282
|
+
y: box ? Math.min(start.y, p.y) : start.y,
|
|
12283
|
+
...box ? { box } : {},
|
|
12284
|
+
style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
|
|
12285
|
+
paragraph: o.align !== "left" ? { align: o.align } : {},
|
|
12286
|
+
name: "Text"
|
|
12287
|
+
});
|
|
12288
|
+
this.session = new TextEditSession(this.controller, id, 0);
|
|
12289
|
+
} catch (e) {
|
|
12290
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
12291
|
+
}
|
|
12292
|
+
return this.session;
|
|
12293
|
+
}
|
|
12294
|
+
/** Ends the open session (an empty new layer is removed). */
|
|
12295
|
+
finish() {
|
|
12296
|
+
const s = this.session;
|
|
12297
|
+
this.session = null;
|
|
12298
|
+
s?.close();
|
|
12299
|
+
}
|
|
12300
|
+
};
|
|
12301
|
+
|
|
11226
12302
|
// src/image-editor/layers/panel.ts
|
|
11227
12303
|
function layerPanelRows(layers, activeId) {
|
|
11228
12304
|
const rows = [];
|
|
@@ -11267,6 +12343,15 @@ function layerBadge(layer) {
|
|
|
11267
12343
|
return "T";
|
|
11268
12344
|
}
|
|
11269
12345
|
}
|
|
12346
|
+
function layerHasThumbnail(layer) {
|
|
12347
|
+
return layer.type === "pixel" || layer.type === "shape" || layer.type === "type";
|
|
12348
|
+
}
|
|
12349
|
+
function layerWarning(layer) {
|
|
12350
|
+
if (layer.type === "type" && layer.missingFonts?.length) {
|
|
12351
|
+
return `Missing ${layer.missingFonts.length === 1 ? "font" : "fonts"}: ${layer.missingFonts.join(", ")}. ${layer.rendering === "stored" ? "Showing the saved pixels until the text is edited." : "Drawn with a fallback font."}`;
|
|
12352
|
+
}
|
|
12353
|
+
return null;
|
|
12354
|
+
}
|
|
11270
12355
|
function dropZone(fraction, isGroup) {
|
|
11271
12356
|
if (isGroup && fraction > 0.3 && fraction < 0.7) return "inside";
|
|
11272
12357
|
return fraction < 0.5 ? "above" : "below";
|
|
@@ -12245,10 +13330,13 @@ export {
|
|
|
12245
13330
|
DEFAULT_MITER_LIMIT,
|
|
12246
13331
|
DEFAULT_MIXER,
|
|
12247
13332
|
DEFAULT_REFINE_EDGE_PARAMS,
|
|
13333
|
+
DEFAULT_RETOUCH,
|
|
12248
13334
|
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
13335
|
+
DEFAULT_SHAPE_TOOL_OPTIONS,
|
|
12249
13336
|
DEFAULT_SMUDGE,
|
|
12250
13337
|
DEFAULT_TEXT_STYLE,
|
|
12251
13338
|
DEFAULT_TRANSFORM,
|
|
13339
|
+
DEFAULT_TYPE_TOOL_OPTIONS,
|
|
12252
13340
|
DEPTH_MASK_CONTROLS,
|
|
12253
13341
|
DEVELOP_FILTER_ID,
|
|
12254
13342
|
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
@@ -12312,16 +13400,26 @@ export {
|
|
|
12312
13400
|
PatternTile,
|
|
12313
13401
|
PsdError,
|
|
12314
13402
|
REFINE_EDGE_CONTROLS,
|
|
13403
|
+
RETOUCH_CHOICES,
|
|
13404
|
+
RETOUCH_OPTION_CONTROLS,
|
|
13405
|
+
RETOUCH_SAMPLES,
|
|
13406
|
+
RETOUCH_TOGGLES,
|
|
13407
|
+
RETOUCH_TOOLS,
|
|
13408
|
+
RegionPixelSource,
|
|
13409
|
+
RetouchDabStroke,
|
|
12315
13410
|
SELECTION_MODES,
|
|
12316
13411
|
SELECTION_MODE_LABELS,
|
|
12317
13412
|
SELECTION_MODIFY_OPS,
|
|
12318
13413
|
SELECTION_TOOLS,
|
|
12319
13414
|
SHADOW_AMBIENT,
|
|
13415
|
+
SHAPE_KIND_LABELS,
|
|
12320
13416
|
SHAPE_OPERATIONS,
|
|
12321
13417
|
SHAPE_RENDER_MARGIN,
|
|
12322
13418
|
SMART_RENDER_MARGIN,
|
|
13419
|
+
SPOT_HEAL_TYPES,
|
|
12323
13420
|
SUPPORTED_ADJUSTMENT_KEYS,
|
|
12324
13421
|
SelectionGesture,
|
|
13422
|
+
ShapeDrawGesture,
|
|
12325
13423
|
SmudgeTip,
|
|
12326
13424
|
StrokeCoverage,
|
|
12327
13425
|
SurfaceDraft,
|
|
@@ -12332,11 +13430,15 @@ export {
|
|
|
12332
13430
|
TILE_SHIFT,
|
|
12333
13431
|
TINT_SCALE,
|
|
12334
13432
|
TONE_LUT_SIZE,
|
|
13433
|
+
TONE_RANGES,
|
|
12335
13434
|
TONE_RANGE_PRESETS,
|
|
12336
13435
|
TRANSFORM_ANCHORS,
|
|
13436
|
+
TextEditSession,
|
|
12337
13437
|
TileMedium,
|
|
13438
|
+
TilePixelSource,
|
|
12338
13439
|
TiledSurface,
|
|
12339
13440
|
TransformTooLargeError,
|
|
13441
|
+
TypeToolGesture,
|
|
12340
13442
|
VIBRANCE_GREY,
|
|
12341
13443
|
WARP_GRID_OPTIONS,
|
|
12342
13444
|
WARP_STYLES,
|
|
@@ -12375,11 +13477,14 @@ export {
|
|
|
12375
13477
|
applyProcess2Adjustments,
|
|
12376
13478
|
applyRegionBlur,
|
|
12377
13479
|
applyRegionMosaic,
|
|
13480
|
+
applyRetouchToTile,
|
|
13481
|
+
applyShapeOptions,
|
|
12378
13482
|
applySmartFilters,
|
|
12379
13483
|
applyStrokeToTile,
|
|
12380
13484
|
backgroundLayer,
|
|
12381
13485
|
base64ToBytes,
|
|
12382
13486
|
baseProjection,
|
|
13487
|
+
bestOffset,
|
|
12383
13488
|
blackWhiteGray,
|
|
12384
13489
|
blendChannel,
|
|
12385
13490
|
blendImageData,
|
|
@@ -12409,6 +13514,7 @@ export {
|
|
|
12409
13514
|
clampControlValue,
|
|
12410
13515
|
clampCropToImage,
|
|
12411
13516
|
cleanMixer,
|
|
13517
|
+
cloneSourceLabel,
|
|
12412
13518
|
collectTiles,
|
|
12413
13519
|
colorBalanceChannel,
|
|
12414
13520
|
colorRange,
|
|
@@ -12440,6 +13546,7 @@ export {
|
|
|
12440
13546
|
createImageLayerTools,
|
|
12441
13547
|
createImagePaintTools,
|
|
12442
13548
|
createImagePsdTools,
|
|
13549
|
+
createImageRetouchTools,
|
|
12443
13550
|
createImageSelectionTools,
|
|
12444
13551
|
createImageShapeTools,
|
|
12445
13552
|
createImageSmartObjectTools,
|
|
@@ -12486,8 +13593,11 @@ export {
|
|
|
12486
13593
|
developFilter,
|
|
12487
13594
|
developFromAdjustments,
|
|
12488
13595
|
developTarget,
|
|
13596
|
+
dilateMask,
|
|
12489
13597
|
distanceTransform,
|
|
12490
13598
|
documentContents,
|
|
13599
|
+
dodgeBurn,
|
|
13600
|
+
dodgeBurnCurve,
|
|
12491
13601
|
downloadPsd,
|
|
12492
13602
|
dragWarpMeshPoint,
|
|
12493
13603
|
dropPlacement,
|
|
@@ -12549,6 +13659,7 @@ export {
|
|
|
12549
13659
|
hasWarp,
|
|
12550
13660
|
hashMaskContent,
|
|
12551
13661
|
hashNoise,
|
|
13662
|
+
healPixels,
|
|
12552
13663
|
hexToRgb,
|
|
12553
13664
|
histogramPath,
|
|
12554
13665
|
hitTestAnnotation,
|
|
@@ -12592,6 +13703,8 @@ export {
|
|
|
12592
13703
|
isQuarterTurn,
|
|
12593
13704
|
isQuarterTurnSwapped,
|
|
12594
13705
|
isRenderedLayer,
|
|
13706
|
+
isRetouchPaintTool,
|
|
13707
|
+
isRetouchTool,
|
|
12595
13708
|
isSelectionEmpty,
|
|
12596
13709
|
isSelectionMode,
|
|
12597
13710
|
isSeparableBlendMode,
|
|
@@ -12607,6 +13720,7 @@ export {
|
|
|
12607
13720
|
layerEffectsBadge,
|
|
12608
13721
|
layerEffectsOf,
|
|
12609
13722
|
layerEffectsPanel,
|
|
13723
|
+
layerHasThumbnail,
|
|
12610
13724
|
layerLabel,
|
|
12611
13725
|
layerMaskCoverage,
|
|
12612
13726
|
layerMaskFromSelection,
|
|
@@ -12616,6 +13730,7 @@ export {
|
|
|
12616
13730
|
layerPanelRows,
|
|
12617
13731
|
layerTree,
|
|
12618
13732
|
layerTypeLabel,
|
|
13733
|
+
layerWarning,
|
|
12619
13734
|
layersFromLegacy,
|
|
12620
13735
|
layoutText,
|
|
12621
13736
|
legacyAdjustmentLayer,
|
|
@@ -12627,16 +13742,20 @@ export {
|
|
|
12627
13742
|
linearSrgbToXy,
|
|
12628
13743
|
linearToSrgb,
|
|
12629
13744
|
liveComponent,
|
|
13745
|
+
liveShapeForDrag,
|
|
12630
13746
|
liveShapeSubpaths,
|
|
12631
13747
|
loadImageEditorDocument,
|
|
12632
13748
|
loadMixer,
|
|
13749
|
+
luma,
|
|
12633
13750
|
magicWand,
|
|
12634
13751
|
mapCropHandle,
|
|
12635
13752
|
mapLayer,
|
|
12636
13753
|
mapSubpaths,
|
|
13754
|
+
maskBounds,
|
|
12637
13755
|
maskSummary,
|
|
12638
13756
|
matrixFromCorners,
|
|
12639
13757
|
matrixFromParams,
|
|
13758
|
+
membraneFill,
|
|
12640
13759
|
mergeLayerEffect,
|
|
12641
13760
|
mergeShapeComponents,
|
|
12642
13761
|
migrateSerializedImageEditorState,
|
|
@@ -12671,6 +13790,7 @@ export {
|
|
|
12671
13790
|
normalizeProcessVersion,
|
|
12672
13791
|
normalizeRect,
|
|
12673
13792
|
normalizeRefineEdgeParams,
|
|
13793
|
+
normalizeRetouch,
|
|
12674
13794
|
normalizeShapePaint,
|
|
12675
13795
|
normalizeShapeStroke,
|
|
12676
13796
|
normalizeSmartFilter,
|
|
@@ -12697,6 +13817,7 @@ export {
|
|
|
12697
13817
|
parseVersionedDescriptor,
|
|
12698
13818
|
patchParagraphStyle,
|
|
12699
13819
|
patchTextStyle,
|
|
13820
|
+
perDabStrength,
|
|
12700
13821
|
photoFilterMatrix,
|
|
12701
13822
|
pigment8,
|
|
12702
13823
|
pixelLayerBounds,
|
|
@@ -12733,6 +13854,7 @@ export {
|
|
|
12733
13854
|
readPixel,
|
|
12734
13855
|
readPsd,
|
|
12735
13856
|
readRegion,
|
|
13857
|
+
readSourceRegion,
|
|
12736
13858
|
rectSelection,
|
|
12737
13859
|
rectToQuad,
|
|
12738
13860
|
reduceSurface,
|
|
@@ -12774,11 +13896,14 @@ export {
|
|
|
12774
13896
|
rgbToHsl,
|
|
12775
13897
|
rgbToReflectance,
|
|
12776
13898
|
rgbUnitToHex,
|
|
13899
|
+
rgbaViewOfGrey,
|
|
12777
13900
|
runLayerFilters,
|
|
12778
13901
|
runLayerMenuAction,
|
|
12779
13902
|
sampleLut,
|
|
12780
13903
|
sampleToneLut,
|
|
13904
|
+
saturationOf,
|
|
12781
13905
|
scaleLayerEffect,
|
|
13906
|
+
seamlessClone,
|
|
12782
13907
|
selectionArea,
|
|
12783
13908
|
selectionFromCoverage,
|
|
12784
13909
|
selectionFromPixels,
|
|
@@ -12801,6 +13926,7 @@ export {
|
|
|
12801
13926
|
setLayerProperties,
|
|
12802
13927
|
setLum,
|
|
12803
13928
|
setPigmentColor,
|
|
13929
|
+
setRetouchChoice,
|
|
12804
13930
|
setSmartFilterMask,
|
|
12805
13931
|
setSmartFiltersEnabled,
|
|
12806
13932
|
setSmartPlacement,
|
|
@@ -12815,9 +13941,15 @@ export {
|
|
|
12815
13941
|
smartObjectLayers,
|
|
12816
13942
|
smoothCoverage,
|
|
12817
13943
|
smoothPoints,
|
|
13944
|
+
smudgeEffect,
|
|
12818
13945
|
solidSurface,
|
|
13946
|
+
solveMembrane,
|
|
12819
13947
|
spliceRuns,
|
|
12820
13948
|
splitOverlayAnnotations,
|
|
13949
|
+
sponge,
|
|
13950
|
+
spongeEffect,
|
|
13951
|
+
spotHealPixels,
|
|
13952
|
+
spotHealWindow,
|
|
12821
13953
|
srgb8ToLab,
|
|
12822
13954
|
srgbToLinear,
|
|
12823
13955
|
standardBlendMode,
|
|
@@ -12843,6 +13975,7 @@ export {
|
|
|
12843
13975
|
toTextSpace,
|
|
12844
13976
|
toneCurve,
|
|
12845
13977
|
toneLuts,
|
|
13978
|
+
toneOf,
|
|
12846
13979
|
toneRange,
|
|
12847
13980
|
transformHomography,
|
|
12848
13981
|
transformMap,
|
|
@@ -12856,6 +13989,7 @@ export {
|
|
|
12856
13989
|
typeAsPixelLayer,
|
|
12857
13990
|
typeFrame,
|
|
12858
13991
|
typeFromAnnotation,
|
|
13992
|
+
typeLayerAt,
|
|
12859
13993
|
typeLayerName,
|
|
12860
13994
|
ungroupLayer,
|
|
12861
13995
|
uniformTile,
|