@pixerate/editor 0.92.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.
Files changed (37) hide show
  1. package/THIRD_PARTY_NOTICES.md +2 -0
  2. package/dist/{chunk-PDCITYBT.cjs → chunk-4F3YDIWW.cjs} +367 -2
  3. package/dist/chunk-4F3YDIWW.cjs.map +1 -0
  4. package/dist/chunk-73L6TROJ.cjs +3685 -0
  5. package/dist/chunk-73L6TROJ.cjs.map +1 -0
  6. package/dist/chunk-747CGOT2.js +3685 -0
  7. package/dist/chunk-747CGOT2.js.map +1 -0
  8. package/dist/{chunk-YLB2WVNA.js → chunk-ISDCJHTO.js} +367 -2
  9. package/dist/chunk-ISDCJHTO.js.map +1 -0
  10. package/dist/image-editor/index.cjs +787 -302
  11. package/dist/image-editor/index.cjs.map +1 -1
  12. package/dist/image-editor/index.d.cts +84 -18
  13. package/dist/image-editor/index.d.ts +84 -18
  14. package/dist/image-editor/index.js +560 -75
  15. package/dist/image-editor/index.js.map +1 -1
  16. package/dist/paint/index.cjs +26 -2
  17. package/dist/paint/index.cjs.map +1 -1
  18. package/dist/paint/index.d.cts +96 -2
  19. package/dist/paint/index.d.ts +96 -2
  20. package/dist/paint/index.js +25 -1
  21. package/dist/raster/index.cjs +78 -2
  22. package/dist/raster/index.cjs.map +1 -1
  23. package/dist/raster/index.d.cts +99 -348
  24. package/dist/raster/index.d.ts +99 -348
  25. package/dist/raster/index.js +78 -2
  26. package/dist/{surface-fJhkmNQn.d.cts → surface-CuBg2c6k.d.cts} +1 -1
  27. package/dist/{surface-fJhkmNQn.d.ts → surface-CuBg2c6k.d.ts} +1 -1
  28. package/dist/zlib-BJ6U7LuB.d.cts +795 -0
  29. package/dist/zlib-BjQkpMFi.d.ts +795 -0
  30. package/package.json +3 -3
  31. package/skills/pixerate-image-editor/SKILL.md +63 -4
  32. package/dist/chunk-H543GD6T.cjs +0 -1696
  33. package/dist/chunk-H543GD6T.cjs.map +0 -1
  34. package/dist/chunk-P5TPUNBZ.js +0 -1696
  35. package/dist/chunk-P5TPUNBZ.js.map +0 -1
  36. package/dist/chunk-PDCITYBT.cjs.map +0 -1
  37. package/dist/chunk-YLB2WVNA.js.map +0 -1
@@ -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,30 +31,48 @@ 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,
35
49
  DabWalker,
36
50
  MAX_BRUSH_SIZE,
51
+ PIGMENT_BANDS,
52
+ PIGMENT_LAMBDA_MIN,
53
+ PIGMENT_LAMBDA_STEP,
54
+ PIGMENT_WAVELENGTHS,
37
55
  StrokeCoverage,
38
56
  applyStrokeToTile,
39
57
  coverageTileCoord,
58
+ createPigment,
59
+ createPigmentTools,
40
60
  dabCoverage,
41
61
  dabRect,
62
+ lerpPigment,
63
+ lerpPigmentInto,
64
+ mixPigments,
42
65
  normalizeBrush,
43
66
  rasterizeDab,
67
+ reflectanceToRgb,
68
+ rgbToReflectance,
69
+ setPigmentColor,
44
70
  smoothPoints,
45
71
  tipFalloff,
46
72
  tipReach,
47
73
  tipShape,
48
74
  walkDabs
49
- } from "../chunk-YLB2WVNA.js";
75
+ } from "../chunk-ISDCJHTO.js";
50
76
  import {
51
77
  BLEND_MODES,
52
78
  SurfaceDraft,
@@ -1484,59 +1510,6 @@ function normalizeAdjustmentLayers(raw) {
1484
1510
  }
1485
1511
  return out;
1486
1512
  }
