@pixerate/editor 0.93.0 → 0.94.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.
@@ -5,17 +5,25 @@ import {
5
5
  createCaptureTool
6
6
  } from "../chunk-REOJJIY3.js";
7
7
  import {
8
+ DEFAULT_GLOBAL_LIGHT,
8
9
  DIRECT_GAUSSIAN_MAX_SIGMA,
10
+ LAYER_EFFECT_KINDS,
11
+ LayerEffectError,
9
12
  RasterCompositor,
10
13
  ZlibError,
11
14
  adler32,
12
15
  base64ToBytes,
13
16
  blendTile,
17
+ builtinPattern,
18
+ builtinPatternIds,
14
19
  bytesToBase64,
15
20
  changedTiles,
21
+ curveLut,
16
22
  decodePng,
23
+ defaultLayerEffect,
17
24
  deflate,
18
25
  deflateRaw,
26
+ effectsMargin,
19
27
  encodePng,
20
28
  findLayer,
21
29
  flattenLayers,
@@ -23,12 +31,18 @@ import {
23
31
  inflate,
24
32
  inflateRaw,
25
33
  insertLayer,
34
+ isLayerEffectKind,
26
35
  isLayerLocked,
27
36
  isPng,
37
+ layerEffectLabel,
28
38
  mapLayer,
39
+ mergeLayerEffect,
40
+ newEffectId,
41
+ normalizeLayerEffects,
29
42
  parseDataUrl,
30
- pngDataUrl
31
- } from "../chunk-P5TPUNBZ.js";
43
+ pngDataUrl,
44
+ validateLayerEffects
45
+ } from "../chunk-747CGOT2.js";
32
46
  import {
33
47
  CoverageMap,
34
48
  DEFAULT_BRUSH,
@@ -1496,59 +1510,6 @@ function normalizeAdjustmentLayers(raw) {
1496
1510
  }
1497
1511
  return out;
1498
1512
  }
1499
- function curveLut(points, size = ADJUSTMENT_LUT_SIZE) {
1500
- const pts = points.filter((p) => Number.isFinite(p.input) && Number.isFinite(p.output)).map((p) => [p.input, p.output]).sort((a, b) => a[0] - b[0]);
1501
- const dedup = [];
1502
- for (const p of pts) {
1503
- if (dedup.length && Math.abs(dedup[dedup.length - 1][0] - p[0]) < 1e-6) continue;
1504
- dedup.push(p);
1505
- }
1506
- const out = new Float32Array(size);
1507
- const n = dedup.length;
1508
- if (n < 2) {
1509
- for (let k = 0; k < size; k++) out[k] = k / (size - 1);
1510
- return out;
1511
- }
1512
- const x = dedup.map((p) => p[0]);
1513
- const y = dedup.map((p) => p[1]);
1514
- const m2 = new Float64Array(n);
1515
- if (n > 2) {
1516
- const c = new Float64Array(n);
1517
- const d = new Float64Array(n);
1518
- for (let i = 1; i < n - 1; i++) {
1519
- const h0 = x[i] - x[i - 1];
1520
- const h1 = x[i + 1] - x[i];
1521
- const a = h0 / 6;
1522
- const b = (h0 + h1) / 3;
1523
- const cc = h1 / 6;
1524
- const r = (y[i + 1] - y[i]) / h1 - (y[i] - y[i - 1]) / h0;
1525
- const denom = b - a * c[i - 1];
1526
- c[i] = cc / denom;
1527
- d[i] = (r - a * d[i - 1]) / denom;
1528
- }
1529
- for (let i = n - 2; i >= 1; i--) m2[i] = d[i] - c[i] * m2[i + 1];
1530
- }
1531
- let seg = 0;
1532
- for (let k = 0; k < size; k++) {
1533
- const xv = k / (size - 1);
1534
- if (xv <= x[0]) {
1535
- out[k] = clamp01(y[0]);
1536
- continue;
1537
- }
1538
- if (xv >= x[n - 1]) {
1539
- out[k] = clamp01(y[n - 1]);
1540
- continue;
1541
- }
1542
- while (seg < n - 2 && xv > x[seg + 1]) seg++;
1543
- const x0 = x[seg], x1 = x[seg + 1];
1544
- const h = x1 - x0;
1545
- const a = (x1 - xv) / h;
1546
- const b = (xv - x0) / h;
1547
- const v = a * y[seg] + b * y[seg + 1] + ((a * a * a - a) * m2[seg] + (b * b * b - b) * m2[seg + 1]) * (h * h) / 6;
1548
- out[k] = clamp01(v);
1549
- }
1550
- return out;
1551
- }
1552
1513
  function curveLutFromLevels(points) {
1553
1514
  return curveLut(points.map((p) => ({ input: p.input / 255, output: p.output / 255 })));
1554
1515
  }
@@ -2700,7 +2661,7 @@ function developTarget(layers, activeLayerId) {
2700
2661
  }
2701
2662
  return pixels.find((l) => l.source === "document") ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;
2702
2663
  }
2703
- var isPlain = (l) => l.visible && l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask && !l.locks;
2664
+ var isPlain = (l) => l.visible && l.opacity === 1 && l.blend === "normal" && !l.clipped && !l.mask && !l.locks && !l.effects;
2704
2665
  function legacyView(stack, width, height, activeLayerId = null) {
2705
2666
  const { layers } = stack;
2706
2667
  const target = developTarget(layers, activeLayerId);
@@ -2740,7 +2701,7 @@ function isLegacyShape(stack) {
2740
2701
  const bg = layers[0];
2741
2702
  if (!bg || bg.type !== "pixel" || bg.source !== "document") return false;
2742
2703
  if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;
2743
- if (!bg.visible || bg.opacity !== 1 || bg.blend !== "normal" || bg.clipped || bg.locks) return false;
2704
+ if (!bg.visible || bg.opacity !== 1 || bg.blend !== "normal" || bg.clipped || bg.locks || bg.effects) return false;
2744
2705
  if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;
2745
2706
  const filters = bg.filters ?? [];
2746
2707
  if (filters.length < 1 || filters.length > 2) return false;
@@ -2756,7 +2717,7 @@ function isLegacyShape(stack) {
2756
2717
  let adjustments = 0;
2757
2718
  for (; i < layers.length && layers[i].type === "adjustment"; i++) {
2758
2719
  const l = layers[i];
2759
- if (l.clipped || l.locks || l.mask?.source) return false;
2720
+ if (l.clipped || l.locks || l.mask?.source || l.effects) return false;
2760
2721
  adjustments++;
2761
2722
  }
2762
2723
  for (; i < layers.length; i++) {
@@ -3262,7 +3223,9 @@ var ImageEditorRenderer = class {
3262
3223
  return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;
3263
3224
  },
3264
3225
  rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),
3265
- version: `${host?.version() ?? ""}:${this.assetVersion}`
3226
+ version: `${host?.version() ?? ""}:${this.assetVersion}`,
3227
+ globalLight: host?.globalLight?.(),
3228
+ pattern: host?.pattern ? (id) => host.pattern(id) : void 0
3266
3229
  };
3267
3230
  }
3268
3231
  /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */
@@ -4535,8 +4498,29 @@ function baseFields(layer, ctx) {
4535
4498
  if (layer.clipped) out.clipped = true;
4536
4499
  if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
4537
4500
  if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };
4501
+ if (layer.effects) out.effects = serializeEffects(layer.effects);
4502
+ return out;
4503
+ }
4504
+ function serializeEffects(effects) {
4505
+ const out = { enabled: effects.enabled, items: JSON.parse(JSON.stringify(effects.items)) };
4506
+ if (effects.scale !== void 0 && effects.scale !== 1) out.scale = effects.scale;
4507
+ if (effects.psd) out.psd = { key: effects.psd.key, data: bytesToBase64(effects.psd.data) };
4538
4508
  return out;
4539
4509
  }
