@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.
Files changed (38) hide show
  1. package/THIRD_PARTY_NOTICES.md +6 -0
  2. package/dist/chunk-4KLT7ZYS.cjs +781 -0
  3. package/dist/chunk-4KLT7ZYS.cjs.map +1 -0
  4. package/dist/{chunk-HHZ6ZJTW.js → chunk-CZOXTEMN.js} +1772 -456
  5. package/dist/chunk-CZOXTEMN.js.map +1 -0
  6. package/dist/chunk-LPYOIVLA.js +781 -0
  7. package/dist/chunk-LPYOIVLA.js.map +1 -0
  8. package/dist/{chunk-PHUMLSCW.cjs → chunk-YGIMK4NX.cjs} +1810 -494
  9. package/dist/chunk-YGIMK4NX.cjs.map +1 -0
  10. package/dist/{export-DoDhCszr.d.cts → export-CEXEzqBf.d.cts} +150 -18
  11. package/dist/{export-DBNVwb6h.d.ts → export-DIkoK8-r.d.ts} +150 -18
  12. package/dist/image-editor/index.cjs +1922 -788
  13. package/dist/image-editor/index.cjs.map +1 -1
  14. package/dist/image-editor/index.d.cts +233 -9
  15. package/dist/image-editor/index.d.ts +233 -9
  16. package/dist/image-editor/index.js +1208 -74
  17. package/dist/image-editor/index.js.map +1 -1
  18. package/dist/paint/index.cjs +61 -2
  19. package/dist/paint/index.cjs.map +1 -1
  20. package/dist/paint/index.d.cts +29 -2
  21. package/dist/paint/index.d.ts +29 -2
  22. package/dist/paint/index.js +60 -1
  23. package/dist/paint-editor/index.cjs +6 -6
  24. package/dist/paint-editor/index.d.cts +3 -3
  25. package/dist/paint-editor/index.d.ts +3 -3
  26. package/dist/paint-editor/index.js +1 -1
  27. package/dist/{blending-DXl06NSx.d.ts → retouch-BXmeSCaB.d.ts} +218 -1
  28. package/dist/{blending-aOE6Y3UE.d.cts → retouch-CEIKR22W.d.cts} +218 -1
  29. package/package.json +1 -1
  30. package/skills/pixerate-image-editor/SKILL.md +30 -6
  31. package/skills/pixerate-image-editor/references/retouching.md +36 -0
  32. package/skills/pixerate-vector/SKILL.md +102 -5
  33. package/dist/chunk-FTBZPI77.cjs +0 -1
  34. package/dist/chunk-FTBZPI77.cjs.map +0 -1
  35. package/dist/chunk-HHZ6ZJTW.js.map +0 -1
  36. package/dist/chunk-PHUMLSCW.cjs.map +0 -1
  37. package/dist/chunk-VKK5VS2T.js +0 -1
  38. 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 "../chunk-VKK5VS2T.js";
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-HHZ6ZJTW.js";
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 pick2 = (font) => {
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 } = pick2(font);
2681
+ const { entry, available } = pick3(font);
2613
2682
  return { family: entry.family, available };
2614
2683
  },
2615
2684
  metrics(font) {
2616
- const { face } = pick2(font).entry;
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 } = pick2(font).entry;
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(pick2(font).entry.face.getPath(text, x, y, font.size, { kerning: true }).commands);
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.update(this.mixer ? this.mixer.push(points) : this.stroke.push(points));
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 = applyStrokeToTile(stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});
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.update(this.mixer ? this.mixer.finish(last) : this.stroke.finish(last));
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.mixer ? this.mixerInit.end(this.mixer.load) : void 0;
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 !== "brush" && tool !== "eraser" && tool !== "mixer") throw new LayerOperationError(`Unknown paint tool "${String(tool)}"; use brush, eraser or mixer.`);
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.layers;
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
- add2(layer.type === "shape" ? "Shape layers were written as pixel layers" : "Type layers were written as pixel layers", layer.name);
6582
- const out2 = layer.type === "shape" ? shapeAsPixelLayer(layer) : typeAsPixelLayer(layer);
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 isPaintTool = (tool) => tool === "move" || tool === "brush" || tool === "mixer" || tool === "eraser";
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 === "brush" || tool === "eraser" || tool === "mixer") {
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/psd-tools.ts
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", ...READ5 },
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); type, fill and shape 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).",
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", ...READ5 },
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 READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9509
- var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
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 TARGET2 = {
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: TARGET2.layerId,
9601
- content: { type: "string", enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET2.content.description} selection-outline frames the selection outline alone.` },
9602
- channel: TARGET2.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", ...READ6 },
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: { ...TARGET2, ...GEOMETRY },
10249
+ properties: { ...TARGET3, ...GEOMETRY },
9639
10250
  additionalProperties: false
9640
10251
  },
9641
- annotations: { title: "Free Transform a layer", ...WRITE5 },
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
- ...TARGET2,
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", ...WRITE5 },
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: { ...TARGET2.interpolation, description: "Resampling of soft edges: nearest, bilinear or bicubic (default)." }, ...GEOMETRY },
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", ...WRITE5 },
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: TARGET2.layerId,
9720
- content: TARGET2.content,
9721
- channel: TARGET2.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", ...WRITE5 },
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: TARGET2.layerId,
9738
- content: TARGET2.content,
9739
- channel: TARGET2.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", ...WRITE5 },
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 READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
9752
- var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
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", ...READ7 },
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", ...WRITE6 },
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", ...READ7 },
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", ...WRITE6 },
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", ...WRITE6 },
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", ...WRITE6 },
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", ...WRITE6 },
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", ...WRITE6 },
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 READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10112
- var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
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", ...READ8 },
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", ...WRITE7 },
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", ...WRITE7 },
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 READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
10515
- var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
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 pick(args, keys) {
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", ...READ9 },
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", ...READ9 },
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", ...WRITE8 },
11218
+ annotations: { title: "Add a type layer", ...WRITE9 },
10608
11219
  handler: (args) => {
10609
- const init = pick(args, ["text", "x", "y", "box", "style", "paragraph", "rotation", "name", "opacity", "blend", "aboveId", "belowId", "parentId", "index"]);
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", ...WRITE8 },
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 = pick(args, Object.keys(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 = pick(args, Object.keys(PARAGRAPH));
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,