1487
- function curveLut(points, size = ADJUSTMENT_LUT_SIZE) {
1488
- 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]);
1489
- const dedup = [];
1490
- for (const p of pts) {
1491
- if (dedup.length && Math.abs(dedup[dedup.length - 1][0] - p[0]) < 1e-6) continue;
1492
- dedup.push(p);
1493
- }
1494
- const out = new Float32Array(size);
1495
- const n = dedup.length;
1496
- if (n < 2) {
1497
- for (let k = 0; k < size; k++) out[k] = k / (size - 1);
1498
- return out;
1499
- }
1500
- const x = dedup.map((p) => p[0]);
1501
- const y = dedup.map((p) => p[1]);
1502
- const m2 = new Float64Array(n);
1503
- if (n > 2) {
1504
- const c = new Float64Array(n);
1505
- const d = new Float64Array(n);
1506
- for (let i = 1; i < n - 1; i++) {
1507
- const h0 = x[i] - x[i - 1];
1508
- const h1 = x[i + 1] - x[i];
1509
- const a = h0 / 6;
1510
- const b = (h0 + h1) / 3;
1511
- const cc = h1 / 6;
1512
- const r = (y[i + 1] - y[i]) / h1 - (y[i] - y[i - 1]) / h0;
1513
- const denom = b - a * c[i - 1];
1514
- c[i] = cc / denom;
1515
- d[i] = (r - a * d[i - 1]) / denom;
1516
- }
1517
- for (let i = n - 2; i >= 1; i--) m2[i] = d[i] - c[i] * m2[i + 1];
1518
- }
1519
- let seg = 0;
1520
- for (let k = 0; k < size; k++) {
1521
- const xv = k / (size - 1);
1522
- if (xv <= x[0]) {
1523
- out[k] = clamp01(y[0]);
1524
- continue;
1525
- }
1526
- if (xv >= x[n - 1]) {
1527
- out[k] = clamp01(y[n - 1]);
1528
- continue;
1529
- }
1530
- while (seg < n - 2 && xv > x[seg + 1]) seg++;
1531
- const x0 = x[seg], x1 = x[seg + 1];
1532
- const h = x1 - x0;
1533
- const a = (x1 - xv) / h;
1534
- const b = (xv - x0) / h;
1535
- 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;
1536
- out[k] = clamp01(v);
1537
- }
1538
- return out;
1539
- }
1540
1513
  function curveLutFromLevels(points) {
1541
1514
  return curveLut(points.map((p) => ({ input: p.input / 255, output: p.output / 255 })));
1542
1515
  }
@@ -2688,7 +2661,7 @@ function developTarget(layers, activeLayerId) {
2688
2661
  }
2689
2662
  return pixels.find((l) => l.source === "document") ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;
2690
2663
  }
2691
- 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;
2692
2665
  function legacyView(stack, width, height, activeLayerId = null) {
2693
2666
  const { layers } = stack;
2694
2667
  const target = developTarget(layers, activeLayerId);
@@ -2728,7 +2701,7 @@ function isLegacyShape(stack) {
2728
2701
  const bg = layers[0];
2729
2702
  if (!bg || bg.type !== "pixel" || bg.source !== "document") return false;
2730
2703
  if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;
2731
- 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;
2732
2705
  if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;
2733
2706
  const filters = bg.filters ?? [];
2734
2707
  if (filters.length < 1 || filters.length > 2) return false;
@@ -2744,7 +2717,7 @@ function isLegacyShape(stack) {
2744
2717
  let adjustments = 0;
2745
2718
  for (; i < layers.length && layers[i].type === "adjustment"; i++) {
2746
2719
  const l = layers[i];
2747
- if (l.clipped || l.locks || l.mask?.source) return false;
2720
+ if (l.clipped || l.locks || l.mask?.source || l.effects) return false;
2748
2721
  adjustments++;
2749
2722
  }
2750
2723
  for (; i < layers.length; i++) {
@@ -3250,7 +3223,9 @@ var ImageEditorRenderer = class {
3250
3223
  return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;
3251
3224
  },
3252
3225
  rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),
3253
- 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
3254
3229
  };
3255
3230
  }
3256
3231
  /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */
@@ -4523,8 +4498,29 @@ function baseFields(layer, ctx) {
4523
4498
  if (layer.clipped) out.clipped = true;
4524
4499
  if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);
4525
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) };
4526
4508
  return out;