4510
+ function deserializeEffects(raw) {
4511
+ if (!raw || typeof raw !== "object") return void 0;
4512
+ const r = raw;
4513
+ let psd;
4514
+ const p = r.psd;
4515
+ if (p && typeof p.key === "string" && typeof p.data === "string") {
4516
+ try {
4517
+ psd = { key: p.key, data: base64ToBytes(p.data) };
4518
+ } catch {
4519
+ psd = void 0;
4520
+ }
4521
+ }
4522
+ return normalizeLayerEffects({ ...r, psd });
4523
+ }
4540
4524
  function serializeMask(mask, hint, ctx) {
4541
4525
  const out = { defaultValue: mask.defaultValue };
4542
4526
  if (!mask.source) {
@@ -4739,6 +4723,8 @@ function deserializeLayer(raw, ctx) {
4739
4723
  for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
4740
4724
  if (Object.keys(locks).length) base.locks = locks;
4741
4725
  }
4726
+ const effects = deserializeEffects(r.effects);
4727
+ if (effects && r.type !== "adjustment") base.effects = effects;
4742
4728
  switch (r.type) {
4743
4729
  case "pixel": {
4744
4730
  const layer = {
@@ -5021,6 +5007,77 @@ function removeLayerFilter(layers, id, filterId) {
5021
5007
  else delete next.filters;
5022
5008
  return mapLayer(layers, id, () => next);
5023
5009
  }
5010
+ function requireEffectLayer(layers, id, action) {
5011
+ const layer = requireLayer(layers, id).layer;
5012
+ if (layer.type === "adjustment") fail(`Layer "${layer.name || id}" is an adjustment layer; effects go on pixel, annotation and group layers.`);
5013
+ requireUnlocked(layer, "all", action);
5014
+ return layer;
5015
+ }
5016
+ function layerEffectsOf(layer) {
5017
+ return layer.effects ?? { enabled: true, items: [] };
5018
+ }
5019
+ function guard(fn) {
5020
+ try {
5021
+ return fn();
5022
+ } catch (e) {
5023
+ if (e instanceof LayerEffectError) fail(e.message);
5024
+ throw e;
5025
+ }
5026
+ }
5027
+ function setLayerEffects(layers, id, effects) {
5028
+ const layer = requireEffectLayer(layers, id, "change its effects");
5029
+ const next = { ...layer };
5030
+ if (effects && (effects.items.length || effects.psd || effects.scale !== void 0 || !effects.enabled)) {
5031
+ guard(() => validateLayerEffects(effects));
5032
+ next.effects = effects;
5033
+ } else delete next.effects;
5034
+ return mapLayer(layers, id, () => next);
5035
+ }
5036
+ function addLayerEffect(layers, id, kind, params = {}, index) {
5037
+ if (!isLayerEffectKind(kind)) fail(`Unknown effect kind "${String(kind)}". Kinds: ${LAYER_EFFECT_KINDS.join(", ")}.`);
5038
+ const layer = requireEffectLayer(layers, id, "add effects");
5039
+ const current = layerEffectsOf(layer);
5040
+ const ids = new Set(current.items.map((e) => e.id));
5041
+ let effectId = newEffectId();
5042
+ while (ids.has(effectId)) effectId = newEffectId();
5043
+ const { id: _ignored, kind: _kind, ...rest } = params;
5044
+ const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));
5045
+ const items = [...current.items];
5046
+ const at = index === void 0 ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));
5047
+ items.splice(at, 0, effect);
5048
+ return { layers: setLayerEffects(layers, id, { ...current, items }), effect };
5049
+ }
5050
+ function requireEffect(layer, effectId) {
5051
+ const items = layer.effects?.items ?? [];
5052
+ const i = items.findIndex((e) => e.id === effectId);
5053
+ if (i === -1) fail(`Layer "${layer.name || layer.id}" has no effect "${effectId}". Effects: ${items.map((e) => `${e.id} (${e.kind})`).join(", ") || "none"}; call list_layer_effects.`);
5054
+ return i;
5055
+ }
5056
+ function updateLayerEffect(layers, id, effectId, changes) {
5057
+ const layer = requireEffectLayer(layers, id, "change its effects");
5058
+ const i = requireEffect(layer, effectId);
5059
+ const current = layer.effects;
5060
+ const items = [...current.items];
5061
+ items[i] = guard(() => mergeLayerEffect(items[i], changes));
5062
+ return setLayerEffects(layers, id, { ...current, items });
5063
+ }
5064
+ function removeLayerEffect(layers, id, effectId) {
5065
+ const layer = requireEffectLayer(layers, id, "change its effects");
5066
+ requireEffect(layer, effectId);
5067
+ const current = layer.effects;
5068
+ return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });
5069
+ }
5070
+ function setLayerEffectsEnabled(layers, id, enabled, scale) {
5071
+ const layer = requireEffectLayer(layers, id, "change its effects");
5072
+ if (scale !== void 0 && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);
5073
+ const current = layerEffectsOf(layer);
5074
+ const next = { ...current, enabled };
5075
+ if (scale !== void 0) {
5076
+ if (scale === 1) delete next.scale;
5077
+ else next.scale = scale;
5078
+ }
5079
+ return setLayerEffects(layers, id, next);
5080
+ }
5024
5081
  function parseBlend(value, allowPassThrough = false) {
5025
5082
  if (value === void 0) return void 0;
5026
5083
  if (value === "pass-through" && allowPassThrough) return value;
@@ -7305,7 +7362,8 @@ var ImageEditorController = class {
7305
7362
  this.renderer.layerSources = {
7306
7363
  surfaceOf: (layer) => this.surfaceOf(layer),
7307
7364
  sourceIsOpaque: () => this.sourceIsOpaque(),
7308
- version: () => this.sourceVersion()
7365
+ version: () => this.sourceVersion(),
7366
+ globalLight: () => this.state.globalLight
7309
7367
  };
7310
7368
  this.state = {
7311
7369
  sourceUrl: typeof options.image === "string" ? options.image : null,
@@ -7459,6 +7517,7 @@ var ImageEditorController = class {
7459
7517
  }
7460
7518
  };
7461
7519
  if (s.legacyOutputBlur !== void 0) entry.legacyOutputBlur = s.legacyOutputBlur;
7520
+ if (s.globalLight) entry.globalLight = { ...s.globalLight };
7462
7521
  return entry;
7463
7522
  }
7464
7523
  restoreHistoryEntry(entry) {
@@ -7471,6 +7530,8 @@ var ImageEditorController = class {
7471
7530
  s.outputOpacity = entry.outputOpacity;
7472
7531
  if (entry.legacyOutputBlur !== void 0) s.legacyOutputBlur = entry.legacyOutputBlur;
7473
7532
  else delete s.legacyOutputBlur;
7533
+ if (entry.globalLight) s.globalLight = { ...entry.globalLight };
7534
+ else delete s.globalLight;
7474
7535
  s.selection = entry.selection;
7475
7536
  if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
7476
7537
  this.imageElement = entry.source.element;
@@ -7692,7 +7753,7 @@ var ImageEditorController = class {
7692
7753
  }
7693
7754
  compositorSources() {
7694
7755
  const { width, height } = this.naturalSize;
7695
- return { width, height, surfaceOf: (l) => this.surfaceOf(l), version: this.sourceVersion() };
7756
+ return { width, height, surfaceOf: (l) => this.surfaceOf(l), version: this.sourceVersion(), globalLight: this.state.globalLight };
7696
7757
  }
7697
7758
  sourceIds = /* @__PURE__ */ new WeakMap();
7698
7759
  nextSourceId = 1;
@@ -8104,6 +8165,55 @@ var ImageEditorController = class {
8104
8165
  this.commitLayers(next);
8105
8166
  return findLayer(next, id).layer.filters ?? [];
8106
8167
  }
8168
+ // --- Layer effects ---
8169
+ /** A layer's effects (an empty, enabled list when it has none). */
8170
+ getLayerEffects(id) {
8171
+ return layerEffectsOf(requireLayer(this.state.layers, id).layer);
8172
+ }
8173
+ /**
8174
+ * Adds an effect of `kind` with Photoshop's defaults and `params` applied (one undo step), at
8175
+ * `index` in the layer's effect list (default the end). Returns the new effect and the effects.
8176
+ */
8177
+ addLayerEffect(id, kind, params = {}, index) {
8178
+ const res = addLayerEffect(this.state.layers, id, kind, params, index);
8179
+ this.commitLayers(res.layers);
8180
+ return { effect: res.effect, effects: this.getLayerEffects(id) };
8181
+ }
8182
+ /** Changes one effect (one undo step, or none with `commitToHistory = false` during a drag). Returns the effects. */
8183
+ updateLayerEffect(id, effectId, changes, commitToHistory = true) {
8184
+ this.commitLayers(updateLayerEffect(this.state.layers, id, effectId, changes), void 0, commitToHistory);
8185
+ return this.getLayerEffects(id);
8186
+ }
8187
+ /** Removes one effect (one undo step). Returns the remaining effects. */
8188
+ removeLayerEffect(id, effectId) {
8189
+ this.commitLayers(removeLayerEffect(this.state.layers, id, effectId));
8190
+ return this.getLayerEffects(id);
8191
+ }
8192
+ /** Turns a layer's effects on or off as a whole and, when given, sets Scale Effects (one undo step). */
8193
+ setLayerEffectsEnabled(id, enabled, scale, commitToHistory = true) {
8194
+ this.commitLayers(setLayerEffectsEnabled(this.state.layers, id, enabled, scale), void 0, commitToHistory);
8195
+ return this.getLayerEffects(id);
8196
+ }
8197
+ /** Replaces a layer's effects (one undo step; `null` removes them). Returns the effects. */
8198
+ setLayerEffects(id, effects, commitToHistory = true) {
8199
+ this.commitLayers(setLayerEffects(this.state.layers, id, effects), void 0, commitToHistory);
8200
+ return this.getLayerEffects(id);
8201
+ }
8202
+ /** The light effects with "use global light" follow. */
8203
+ getGlobalLight() {
8204
+ return { ...this.state.globalLight ?? DEFAULT_GLOBAL_LIGHT };
8205
+ }
8206
+ /** Sets the global light (one undo step). `angle` -360..360°, `altitude` 0..90°. Returns it. */
8207
+ setGlobalLight(light, commitToHistory = true) {
8208
+ const next = { ...this.getGlobalLight(), ...light };
8209
+ if (!(Number.isFinite(next.angle) && next.angle >= -360 && next.angle <= 360)) throw new LayerOperationError(`angle must be from -360 to 360 degrees, got ${next.angle}.`);
8210
+ if (!(Number.isFinite(next.altitude) && next.altitude >= 0 && next.altitude <= 90)) throw new LayerOperationError(`altitude must be from 0 to 90 degrees, got ${next.altitude}.`);
8211
+ if (commitToHistory) this.recordHistory();
8212
+ if (next.angle === DEFAULT_GLOBAL_LIGHT.angle && next.altitude === DEFAULT_GLOBAL_LIGHT.altitude) delete this.state.globalLight;
8213
+ else this.state.globalLight = next;
8214
+ this.notify();
8215
+ return this.getGlobalLight();
8216
+ }
8107
8217
  /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */
8108
8218
  async compositeToSurface(layers, rect) {
8109
8219
  const { width, height } = this.naturalSize;
@@ -8133,7 +8243,8 @@ var ImageEditorController = class {
8133
8243
  }
8134
8244
  /**
8135
8245
  * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,
8136
- * opacity and blend; masks, fill and filters are baked), as one undo step. Returns the merged id.
8246
+ * opacity and blend; masks, fill, filters and both layers' effects are baked), as one undo step.
8247
+ * Returns the merged id.
8137
8248
  */
8138
8249
  async mergeDown(id) {
8139
8250
  const loc = requireLayer(this.state.layers, id);
@@ -8145,7 +8256,9 @@ var ImageEditorController = class {
8145
8256
  if (!upper.visible) throw new LayerOperationError(`Layer "${upper.name || id}" is hidden; show it or delete it instead of merging.`);
8146
8257
  requireUnlocked(upper, "all", "merge it");
8147
8258
  requireUnlocked(lower, "pixels", "merge into it");
8148
- const rect = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
8259
+ let rect = unionRect(this.paintBounds(lower), upper.type === "adjustment" ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };
8260
+ const reach = Math.max(effectsMargin(lower.effects), upper.type === "adjustment" ? 0 : effectsMargin(upper.effects));
8261
+ if (reach) rect = { x: rect.x - reach, y: rect.y - reach, width: rect.width + 2 * reach, height: rect.height + 2 * reach };
8149
8262
  const base = { ...lower, blend: "normal", opacity: 1 };
8150
8263
  delete base.clipped;
8151
8264
  const surface = await this.compositeToSurface([base, upper], rect);
@@ -8154,6 +8267,7 @@ var ImageEditorController = class {
8154
8267
  delete merged.fillOpacity;
8155
8268
  delete merged.filters;
8156
8269
  delete merged.source;
8270
+ delete merged.effects;
8157
8271
  let next = mapLayer(this.state.layers, id, () => null);
8158
8272
  next = mapLayer(next, lower.id, () => merged);
8159
8273
  this.commitLayers(next, () => {
@@ -9534,6 +9648,9 @@ var ImageEditorController = class {
9534
9648
  if (typeof data.legacyOutputBlur === "number" && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;
9535
9649
  else delete s.legacyOutputBlur;
9536
9650
  s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;
9651
+ const light = data.globalLight;
9652
+ if (light && Number.isFinite(light.angle) && Number.isFinite(light.altitude)) s.globalLight = { angle: light.angle, altitude: Math.min(90, Math.max(0, light.altitude)) };
9653
+ else delete s.globalLight;
9537
9654
  if (!this.imageElement && typeof data.width === "number" && typeof data.height === "number" && data.width > 0 && data.height > 0) {
9538
9655
  s.imageDimensions = { width: data.width, height: data.height };
9539
9656
  }
@@ -9547,6 +9664,7 @@ var ImageEditorController = class {
9547
9664
  s.crop = data.crop ? { ...data.crop } : null;
9548
9665
  s.transform = { ...data.transform };
9549
9666
  s.activeLayerId = null;
9667
+ delete s.globalLight;
9550
9668
  }
9551
9669
  s.activeTarget = "pixels";
9552
9670
  this.viewKey = [];
@@ -9556,7 +9674,7 @@ var ImageEditorController = class {
9556
9674
  serializeState() {
9557
9675
  const { width, height } = this.naturalSize;
9558
9676
  const view = legacyView(this.stack(), width, height, null);
9559
- if (view.exact) {
9677
+ if (view.exact && !this.state.globalLight) {
9560
9678
  return {
9561
9679
  // States without adjustment layers keep schema 1, so they round-trip unchanged.
9562
9680
  version: view.adjustments.layers?.length ? 2 : 1,
@@ -9583,6 +9701,7 @@ var ImageEditorController = class {
9583
9701
  opacity: this.state.outputOpacity
9584
9702
  };
9585
9703
  if (this.state.legacyOutputBlur !== void 0) out.legacyOutputBlur = this.state.legacyOutputBlur;
9704
+ if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };
9586
9705
  return out;
9587
9706
  }
9588
9707
  };
@@ -10820,6 +10939,7 @@ function describeLayer(layer, activeId, boundsOf) {
10820
10939
  clipped: Boolean(layer.clipped),
10821
10940
  locks: layer.locks ? { ...layer.locks } : {},
10822
10941
  mask: maskSummary(layer.mask),
10942
+ effects: layer.effects ? { enabled: layer.effects.enabled, scale: layer.effects.scale ?? 1, items: layer.effects.items.map((e) => ({ id: e.id, kind: e.kind, enabled: e.enabled })) } : null,
10823
10943
  active: layer.id === activeId
10824
10944
  };
10825
10945
  switch (layer.type) {
@@ -10879,7 +10999,7 @@ function createImageLayerTools(controller) {
10879
10999
  const listLayers = {
10880
11000
  name: "list_layers",
10881
11001
  title: "List image layers",
10882
- description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
11002
+ description: "Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, an effects summary (master switch, scale, effect ids and kinds; details from list_layer_effects), filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.",
10883
11003
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
10884
11004
  annotations: { title: "List image layers", ...READ2 },
10885
11005
  handler: () => {
@@ -11315,9 +11435,338 @@ function createImageLayerTools(controller) {
11315
11435
  ];
11316
11436
  }
11317
11437
 
11318
- // src/image-editor/paint-tools.ts
11438
+ // src/image-editor/effect-tools.ts
11439
+ var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11319
11440
  var WRITE3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
11441
+ var WRITE_IDEMPOTENT3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11320
11442
  var DESTRUCTIVE2 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11443
+ var LAYER_ID3 = { type: "string", description: "Layer id from list_layers (a pixel, annotation or group layer)." };
11444
+ var EFFECT_ID = { type: "string", description: "Effect id from list_layer_effects or add_layer_effect." };
11445
+ var BLEND_IDS = BLEND_MODES.map((m) => m.id);
11446
+ var DEFS = {
11447
+ color: { type: "string", pattern: "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$", description: "Colour as #rrggbb." },
11448
+ contour: {
11449
+ type: "object",
11450
+ description: "A transfer curve over the effect coverage: points { input, output } from 0 to 1 (at least 2), sorted by input. Linear is [{0,0},{1,1}].",
11451
+ properties: {
11452
+ name: { type: "string", description: 'Display name (e.g. "Cone", "Gaussian").' },
11453
+ points: {
11454
+ type: "array",
11455
+ minItems: 2,
11456
+ description: "Curve points.",
11457
+ items: {
11458
+ type: "object",
11459
+ properties: {
11460
+ input: { type: "number", minimum: 0, maximum: 1, description: "Coverage in (0..1)." },
11461
+ output: { type: "number", minimum: 0, maximum: 1, description: "Coverage out (0..1)." },
11462
+ corner: { type: "boolean", description: "Sharp corner instead of a smooth point (default false)." }
11463
+ },
11464
+ required: ["input", "output"],
11465
+ additionalProperties: false
11466
+ }
11467
+ }
11468
+ },
11469
+ required: ["name", "points"],
11470
+ additionalProperties: false
11471
+ },
11472
+ gradient: {
11473
+ type: "object",
11474
+ description: "A gradient: colour stops, optional opacity stops and its layout.",
11475
+ properties: {
11476
+ stops: {
11477
+ type: "array",
11478
+ minItems: 1,
11479
+ description: "Colour stops { position 0..1, color #rrggbb }.",
11480
+ items: {
11481
+ type: "object",
11482
+ properties: { position: { type: "number", minimum: 0, maximum: 1, description: "0..1 along the gradient." }, color: { $ref: "#/$defs/color" } },
11483
+ required: ["position", "color"],
11484
+ additionalProperties: false
11485
+ }
11486
+ },
11487
+ opacityStops: {
11488
+ type: "array",
11489
+ description: "Opacity stops { position 0..1, opacity 0..1 } (default opaque).",
11490
+ items: {
11491
+ type: "object",
11492
+ properties: {
11493
+ position: { type: "number", minimum: 0, maximum: 1, description: "0..1 along the gradient." },
11494
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "0..1." }
11495
+ },
11496
+ required: ["position", "opacity"],
11497
+ additionalProperties: false
11498
+ }
11499
+ },
11500
+ style: { type: "string", enum: ["linear", "radial", "angle", "reflected", "diamond"], description: "Geometry." },
11501
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Degrees, 90 = bottom to top." },
11502
+ scale: { type: "number", minimum: 0.1, maximum: 1.5, description: "1 = 100 %." },
11503
+ reverse: { type: "boolean", description: "Swap the ends." },
11504
+ align: { type: "boolean", description: "Lay out over the layer bounds (true) or the canvas." },
11505
+ offset: {
11506
+ type: "object",
11507
+ description: "Centre offset as fractions of the bounds.",
11508
+ properties: { x: { type: "number", minimum: -1, maximum: 1, description: "Fraction of the width." }, y: { type: "number", minimum: -1, maximum: 1, description: "Fraction of the height." } },
11509
+ required: ["x", "y"],
11510
+ additionalProperties: false
11511
+ }
11512
+ },
11513
+ required: ["stops", "style", "angle", "scale", "reverse", "align", "offset"],
11514
+ additionalProperties: false
11515
+ },
11516
+ phase: {
11517
+ type: "object",
11518
+ description: "Pattern origin offset in px.",
11519
+ properties: { x: { type: "number", description: "px." }, y: { type: "number", description: "px." } },
11520
+ required: ["x", "y"],
11521
+ additionalProperties: false
11522
+ },
11523
+ fill: {
11524
+ description: "What a stroke or glow paints with. Glows take color or gradient only.",
11525
+ anyOf: [
11526
+ {
11527
+ type: "object",
11528
+ properties: { type: { const: "color", description: "Solid colour." }, color: { $ref: "#/$defs/color" } },
11529
+ required: ["type", "color"],
11530
+ additionalProperties: false
11531
+ },
11532
+ {
11533
+ type: "object",
11534
+ properties: { type: { const: "gradient", description: "Gradient." }, gradient: { $ref: "#/$defs/gradient" } },
11535
+ required: ["type", "gradient"],
11536
+ additionalProperties: false
11537
+ },
11538
+ {
11539
+ type: "object",
11540
+ properties: {
11541
+ type: { const: "pattern", description: "Pattern (strokes only)." },
11542
+ patternId: { type: "string", description: "Pattern id from list_layer_effects.patterns." },
11543
+ name: { type: "string", description: "Display name." },
11544
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "1 = 100 %." },
11545
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Rotation in degrees (default 0)." },
11546
+ link: { type: "boolean", description: "Tile from the layer (default true) or the canvas origin." },
11547
+ phase: { $ref: "#/$defs/phase" }
11548
+ },
11549
+ required: ["type", "patternId", "scale"],
11550
+ additionalProperties: false
11551
+ }
11552
+ ]
11553
+ }
11554
+ };
11555
+ var nullable = (schema, description) => ({ anyOf: [schema, { type: "null" }], description });
11556
+ function settingsSchema(forUpdate) {
11557
+ const opt = (schema, description) => forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description };
11558
+ return {
11559
+ type: "object",
11560
+ description: forUpdate ? "Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect." : "Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.",
11561
+ properties: {
11562
+ enabled: { type: "boolean", description: "All kinds: false keeps the effect but hides it (default true)." },
11563
+ blend: { type: "string", enum: BLEND_IDS, description: "All but bevel-emboss: blend mode (shadows multiply, glows screen, others normal)." },
11564
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "All but bevel-emboss: 0..1 (shadows / glows 0.75, satin 0.5, others 1)." },
11565
+ color: { $ref: "#/$defs/color", description: "drop-shadow, inner-shadow, satin, color-overlay: #rrggbb." },
11566
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Shadows, satin, bevel-emboss: light direction in degrees, counter-clockwise from 3 o'clock (default 120; satin 19)." },
11567
+ useGlobalLight: { type: "boolean", description: "Shadows, bevel-emboss: follow the document global light instead of angle (default true)." },
11568
+ distance: { type: "number", minimum: 0, maximum: 3e4, description: "Shadows, satin: offset in px (default 5; satin 11)." },
11569
+ spread: { type: "number", minimum: 0, maximum: 1, description: "drop-shadow, outer-glow: 0..1 of the size made solid (default 0)." },
11570
+ choke: { type: "number", minimum: 0, maximum: 1, description: "inner-shadow, inner-glow: 0..1 of the size made solid (default 0)." },
11571
+ size: { type: "number", minimum: 0, maximum: 250, description: "Shadows, glows, satin, bevel-emboss: blur / size in px (default 5; satin 14); stroke: width in px (default 3)." },
11572
+ contour: opt({ $ref: "#/$defs/contour" }, "Shadows, glows, satin: coverage contour (default linear)."),
11573
+ antiAlias: { type: "boolean", description: "Shadows, glows, satin: smooth the contour (default false; satin true)." },
11574
+ noise: { type: "number", minimum: 0, maximum: 1, description: "Shadows, glows: speckle 0..1 (default 0)." },
11575
+ knocksOut: { type: "boolean", description: "drop-shadow: the layer hides the shadow beneath its see-through fill (default true)." },
11576
+ fill: { $ref: "#/$defs/fill", description: "outer-glow, inner-glow (color or gradient), stroke (color, gradient or pattern)." },
11577
+ technique: { type: "string", enum: ["softer", "precise", "smooth", "chisel-hard", "chisel-soft"], description: "Glows: softer | precise (default softer); bevel-emboss: smooth | chisel-hard | chisel-soft (default smooth)." },
11578
+ range: { type: "number", minimum: 0.01, maximum: 1, description: "Glows: the part of the glow the contour spans (default 0.5)." },
11579
+ jitter: { type: "number", minimum: 0, maximum: 1, description: "Glows: gradient jitter 0..1 (stored, not rendered; default 0)." },
11580
+ source: { type: "string", enum: ["edge", "center"], description: "inner-glow: glow from the edge (default) or the centre." },
11581
+ style: { type: "string", enum: ["inner-bevel", "outer-bevel", "emboss", "pillow-emboss", "stroke-emboss"], description: "bevel-emboss: style (default inner-bevel)." },
11582
+ depth: { type: "number", minimum: 0, maximum: 10, description: "bevel-emboss: depth, 1 = 100 % (default 1)." },
11583
+ direction: { type: "string", enum: ["up", "down"], description: "bevel-emboss: raised (up, default) or sunk (down)." },
11584
+ soften: { type: "number", minimum: 0, maximum: 16, description: "bevel-emboss: blur of the height in px (default 0)." },
11585
+ altitude: { type: "number", minimum: 0, maximum: 90, description: "bevel-emboss: light altitude in degrees (default 30)." },
11586
+ glossContour: opt({ $ref: "#/$defs/contour" }, "bevel-emboss: gloss contour over the shading (default linear)."),
11587
+ highlightColor: { $ref: "#/$defs/color", description: "bevel-emboss: highlight colour (default #ffffff)." },
11588
+ highlightBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: highlight mode (default screen)." },
11589
+ highlightOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: highlight opacity (default 0.75)." },
11590
+ shadowColor: { $ref: "#/$defs/color", description: "bevel-emboss: shadow colour (default #000000)." },
11591
+ shadowBlend: { type: "string", enum: BLEND_IDS, description: "bevel-emboss: shadow mode (default multiply)." },
11592
+ shadowOpacity: { type: "number", minimum: 0, maximum: 1, description: "bevel-emboss: shadow opacity (default 0.75)." },
11593
+ contourElement: opt(
11594
+ {
11595
+ type: "object",
11596
+ properties: {
11597
+ contour: { $ref: "#/$defs/contour" },
11598
+ range: { type: "number", minimum: 0, maximum: 1, description: "Stored range (default 0.5)." },
11599
+ antiAlias: { type: "boolean", description: "Smooth the contour." }
11600
+ },
11601
+ required: ["contour", "range", "antiAlias"],
11602
+ additionalProperties: false
11603
+ },
11604
+ "bevel-emboss: the Contour element, shaping the bevel's height profile (default off)."
11605
+ ),
11606
+ texture: opt(
11607
+ {
11608
+ type: "object",
11609
+ properties: {
11610
+ patternId: { type: "string", description: "Pattern id from list_layer_effects.patterns." },
11611
+ name: { type: "string", description: "Display name." },
11612
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "1 = 100 %." },
11613
+ depth: { type: "number", minimum: -10, maximum: 10, description: "1 = +100 %; negative carves the pattern in." },
11614
+ invert: { type: "boolean", description: "Invert the pattern." },
11615
+ link: { type: "boolean", description: "Tile from the layer (default true)." },
11616
+ phase: { $ref: "#/$defs/phase" }
11617
+ },
11618
+ required: ["patternId", "scale", "depth", "invert"],
11619
+ additionalProperties: false
11620
+ },
11621
+ "bevel-emboss: the Texture element, a pattern's luminance as extra height (default off)."
11622
+ ),
11623
+ invert: { type: "boolean", description: "satin: invert (default true)." },
11624
+ gradient: { $ref: "#/$defs/gradient", description: "gradient-overlay: the gradient (default black to white, 90\xB0)." },
11625
+ dither: { type: "boolean", description: "gradient-overlay: dither (stored; default false)." },
11626
+ patternId: { type: "string", description: "pattern-overlay: pattern id from list_layer_effects.patterns (default checker)." },
11627
+ name: { type: "string", description: "pattern-overlay: pattern display name." },
11628
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "pattern-overlay: 1 = 100 % (default 1)." },
11629
+ link: { type: "boolean", description: "pattern-overlay: tile from the layer (default true) or the canvas origin." },
11630
+ phase: { $ref: "#/$defs/phase", description: "pattern-overlay: origin offset in px (default 0, 0)." },
11631
+ position: { type: "string", enum: ["outside", "inside", "center"], description: "stroke: where the stroke sits on the edge (default outside)." }
11632
+ },
11633
+ additionalProperties: false
11634
+ };
11635
+ }
11636
+ function patternList() {
11637
+ return builtinPatternIds().map((id) => {
11638
+ const p = builtinPattern(id);
11639
+ return { id, name: p.name, width: p.width, height: p.height, builtin: true };
11640
+ });
11641
+ }
11642
+ function describeLayerEffects(effects) {
11643
+ return {
11644
+ enabled: effects.enabled,
11645
+ scale: effects.scale ?? 1,
11646
+ margin: effectsMargin(effects),
11647
+ fromPsd: Boolean(effects.psd),
11648
+ effects: effects.items.map((e) => ({ ...JSON.parse(JSON.stringify(e)), label: layerEffectLabel(e.kind) }))
11649
+ };
11650
+ }
11651
+ function createImageEffectTools(controller) {
11652
+ const layerArg = (id) => {
11653
+ if (typeof id !== "string" || !id) throw new LayerOperationError("layerId is required (a layer id from list_layers).");
11654
+ requireLayer(controller.getLayers(), id);
11655
+ return id;
11656
+ };
11657
+ const result = (layerId, extra = {}) => ({ layerId, ...extra, ...describeLayerEffects(controller.getLayerEffects(layerId)) });
11658
+ const list = {
11659
+ name: "list_layer_effects",
11660
+ title: "List layer effects",
11661
+ description: "Returns a layer's effects: the master switch (enabled), Scale Effects (scale, 1 = 100 %), margin (px the effects reach beyond the shape), whether they came from a PSD, and every effect with its id, kind, label and all settings. Also returns the document global light and the pattern ids pattern effects can use.",
11662
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID3 }, required: ["layerId"], additionalProperties: false },
11663
+ annotations: { title: "List layer effects", ...READ3 },
11664
+ handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(), kinds: [...LAYER_EFFECT_KINDS] })
11665
+ };
11666
+ const add = {
11667
+ name: "add_layer_effect",
11668
+ title: "Add a layer effect",
11669
+ 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.",
11670
+ inputSchema: {
11671
+ type: "object",
11672
+ $defs: DEFS,
11673
+ properties: {
11674
+ layerId: LAYER_ID3,
11675
+ kind: { type: "string", enum: [...LAYER_EFFECT_KINDS], description: "Effect kind." },
11676
+ settings: settingsSchema(false),
11677
+ index: { type: "integer", minimum: 0, description: "Position in the layer's effect list (default the end)." }
11678
+ },
11679
+ required: ["layerId", "kind"],
11680
+ additionalProperties: false
11681
+ },
11682
+ annotations: { title: "Add a layer effect", ...WRITE3 },
11683
+ handler: ({ layerId, kind, settings, index }) => {
11684
+ const id = layerArg(layerId);
11685
+ if (!isLayerEffectKind(kind)) throw new LayerOperationError(`kind must be one of ${LAYER_EFFECT_KINDS.join(", ")}.`);
11686
+ const { effect } = controller.addLayerEffect(id, kind, settings ?? {}, index);
11687
+ return result(id, { effectId: effect.id });
11688
+ }
11689
+ };
11690
+ const update = {
11691
+ name: "update_layer_effect",
11692
+ title: "Change a layer effect",
11693
+ description: "Changes settings of one effect (only the given ones; fill, gradient, contour and texture objects are replaced whole, null removes an optional contour or texture). The kind cannot change. Validates everything before changing anything. One undo step. Returns the layer's effects.",
11694
+ inputSchema: {
11695
+ type: "object",
11696
+ $defs: DEFS,
11697
+ properties: { layerId: LAYER_ID3, effectId: EFFECT_ID, settings: settingsSchema(true) },
11698
+ required: ["layerId", "effectId", "settings"],
11699
+ additionalProperties: false
11700
+ },
11701
+ annotations: { title: "Change a layer effect", ...WRITE_IDEMPOTENT3 },
11702
+ handler: ({ layerId, effectId, settings }) => {
11703
+ const id = layerArg(layerId);
11704
+ const changes = settings ?? {};
11705
+ if (!Object.keys(changes).length) throw new LayerOperationError("Give at least one setting to change.");
11706
+ controller.updateLayerEffect(id, effectId, changes);
11707
+ return result(id);
11708
+ }
11709
+ };
11710
+ const remove = {
11711
+ name: "remove_layer_effect",
11712
+ title: "Remove a layer effect",
11713
+ description: "Removes one effect from a layer (to hide it but keep its settings, set enabled false with update_layer_effect instead). One undo step. Returns the layer's remaining effects.",
11714
+ inputSchema: { type: "object", properties: { layerId: LAYER_ID3, effectId: EFFECT_ID }, required: ["layerId", "effectId"], additionalProperties: false },
11715
+ annotations: { title: "Remove a layer effect", ...DESTRUCTIVE2 },
11716
+ handler: ({ layerId, effectId }) => {
11717
+ const id = layerArg(layerId);
11718
+ controller.removeLayerEffect(id, effectId);
11719
+ return result(id);
11720
+ }
11721
+ };
11722
+ const setEnabled = {
11723
+ name: "set_layer_effects_enabled",
11724
+ title: "Switch layer effects on or off",
11725
+ description: "Turns all of a layer's effects on or off at once (the master switch; each effect keeps its own enabled flag) and optionally sets Scale Effects, which multiplies every size, distance and pattern scale. One undo step. Returns the layer's effects.",
11726
+ inputSchema: {
11727
+ type: "object",
11728
+ properties: {
11729
+ layerId: LAYER_ID3,
11730
+ enabled: { type: "boolean", description: "true renders the enabled effects; false hides them all." },
11731
+ scale: { type: "number", minimum: 0.01, maximum: 10, description: "Scale Effects, 1 = 100 % (unchanged when left out)." }
11732
+ },
11733
+ required: ["layerId", "enabled"],
11734
+ additionalProperties: false
11735
+ },
11736
+ annotations: { title: "Switch layer effects on or off", ...WRITE_IDEMPOTENT3 },
11737
+ handler: ({ layerId, enabled, scale }) => {
11738
+ const id = layerArg(layerId);
11739
+ controller.setLayerEffectsEnabled(id, enabled, scale);
11740
+ return result(id);
11741
+ }
11742
+ };
11743
+ const light = {
11744
+ name: "set_global_light",
11745
+ title: "Set the global light",
11746
+ description: "Sets the document global light that shadows and bevels with useGlobalLight follow (Photoshop default: angle 120\xB0, altitude 30\xB0). One undo step. Returns the light.",
11747
+ inputSchema: {
11748
+ type: "object",
11749
+ properties: {
11750
+ angle: { type: "number", minimum: -360, maximum: 360, description: "Degrees, counter-clockwise from 3 o'clock: where the light comes from (unchanged when left out)." },
11751
+ altitude: { type: "number", minimum: 0, maximum: 90, description: "Degrees above the canvas, for bevels (unchanged when left out)." }
11752
+ },
11753
+ additionalProperties: false
11754
+ },
11755
+ annotations: { title: "Set the global light", ...WRITE_IDEMPOTENT3 },
11756
+ handler: ({ angle, altitude }) => {
11757
+ if (angle === void 0 && altitude === void 0) throw new LayerOperationError("Give angle, altitude or both.");
11758
+ const next = {};
11759
+ if (angle !== void 0) next.angle = angle;
11760
+ if (altitude !== void 0) next.altitude = altitude;
11761
+ return { globalLight: controller.setGlobalLight(next) };
11762
+ }
11763
+ };
11764
+ return [list, add, update, remove, setEnabled, light];
11765
+ }
11766
+
11767
+ // src/image-editor/paint-tools.ts
11768
+ var WRITE4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
11769
+ var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11321
11770
  var TARGET = {
11322
11771
  layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
11323
11772
  channel: { type: "string", enum: ["pixels", "mask"], description: "pixels (default for a pixel layer) or the layer's mask: black hides, white reveals." }
@@ -11375,7 +11824,7 @@ function createImagePaintTools(controller) {
11375
11824
  required: ["points"],
11376
11825
  additionalProperties: false
11377
11826
  },
11378
- annotations: { title: "Paint a brush stroke", ...WRITE3 },
11827
+ annotations: { title: "Paint a brush stroke", ...WRITE4 },
11379
11828
  handler: (args) => controller.paintStroke({
11380
11829
  tool: args.tool,
11381
11830
  points: args.points,
@@ -11399,7 +11848,7 @@ function createImagePaintTools(controller) {
11399
11848
  },
11400
11849
  additionalProperties: false
11401
11850
  },
11402
- annotations: { title: "Fill pixels", ...WRITE3 },
11851
+ annotations: { title: "Fill pixels", ...WRITE4 },
11403
11852
  handler: (args) => controller.fillPixels({
11404
11853
  color: args.color,
11405
11854
  opacity: args.opacity,
@@ -11421,15 +11870,15 @@ function createImagePaintTools(controller) {
11421
11870
  },
11422
11871
  additionalProperties: false
11423
11872
  },
11424
- annotations: { title: "Clear pixels", ...DESTRUCTIVE2 },
11873
+ annotations: { title: "Clear pixels", ...DESTRUCTIVE3 },
11425
11874
  handler: (args) => controller.clearPixels({ opacity: args.opacity, useSelection: args.useSelection, ...readTarget(args) })
11426
11875
  };
11427
11876
  return [paintStroke, fill, clear];
11428
11877
  }
11429
11878
 
11430
11879
  // src/image-editor/psd-tools.ts
11431
- var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11432
- var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11880
+ var READ4 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11881
+ var DESTRUCTIVE4 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
11433
11882
  var DATA = {
11434
11883
  type: "string",
11435
11884
  minLength: 1,
@@ -11449,7 +11898,7 @@ function createImagePsdTools(controller) {
11449
11898
  title: "Inspect a PSD",
11450
11899
  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.",
11451
11900
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11452
- annotations: { title: "Inspect a PSD", ...READ3 },
11901
+ annotations: { title: "Inspect a PSD", ...READ4 },
11453
11902
  handler: ({ data }) => {
11454
11903
  const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });
11455
11904
  return {
@@ -11467,7 +11916,7 @@ function createImagePsdTools(controller) {
11467
11916
  title: "Open a PSD as the document",
11468
11917
  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 and locks map directly; type, smart-object, 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).",
11469
11918
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11470
- annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE3 },
11919
+ annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE4 },
11471
11920
  handler: ({ data }) => {
11472
11921
  const res = controller.loadPsd(psdBytesFromBase64(data));
11473
11922
  const s = controller.getState();
@@ -11486,7 +11935,7 @@ function createImagePsdTools(controller) {
11486
11935
  title: "Export the document as a PSD",
11487
11936
  description: "Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers and masks, in the source frame (crop and rotation are not applied). 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.",
11488
11937
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
11489
- annotations: { title: "Export the document as a PSD", ...READ3 },
11938
+ annotations: { title: "Export the document as a PSD", ...READ4 },
11490
11939
  handler: async () => {
11491
11940
  const res = await controller.toPsd();
11492
11941
  return {
@@ -11740,6 +12189,7 @@ function createImageEditorToolProvider(controller, options = {}) {
11740
12189
  ...createImageSelectionTools(controller),
11741
12190
  ...createImageEditTools(controller),
11742
12191
  ...createImageLayerTools(controller),
12192
+ ...createImageEffectTools(controller),
11743
12193
  ...createImagePaintTools(controller),
11744
12194
  ...createPigmentTools(),
11745
12195
  ...createImagePsdTools(controller)
@@ -12427,6 +12877,7 @@ export {
12427
12877
  XYZ_D50_TO_SRGB,
12428
12878
  ZlibError,
12429
12879
  activeAdjustmentLayers,
12880
+ addLayerEffect,
12430
12881
  addLayerFilter,
12431
12882
  addToHistogram,
12432
12883
  adjustmentKindLabel,
@@ -12490,6 +12941,7 @@ export {
12490
12941
  createImageCaptureSurface,
12491
12942
  createImageEditTools,
12492
12943
  createImageEditorToolProvider,
12944
+ createImageEffectTools,
12493
12945
  createImageLayerTools,
12494
12946
  createImagePaintTools,
12495
12947
  createImagePsdTools,
@@ -12516,7 +12968,9 @@ export {
12516
12968
  depthMaskAlpha,
12517
12969
  depthRasterMask,
12518
12970
  describeLayer,
12971
+ describeLayerEffects,
12519
12972
  describeSelection,
12973
+ deserializeEffects,
12520
12974
  deserializeLayer,
12521
12975
  deserializeLayers,
12522
12976
  deserializeSelection,
@@ -12601,6 +13055,7 @@ export {
12601
13055
  isSpatialFilter,
12602
13056
  kelvinToTemperature,
12603
13057
  layerBadge,
13058
+ layerEffectsOf,
12604
13059
  layerLabel,
12605
13060
  layerMaskCoverage,
12606
13061
  layerMaskFromSelection,
@@ -12686,6 +13141,7 @@ export {
12686
13141
  removeAlpha,
12687
13142
  removeCurvePoint,
12688
13143
  removeLayer,
13144
+ removeLayerEffect,
12689
13145
  removeLayerFilter,
12690
13146
  renderLayeredBase,
12691
13147
  requireLayer,
@@ -12708,11 +13164,14 @@ export {
12708
13164
  selectionToCoverage,
12709
13165
  selectionToPixels,
12710
13166
  selectionValueAt,
13167
+ serializeEffects,
12711
13168
  serializeLayer,
12712
13169
  serializeLayers,
12713
13170
  serializeLayersExternal,
12714
13171
  serializeSelection,
12715
13172
  setControlPath,
13173
+ setLayerEffects,
13174
+ setLayerEffectsEnabled,
12716
13175
  setLayerMask,
12717
13176
  setLayerProperties,
12718
13177
  setLum,
@@ -12746,6 +13205,7 @@ export {
12746
13205
  uniformTile,
12747
13206
  unionRect,
12748
13207
  unprojectCanvasPoint,
13208
+ updateLayerEffect,
12749
13209
  updateLayerFilter,
12750
13210
  updateLayerMaskProps,
12751
13211
  uvToXy,