4527
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
+ }
4528
4524
  function serializeMask(mask, hint, ctx) {
4529
4525
  const out = { defaultValue: mask.defaultValue };
4530
4526
  if (!mask.source) {
@@ -4727,6 +4723,8 @@ function deserializeLayer(raw, ctx) {
4727
4723
  for (const k of ["pixels", "transparency", "position", "all"]) if (r.locks[k] === true) locks[k] = true;
4728
4724
  if (Object.keys(locks).length) base.locks = locks;
4729
4725
  }
4726
+ const effects = deserializeEffects(r.effects);
4727
+ if (effects && r.type !== "adjustment") base.effects = effects;
4730
4728
  switch (r.type) {
4731
4729
  case "pixel": {
4732
4730
  const layer = {
@@ -5009,6 +5007,77 @@ function removeLayerFilter(layers, id, filterId) {
5009
5007
  else delete next.filters;
5010
5008
  return mapLayer(layers, id, () => next);
5011
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
+ }
5012
5081
  function parseBlend(value, allowPassThrough = false) {
5013
5082
  if (value === void 0) return void 0;
5014
5083
  if (value === "pass-through" && allowPassThrough) return value;
@@ -7293,7 +7362,8 @@ var ImageEditorController = class {
7293
7362
  this.renderer.layerSources = {
7294
7363
  surfaceOf: (layer) => this.surfaceOf(layer),
7295
7364
  sourceIsOpaque: () => this.sourceIsOpaque(),
7296
- version: () => this.sourceVersion()
7365
+ version: () => this.sourceVersion(),
7366
+ globalLight: () => this.state.globalLight
7297
7367
  };
7298
7368
  this.state = {
7299
7369
  sourceUrl: typeof options.image === "string" ? options.image : null,
@@ -7447,6 +7517,7 @@ var ImageEditorController = class {
7447
7517
  }
7448
7518
  };
7449
7519
  if (s.legacyOutputBlur !== void 0) entry.legacyOutputBlur = s.legacyOutputBlur;
7520
+ if (s.globalLight) entry.globalLight = { ...s.globalLight };
7450
7521
  return entry;
7451
7522
  }
7452
7523
  restoreHistoryEntry(entry) {
@@ -7459,6 +7530,8 @@ var ImageEditorController = class {
7459
7530
  s.outputOpacity = entry.outputOpacity;
7460
7531
  if (entry.legacyOutputBlur !== void 0) s.legacyOutputBlur = entry.legacyOutputBlur;
7461
7532
  else delete s.legacyOutputBlur;
7533
+ if (entry.globalLight) s.globalLight = { ...entry.globalLight };
7534
+ else delete s.globalLight;
7462
7535
  s.selection = entry.selection;
7463
7536
  if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;
7464
7537
  this.imageElement = entry.source.element;
@@ -7680,7 +7753,7 @@ var ImageEditorController = class {
7680
7753
  }
7681
7754
  compositorSources() {
7682
7755
  const { width, height } = this.naturalSize;
7683
- 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 };
7684
7757
  }
7685
7758
  sourceIds = /* @__PURE__ */ new WeakMap();
7686
7759
  nextSourceId = 1;
@@ -8092,6 +8165,55 @@ var ImageEditorController = class {
8092
8165
  this.commitLayers(next);
8093
8166
  return findLayer(next, id).layer.filters ?? [];
8094
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
+ }
8095
8217
  /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */
8096
8218
  async compositeToSurface(layers, rect) {
8097
8219
  const { width, height } = this.naturalSize;
@@ -8121,7 +8243,8 @@ var ImageEditorController = class {
8121
8243
  }
8122
8244
  /**
8123
8245
  * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,
8124
- * 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.
8125
8248
  */
8126
8249
  async mergeDown(id) {
8127
8250
  const loc = requireLayer(this.state.layers, id);
@@ -8133,7 +8256,9 @@ var ImageEditorController = class {
8133
8256
  if (!upper.visible) throw new LayerOperationError(`Layer "${upper.name || id}" is hidden; show it or delete it instead of merging.`);
8134
8257
  requireUnlocked(upper, "all", "merge it");
8135
8258
  requireUnlocked(lower, "pixels", "merge into it");
8136
- 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 };
8137
8262
  const base = { ...lower, blend: "normal", opacity: 1 };
8138
8263
  delete base.clipped;
8139
8264
  const surface = await this.compositeToSurface([base, upper], rect);
@@ -8142,6 +8267,7 @@ var ImageEditorController = class {
8142
8267
  delete merged.fillOpacity;
8143
8268
  delete merged.filters;
8144
8269
  delete merged.source;
8270
+ delete merged.effects;
8145
8271
  let next = mapLayer(this.state.layers, id, () => null);
8146
8272
  next = mapLayer(next, lower.id, () => merged);
8147
8273
  this.commitLayers(next, () => {
@@ -9522,6 +9648,9 @@ var ImageEditorController = class {
9522
9648
  if (typeof data.legacyOutputBlur === "number" && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;
9523
9649
  else delete s.legacyOutputBlur;
9524
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;
9525
9654
  if (!this.imageElement && typeof data.width === "number" && typeof data.height === "number" && data.width > 0 && data.height > 0) {
9526
9655
  s.imageDimensions = { width: data.width, height: data.height };
9527
9656
  }
@@ -9535,6 +9664,7 @@ var ImageEditorController = class {
9535
9664
  s.crop = data.crop ? { ...data.crop } : null;
9536
9665
  s.transform = { ...data.transform };
9537
9666
  s.activeLayerId = null;
9667
+ delete s.globalLight;
9538
9668
  }
9539
9669
  s.activeTarget = "pixels";
9540
9670
  this.viewKey = [];
@@ -9544,7 +9674,7 @@ var ImageEditorController = class {
9544
9674
  serializeState() {
9545
9675
  const { width, height } = this.naturalSize;
9546
9676
  const view = legacyView(this.stack(), width, height, null);
9547
- if (view.exact) {
9677
+ if (view.exact && !this.state.globalLight) {
9548
9678
  return {
9549
9679
  // States without adjustment layers keep schema 1, so they round-trip unchanged.
9550
9680
  version: view.adjustments.layers?.length ? 2 : 1,
@@ -9571,6 +9701,7 @@ var ImageEditorController = class {
9571
9701
  opacity: this.state.outputOpacity
9572
9702
  };
9573
9703
  if (this.state.legacyOutputBlur !== void 0) out.legacyOutputBlur = this.state.legacyOutputBlur;
9704
+ if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };
9574
9705
  return out;
9575
9706
  }
9576
9707
  };
@@ -10808,6 +10939,7 @@ function describeLayer(layer, activeId, boundsOf) {
10808
10939
  clipped: Boolean(layer.clipped),
10809
10940
  locks: layer.locks ? { ...layer.locks } : {},
10810
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,
10811
10943
  active: layer.id === activeId
10812
10944
  };
10813
10945
  switch (layer.type) {
@@ -10867,7 +10999,7 @@ function createImageLayerTools(controller) {
10867
10999
  const listLayers = {
10868
11000
  name: "list_layers",
10869
11001
  title: "List image layers",
10870
- 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.",
10871
11003
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
10872
11004
  annotations: { title: "List image layers", ...READ2 },
10873
11005
  handler: () => {
@@ -11303,9 +11435,338 @@ function createImageLayerTools(controller) {
11303
11435
  ];
11304
11436
  }
11305
11437
 
11306
- // src/image-editor/paint-tools.ts
11438
+ // src/image-editor/effect-tools.ts
11439
+ var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11307
11440
  var WRITE3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
11441
+ var WRITE_IDEMPOTENT3 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11308
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 };
11309
11770
  var TARGET = {
11310
11771
  layerId: { type: "string", description: "Layer to paint (default the active layer; see list_layers)." },
11311
11772
  channel: { type: "string", enum: ["pixels", "mask"], description: "pixels (default for a pixel layer) or the layer's mask: black hides, white reveals." }
@@ -11363,7 +11824,7 @@ function createImagePaintTools(controller) {
11363
11824
  required: ["points"],
11364
11825
  additionalProperties: false
11365
11826
  },
11366
- annotations: { title: "Paint a brush stroke", ...WRITE3 },
11827
+ annotations: { title: "Paint a brush stroke", ...WRITE4 },
11367
11828
  handler: (args) => controller.paintStroke({
11368
11829
  tool: args.tool,
11369
11830
  points: args.points,
@@ -11387,7 +11848,7 @@ function createImagePaintTools(controller) {
11387
11848
  },
11388
11849
  additionalProperties: false
11389
11850
  },
11390
- annotations: { title: "Fill pixels", ...WRITE3 },
11851
+ annotations: { title: "Fill pixels", ...WRITE4 },
11391
11852
  handler: (args) => controller.fillPixels({
11392
11853
  color: args.color,
11393
11854
  opacity: args.opacity,
@@ -11409,15 +11870,15 @@ function createImagePaintTools(controller) {
11409
11870
  },
11410
11871
  additionalProperties: false
11411
11872
  },
11412
- annotations: { title: "Clear pixels", ...DESTRUCTIVE2 },
11873
+ annotations: { title: "Clear pixels", ...DESTRUCTIVE3 },
11413
11874
  handler: (args) => controller.clearPixels({ opacity: args.opacity, useSelection: args.useSelection, ...readTarget(args) })
11414
11875
  };
11415
11876
  return [paintStroke, fill, clear];
11416
11877
  }
11417
11878
 
11418
11879
  // src/image-editor/psd-tools.ts
11419
- var READ3 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
11420
- 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 };
11421
11882
  var DATA = {
11422
11883
  type: "string",
11423
11884
  minLength: 1,
@@ -11437,7 +11898,7 @@ function createImagePsdTools(controller) {
11437
11898
  title: "Inspect a PSD",
11438
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.",
11439
11900
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11440
- annotations: { title: "Inspect a PSD", ...READ3 },
11901
+ annotations: { title: "Inspect a PSD", ...READ4 },
11441
11902
  handler: ({ data }) => {
11442
11903
  const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });
11443
11904
  return {
@@ -11455,7 +11916,7 @@ function createImagePsdTools(controller) {
11455
11916
  title: "Open a PSD as the document",
11456
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).",
11457
11918
  inputSchema: { type: "object", properties: { data: DATA }, required: ["data"], additionalProperties: false },
11458
- annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE3 },
11919
+ annotations: { title: "Open a PSD as the document", ...DESTRUCTIVE4 },
11459
11920
  handler: ({ data }) => {
11460
11921
  const res = controller.loadPsd(psdBytesFromBase64(data));
11461
11922
  const s = controller.getState();
@@ -11474,7 +11935,7 @@ function createImagePsdTools(controller) {
11474
11935
  title: "Export the document as a PSD",
11475
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.",
11476
11937
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
11477
- annotations: { title: "Export the document as a PSD", ...READ3 },
11938
+ annotations: { title: "Export the document as a PSD", ...READ4 },
11478
11939
  handler: async () => {
11479
11940
  const res = await controller.toPsd();
11480
11941
  return {
@@ -11728,7 +12189,9 @@ function createImageEditorToolProvider(controller, options = {}) {
11728
12189
  ...createImageSelectionTools(controller),
11729
12190
  ...createImageEditTools(controller),
11730
12191
  ...createImageLayerTools(controller),
12192
+ ...createImageEffectTools(controller),
11731
12193
  ...createImagePaintTools(controller),
12194
+ ...createPigmentTools(),
11732
12195
  ...createImagePsdTools(controller)
11733
12196
  ]
11734
12197
  };
@@ -12386,6 +12849,10 @@ export {
12386
12849
  MIN_KELVIN,
12387
12850
  MIRED_PER_TEMPERATURE_UNIT,
12388
12851
  PAINT_TOOLS,
12852
+ PIGMENT_BANDS,
12853
+ PIGMENT_LAMBDA_MIN,
12854
+ PIGMENT_LAMBDA_STEP,
12855
+ PIGMENT_WAVELENGTHS,
12389
12856
  PSD_BLEND_KEYS,
12390
12857
  PSD_FILE_ACCEPT,
12391
12858
  PaintGesture,
@@ -12410,6 +12877,7 @@ export {
12410
12877
  XYZ_D50_TO_SRGB,
12411
12878
  ZlibError,
12412
12879
  activeAdjustmentLayers,
12880
+ addLayerEffect,
12413
12881
  addLayerFilter,
12414
12882
  addToHistogram,
12415
12883
  adjustmentKindLabel,
@@ -12473,10 +12941,13 @@ export {
12473
12941
  createImageCaptureSurface,
12474
12942
  createImageEditTools,
12475
12943
  createImageEditorToolProvider,
12944
+ createImageEffectTools,
12476
12945
  createImageLayerTools,
12477
12946
  createImagePaintTools,
12478
12947
  createImagePsdTools,
12479
12948
  createImageSelectionTools,
12949
+ createPigment,
12950
+ createPigmentTools,
12480
12951
  createSelectionMask,
12481
12952
  curveLut,
12482
12953
  curveLutFromLevels,
@@ -12497,7 +12968,9 @@ export {
12497
12968
  depthMaskAlpha,
12498
12969
  depthRasterMask,
12499
12970
  describeLayer,
12971
+ describeLayerEffects,
12500
12972
  describeSelection,
12973
+ deserializeEffects,
12501
12974
  deserializeLayer,
12502
12975
  deserializeLayers,
12503
12976
  deserializeSelection,
@@ -12582,6 +13055,7 @@ export {
12582
13055
  isSpatialFilter,
12583
13056
  kelvinToTemperature,
12584
13057
  layerBadge,
13058
+ layerEffectsOf,
12585
13059
  layerLabel,
12586
13060
  layerMaskCoverage,
12587
13061
  layerMaskFromSelection,
@@ -12595,6 +13069,8 @@ export {
12595
13069
  legacyAdjustmentLayer,
12596
13070
  legacyMaskOf,
12597
13071
  legacyView,
13072
+ lerpPigment,
13073
+ lerpPigmentInto,
12598
13074
  levelsValue,
12599
13075
  linearSrgbToXy,
12600
13076
  linearToSrgb,
@@ -12605,6 +13081,7 @@ export {
12605
13081
  maskSummary,
12606
13082
  migrateSerializedImageEditorState,
12607
13083
  migrateToLayeredState,
13084
+ mixPigments,
12608
13085
  modifySelection,
12609
13086
  moveCurvePoint,
12610
13087
  moveLayer,
@@ -12660,9 +13137,11 @@ export {
12660
13137
  refineCoverage,
12661
13138
  refineEdgeHalo,
12662
13139
  refineSelection,
13140
+ reflectanceToRgb,
12663
13141
  removeAlpha,
12664
13142
  removeCurvePoint,
12665
13143
  removeLayer,
13144
+ removeLayerEffect,
12666
13145
  removeLayerFilter,
12667
13146
  renderLayeredBase,
12668
13147
  requireLayer,
@@ -12673,6 +13152,7 @@ export {
12673
13152
  resolveMosaicRegionBounds,
12674
13153
  rgbToGray,
12675
13154
  rgbToHsl,
13155
+ rgbToReflectance,
12676
13156
  runLayerMenuAction,
12677
13157
  sampleLut,
12678
13158
  sampleToneLut,
@@ -12684,14 +13164,18 @@ export {
12684
13164
  selectionToCoverage,
12685
13165
  selectionToPixels,
12686
13166
  selectionValueAt,
13167
+ serializeEffects,
12687
13168
  serializeLayer,
12688
13169
  serializeLayers,
12689
13170
  serializeLayersExternal,
12690
13171
  serializeSelection,
12691
13172
  setControlPath,
13173
+ setLayerEffects,
13174
+ setLayerEffectsEnabled,
12692
13175
  setLayerMask,
12693
13176
  setLayerProperties,
12694
13177
  setLum,
13178
+ setPigmentColor,
12695
13179
  shiftCoverage,
12696
13180
  smoothCoverage,
12697
13181
  smoothPoints,
@@ -12721,6 +13205,7 @@ export {
12721
13205
  uniformTile,
12722
13206
  unionRect,
12723
13207
  unprojectCanvasPoint,
13208
+ updateLayerEffect,
12724
13209
  updateLayerFilter,
12725
13210
  updateLayerMaskProps,
12726
13211
  uvToXy,