@pixerate/editor 0.102.0 → 0.104.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 (59) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/dist/agent/index.cjs +7 -7
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.js +7 -7
  5. package/dist/canvas/index.cjs +3 -3
  6. package/dist/canvas/index.cjs.map +1 -1
  7. package/dist/canvas/index.js +4 -4
  8. package/dist/chunk-6BU5WU4R.cjs +1 -0
  9. package/dist/chunk-6BU5WU4R.cjs.map +1 -0
  10. package/dist/{chunk-IB53LVGV.js → chunk-DS7KDKPX.js} +1 -1
  11. package/dist/chunk-NQLQ2JOI.js +7176 -0
  12. package/dist/chunk-NQLQ2JOI.js.map +1 -0
  13. package/dist/{chunk-ZZOTHJ4K.cjs → chunk-PV42JITT.cjs} +1 -1
  14. package/dist/{chunk-ZZOTHJ4K.cjs.map → chunk-PV42JITT.cjs.map} +1 -1
  15. package/dist/chunk-VSMM7B3P.cjs +7176 -0
  16. package/dist/chunk-VSMM7B3P.cjs.map +1 -0
  17. package/dist/chunk-XYBJKUAU.js +1 -0
  18. package/dist/chunk-XYBJKUAU.js.map +1 -0
  19. package/dist/compositor-B1ZrKR89.d.ts +835 -0
  20. package/dist/compositor-CTMU-B1t.d.cts +835 -0
  21. package/dist/export-BLpMyCYg.d.ts +3139 -0
  22. package/dist/export-X9rY79p0.d.cts +3139 -0
  23. package/dist/homography-CZ-tFjay.d.cts +50 -0
  24. package/dist/homography-Ctfjoc1b.d.ts +50 -0
  25. package/dist/image-editor/index.cjs +2846 -9407
  26. package/dist/image-editor/index.cjs.map +1 -1
  27. package/dist/image-editor/index.d.cts +24 -3151
  28. package/dist/image-editor/index.d.ts +24 -3151
  29. package/dist/image-editor/index.js +2508 -9069
  30. package/dist/image-editor/index.js.map +1 -1
  31. package/dist/mixer-DG_9Yu2W.d.ts +282 -0
  32. package/dist/mixer-DGcH6HLu.d.cts +282 -0
  33. package/dist/paint/index.cjs +4 -3
  34. package/dist/paint/index.cjs.map +1 -1
  35. package/dist/paint/index.d.cts +3 -281
  36. package/dist/paint/index.d.ts +3 -281
  37. package/dist/paint/index.js +2 -1
  38. package/dist/paint-editor/index.cjs +4417 -0
  39. package/dist/paint-editor/index.cjs.map +1 -0
  40. package/dist/paint-editor/index.d.cts +1539 -0
  41. package/dist/paint-editor/index.d.ts +1539 -0
  42. package/dist/paint-editor/index.js +4417 -0
  43. package/dist/paint-editor/index.js.map +1 -0
  44. package/dist/raster/index.d.cts +6 -835
  45. package/dist/raster/index.d.ts +6 -835
  46. package/dist/{surface-DIA-XatS.d.cts → surface-AfHCH7BR.d.cts} +1 -1
  47. package/dist/{surface-DIA-XatS.d.ts → surface-AfHCH7BR.d.ts} +1 -1
  48. package/dist/tools/index.cjs +3 -3
  49. package/dist/tools/index.cjs.map +1 -1
  50. package/dist/tools/index.js +3 -3
  51. package/dist/vector/index.d.cts +2 -1
  52. package/dist/vector/index.d.ts +2 -1
  53. package/dist/{warp-B8rAguzs.d.ts → warp-CWHZzE3x.d.ts} +2 -49
  54. package/dist/{warp-nYs2DPrx.d.cts → warp-D5jz_tN7.d.cts} +2 -49
  55. package/package.json +6 -1
  56. package/skills/README.md +1 -0
  57. package/skills/pixerate-image-editor/SKILL.md +14 -5
  58. package/skills/pixerate-paint-editor/SKILL.md +203 -0
  59. /package/dist/{chunk-IB53LVGV.js.map → chunk-DS7KDKPX.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","../../src/image-editor/color.ts","../../src/image-editor/geometry.ts","../../src/image-editor/selection/mask.ts","../../src/image-editor/selection/modify.ts","../../src/image-editor/selection/layer-mask.ts","../../src/image-editor/adjustment-layers.ts","../../src/image-editor/filters.ts","../../src/image-editor/histogram.ts","../../src/image-editor/selection/overlay.ts","../../src/image-editor/layers/model.ts","../../src/image-editor/layers/compositor.ts","../../src/image-editor/layers/defaults.ts","../../src/image-editor/layers/migrate.ts","../../src/image-editor/layers/render.ts","../../src/image-editor/renderer.ts","../../src/image-editor/selection/shapes.ts","../../src/image-editor/selection/wand.ts","../../src/image-editor/selection/refine.ts","../../src/image-editor/transform.ts","../../src/image-editor/layers/ops.ts","../../src/image-editor/layers/smart.ts","../../src/image-editor/layers/serialize.ts","../../src/image-editor/psd/binary.ts","../../src/image-editor/psd/descriptor.ts","../../src/image-editor/psd/adjust.ts","../../src/image-editor/psd/compression.ts","../../src/image-editor/psd/file.ts","../../src/image-editor/psd/pixels.ts","../../src/image-editor/psd/effects.ts","../../src/image-editor/psd/patterns.ts","../../src/image-editor/psd/import.ts","../../src/image-editor/psd/export.ts","../../src/image-editor/free-transform.ts","../../src/image-editor/controller.ts","../../src/image-editor/document.ts","../../src/image-editor/controls.ts","../../src/image-editor/selection/tool-options.ts","../../src/image-editor/selection-tools.ts","../../src/image-editor/capture.ts","../../src/image-editor/edit-tools.ts","../../src/image-editor/layer-tools.ts","../../src/image-editor/effect-tools.ts","../../src/image-editor/layers/paint-ui.ts","../../src/image-editor/paint-tools.ts","../../src/image-editor/psd-tools.ts","../../src/image-editor/transform-tools.ts","../../src/image-editor/smart-tools.ts","../../src/image-editor/tools.ts","../../src/image-editor/curve-editing.ts","../../src/image-editor/selection/gesture.ts","../../src/image-editor/mosaic-filter.ts","../../src/image-editor/layers/panel.ts","../../src/image-editor/layers/effects-ui.ts","../../src/image-editor/psd/ui.ts","../../src/image-editor/layers/transform-ui.ts"],"names":["lu","lv","clamp","x","y","px","q","v","op","list","sub","fail","document","lightness","props","effects","mask","layer","patternIds","decoded","out","obj","pct","num","common","moveLayer","WRITE_IDEMPOTENT","LAYER_ID","WRITE","opt","DESTRUCTIVE","READ","TARGET"],"mappings":"AAAA;AACE;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACA;AC9JO,IAAM,uBAAA,EAAyB,CAAA;AAE/B,IAAM,uBAAA,EAAyB,CAAA;AAQ/B,SAAS,uBAAA,CAAwB,KAAA,EAAqC;AAC3E,EAAA,GAAA,CAAI,OAAO,MAAA,IAAU,SAAA,GAAY,CAAC,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA,EAAG,OAAO,sBAAA;AAClE,EAAA,OAAO,MAAA,GAAS,uBAAA,EAAyB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A;AAC6C,IAAA;AACtE,IAAA;AACQ,MAAA;AACW,MAAA;AACA,MAAA;AACU,MAAA;AACA,MAAA;AACA,MAAA;AACF,MAAA;AACL,MAAA;AAC7B,IAAA;AACF,EAAA;AAAA;AAGyD,EAAA;AACxC,IAAA;AACM,IAAA;AACsB,IAAA;AACL,IAAA;AACqB,IAAA;AACA,IAAA;AACzB,IAAA;AACnB,MAAA;AACA,MAAA;AACb,MAAA;AACF,IAAA;AACkC,IAAA;AACnB,MAAA;AACA,MAAA;AACA,MAAA;AACD,MAAA;AACA,MAAA;AACI,MAAA;AACS,MAAA;AACA,MAAA;AAC1B,IAAA;AAC0C,IAAA;AACc,IAAA;AAC5C,IAAA;AACf,EAAA;AAAA;AAGqC,EAAA;AACpB,IAAA;AACA,IAAA;AACuB,IAAA;AACvB,IAAA;AACA,IAAA;AAC6C,MAAA;AAC3C,MAAA;AAChB,IAAA;AACiB,IAAA;AACA,IAAA;AACH,IAAA;AACH,IAAA;AACA,IAAA;AACC,IAAA;AACN,IAAA;AACT,EAAA;AAAA;AAGe,EAAA;AACW,oBAAA;AACN,IAAA;AACH,IAAA;AACH,IAAA;AACA,IAAA;AACG,IAAA;AACF,IAAA;AACA,IAAA;AACf,EAAA;AAAA;AAAA;AAAA;AAAA;AAM+B,EAAA;AACjB,IAAA;AAC0C,IAAA;AACpC,IAAA;AACmC,MAAA;AACtC,QAAA;AACJ,QAAA;AACT,MAAA;AACO,MAAA;AACT,IAAA;AACuB,IAAA;AACT,MAAA;AACL,MAAA;AACT,IAAA;AACwB,IAAA;AACV,MAAA;AACL,MAAA;AACT,IAAA;AACmB,IAAA;AACY,IAAA;AACoD,IAAA;AAC9D,IAAA;AACE,IAAA;AACgD,MAAA;AAC9D,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACF;AvD00gBiE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","sourcesContent":[null,"// Portions ported from lightcraft-main/crates/color/src/cct.rs, lightcraft-main/crates/color/src/lib.rs,\n// lightcraft-main/crates/pipeline/src/tone.rs, lightcraft-main/crates/pipeline/src/local.rs and\n// lightcraft-main/crates/develop/src/settings.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors).\n// See THIRD_PARTY_NOTICES.md.\n\n/**\n * Colour science for the image editor's process 2 rendering: sRGB transfer functions,\n * correlated colour temperature / tint (Kim et al. Planckian locus, tint as Duv), Bradford\n * white-balance adaptation relative to \"as shot\", and the display-referred tone curve.\n */\n\n/** A CIE 1931 chromaticity. */\nexport interface Chromaticity {\n x: number;\n y: number;\n}\n\n/** A row-major 3×3 matrix (`[m00, m01, m02, m10, …, m22]`). */\nexport type Matrix3 = [number, number, number, number, number, number, number, number, number];\n\n// ---------------------------------------------------------------------------\n// Process versions\n// ---------------------------------------------------------------------------\n\n/** The rendering of adjustments from before process versions existed. Frozen. */\nexport const LEGACY_PROCESS_VERSION = 1 as const;\n/** The process new edits and Reset get. */\nexport const LATEST_PROCESS_VERSION = 2 as const;\nexport type ImageProcessVersion = 1 | 2;\n\n/**\n * Reads a stored process number leniently: a missing or damaged value (not a whole number) is\n * the legacy process, so a saved edit from before process versions renders exactly as it did. A\n * whole number newer than this build knows renders with the latest process.\n */\nexport function normalizeProcessVersion(value: unknown): ImageProcessVersion {\n if (typeof value !== 'number' || !Number.isInteger(value)) return LEGACY_PROCESS_VERSION;\n return value >= LATEST_PROCESS_VERSION ? LATEST_PROCESS_VERSION : LEGACY_PROCESS_VERSION;\n}\n\n// ---------------------------------------------------------------------------\n// sRGB transfer\n// ---------------------------------------------------------------------------\n\nexport function srgbToLinear(v: number): number {\n return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n}\n\nexport function linearToSrgb(v: number): number {\n return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n}\n\n/** Rec.709 / sRGB luminance weights (linear light). */\nexport const LUMA_709: readonly [number, number, number] = [0.2126, 0.7152, 0.0722];\n\n// ---------------------------------------------------------------------------\n// 3×3 matrices\n// ---------------------------------------------------------------------------\n\nexport const IDENTITY_MATRIX3: Matrix3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];\n\nexport function mulMatrix3(a: Matrix3, b: Matrix3): Matrix3 {\n const out = new Array(9).fill(0) as Matrix3;\n for (let r = 0; r < 3; r++) {\n for (let c = 0; c < 3; c++) {\n out[r * 3 + c] = a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c];\n }\n }\n return out;\n}\n\nexport function applyMatrix3(m: Matrix3, v: readonly [number, number, number]): [number, number, number] {\n return [\n m[0] * v[0] + m[1] * v[1] + m[2] * v[2],\n m[3] * v[0] + m[4] * v[1] + m[5] * v[2],\n m[6] * v[0] + m[7] * v[1] + m[8] * v[2],\n ];\n}\n\nexport function invertMatrix3(m: Matrix3): Matrix3 | null {\n const [a, b, c, d, e, f, g, h, i] = m;\n const A = e * i - f * h;\n const B = -(d * i - f * g);\n const C = d * h - e * g;\n const det = a * A + b * B + c * C;\n if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;\n const k = 1 / det;\n return [\n A * k, -(b * i - c * h) * k, (b * f - c * e) * k,\n B * k, (a * i - c * g) * k, -(a * f - c * d) * k,\n C * k, -(a * h - b * g) * k, (a * e - b * d) * k,\n ];\n}\n\nfunction diag3(x: number, y: number, z: number): Matrix3 {\n return [x, 0, 0, 0, y, 0, 0, 0, z];\n}\n\n// ---------------------------------------------------------------------------\n// Chromaticity, sRGB primaries, Bradford adaptation\n// ---------------------------------------------------------------------------\n\nexport const D65: Chromaticity = { x: 0.3127, y: 0.329 };\n\nfunction xyToXyz(p: Chromaticity): [number, number, number] {\n return [p.x / p.y, 1, (1 - p.x - p.y) / p.y];\n}\n\nfunction xyzToXy(v: readonly [number, number, number]): Chromaticity {\n const s = v[0] + v[1] + v[2];\n return s <= 0 ? { ...D65 } : { x: v[0] / s, y: v[1] / s };\n}\n\n/** Linear sRGB → XYZ (D65 white, Y = 1), derived from the primaries. */\nconst SRGB_TO_XYZ: Matrix3 = (() => {\n const p = [{ x: 0.64, y: 0.33 }, { x: 0.3, y: 0.6 }, { x: 0.15, y: 0.06 }].map(xyToXyz);\n const m: Matrix3 = [p[0][0], p[1][0], p[2][0], p[0][1], p[1][1], p[2][1], p[0][2], p[1][2], p[2][2]];\n const inv = invertMatrix3(m);\n const s = inv ? applyMatrix3(inv, xyToXyz(D65)) : [1, 1, 1];\n return mulMatrix3(m, diag3(s[0], s[1], s[2]));\n})();\nconst XYZ_TO_SRGB: Matrix3 = invertMatrix3(SRGB_TO_XYZ) ?? IDENTITY_MATRIX3;\n\n/** The Bradford cone-response matrix (XYZ → sharpened cones). */\nconst BRADFORD: Matrix3 = [0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296];\nconst BRADFORD_INV: Matrix3 = invertMatrix3(BRADFORD) ?? IDENTITY_MATRIX3;\n\n/** Bradford chromatic adaptation (XYZ → XYZ) from white `src` to white `dst`. */\nexport function bradfordAdaptation(src: Chromaticity, dst: Chromaticity): Matrix3 {\n if (src.x === dst.x && src.y === dst.y) return [...IDENTITY_MATRIX3];\n const s = applyMatrix3(BRADFORD, xyToXyz(src));\n const d = applyMatrix3(BRADFORD, xyToXyz(dst));\n return mulMatrix3(BRADFORD_INV, mulMatrix3(diag3(d[0] / s[0], d[1] / s[1], d[2] / s[2]), BRADFORD));\n}\n\n/**\n * Matrix (linear sRGB → linear sRGB) that white-balances a scene lit by `src` so that it\n * renders as if lit by D65.\n */\nexport function wbMatrix(src: Chromaticity): Matrix3 {\n return mulMatrix3(XYZ_TO_SRGB, mulMatrix3(bradfordAdaptation(src, D65), SRGB_TO_XYZ));\n}\n\n/** The chromaticity of a linear sRGB colour. */\nexport function linearSrgbToXy(rgb: readonly [number, number, number]): Chromaticity {\n return xyzToXy(applyMatrix3(SRGB_TO_XYZ, rgb));\n}\n\n// ---------------------------------------------------------------------------\n// Correlated colour temperature and tint\n// ---------------------------------------------------------------------------\n\n/** Tint units per unit of Duv. */\nexport const TINT_SCALE = 3000;\nexport const MIN_KELVIN = 1667;\nexport const MAX_KELVIN = 25000;\n\nfunction kimXy(tIn: number): Chromaticity {\n const t = Math.min(MAX_KELVIN, Math.max(MIN_KELVIN, tIn));\n const t2 = t * t;\n const t3 = t2 * t;\n const x =\n t <= 4000\n ? -0.2661239e9 / t3 - 0.2343589e6 / t2 + 0.8776956e3 / t + 0.17991\n : -3.0258469e9 / t3 + 2.1070379e6 / t2 + 0.2226347e3 / t + 0.24039;\n const x2 = x * x;\n const x3 = x2 * x;\n const y =\n t <= 2222\n ? -1.1063814 * x3 - 1.3481102 * x2 + 2.18555832 * x - 0.20219683\n : t <= 4000\n ? -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867\n : 3.081758 * x3 - 5.8733867 * x2 + 3.75112997 * x - 0.37001483;\n return { x, y };\n}\n\n/** CIE 1931 xy → CIE 1960 uv. */\nexport function xyToUv(p: Chromaticity): [number, number] {\n const d = -2 * p.x + 12 * p.y + 3;\n return [(4 * p.x) / d, (6 * p.y) / d];\n}\n\n/** CIE 1960 uv → CIE 1931 xy. */\nexport function uvToXy(u: number, v: number): Chromaticity {\n const d = 2 * u - 8 * v + 4;\n return { x: (3 * u) / d, y: (2 * v) / d };\n}\n\n/** Planckian locus in (u, v), extended linearly in mired space outside the approximation's range. */\nfunction locusUv(tIn: number): [number, number] {\n const t = Math.max(1000, tIn);\n if (t >= MIN_KELVIN && t <= MAX_KELVIN) return xyToUv(kimXy(t));\n const [a, b] = t < MIN_KELVIN ? [MIN_KELVIN, MIN_KELVIN + 50] : [MAX_KELVIN - 500, MAX_KELVIN];\n const [ua, va] = xyToUv(kimXy(a));\n const [ub, vb] = xyToUv(kimXy(b));\n const ma = 1e6 / a;\n const mb = 1e6 / b;\n const s = (1e6 / t - ma) / (mb - ma);\n return [ua + (ub - ua) * s, va + (vb - va) * s];\n}\n\n/** Unit normal of the locus at `t`, pointing towards green (increasing v). */\nfunction locusNormal(t: number): [number, number] {\n const m = 1e6 / t;\n const [u0, v0] = locusUv(1e6 / (m + 1));\n const [u1, v1] = locusUv(1e6 / Math.max(m - 1, 1));\n const du = u1 - u0;\n const dv = v1 - v0;\n const l = Math.max(Math.hypot(du, dv), 1e-12);\n const nx = -dv / l;\n const ny = du / l;\n return ny < 0 ? [-nx, -ny] : [nx, ny];\n}\n\n/**\n * Source illuminant chromaticity for temperature `kelvin` and correction `tint`.\n * Positive tint means a green source white, whose correction adds magenta to the image.\n */\nexport function tempTintToXy(kelvin: number, tint: number): Chromaticity {\n const [u, v] = locusUv(kelvin);\n const [nu, nv] = locusNormal(kelvin);\n const duv = tint / TINT_SCALE;\n return uvToXy(u + nu * duv, v + nv * duv);\n}\n\n/** Inverse of {@link tempTintToXy}: nearest locus temperature (searched in mired space) and tint. */\nexport function xyToTempTint(p: Chromaticity): { kelvin: number; tint: number } {\n const [u, v] = xyToUv(p);\n const dist = (m: number) => {\n const [lu, lv] = locusUv(1e6 / m);\n return (u - lu) ** 2 + (v - lv) ** 2;\n };\n // Coarse scan, then golden-section refine over mired 20..1000 (50000 K .. 1000 K).\n let best = 20;\n let bd = Infinity;\n for (let m = 20; m <= 1000; m += 5) {\n const d = dist(m);\n if (d < bd) {\n bd = d;\n best = m;\n }\n }\n let a = Math.max(best - 5, 20);\n let b = Math.min(best + 5, 1000);\n const g = 0.61803398875;\n for (let i = 0; i < 60; i++) {\n const c = b - g * (b - a);\n const d = a + g * (b - a);\n if (dist(c) < dist(d)) b = d;\n else a = c;\n }\n const kelvin = 1e6 / ((a + b) / 2);\n const [lu, lv] = locusUv(kelvin);\n const [nu, nv] = locusNormal(kelvin);\n const duv = (u - lu) * nu + (v - lv) * nv;\n return { kelvin, tint: duv * TINT_SCALE };\n}\n\n// ---------------------------------------------------------------------------\n// Relative white balance for rendered (JPEG / PNG / WebP) sources\n// ---------------------------------------------------------------------------\n\n/** Rendered images carry no as-shot white: \"as shot\" is 6500 K / tint 0 and maps to the identity. */\nexport const AS_SHOT_KELVIN = 6500;\n/** Mireds per unit of the `temperature` slider (−100…100). */\nexport const MIRED_PER_TEMPERATURE_UNIT = 1;\nconst AS_SHOT_MIRED = 1e6 / AS_SHOT_KELVIN;\n\n/**\n * Maps the `temperature` slider (−100 cool … +100 warm) to the Kelvin white-balance setting:\n * one mired per unit around 6500 K, so 0 → 6500 K, +100 → ~18 570 K (a strong warming\n * correction), −100 → ~3 940 K (a strong cooling correction). Mireds are perceptually even,\n * so equal slider steps look like equal changes.\n */\nexport function temperatureToKelvin(temperature: number): number {\n return 1e6 / (AS_SHOT_MIRED - temperature * MIRED_PER_TEMPERATURE_UNIT);\n}\n\n/** Inverse of {@link temperatureToKelvin}. */\nexport function kelvinToTemperature(kelvin: number): number {\n return (AS_SHOT_MIRED - 1e6 / kelvin) / MIRED_PER_TEMPERATURE_UNIT;\n}\n\nconst AS_SHOT_WB_INV: Matrix3 = invertMatrix3(wbMatrix(tempTintToXy(AS_SHOT_KELVIN, 0))) ?? IDENTITY_MATRIX3;\n\n/**\n * White-balance matrix (linear sRGB) for the `temperature` / `tint` sliders, relative to\n * \"as shot\": the identity at 0 / 0. Normalised so neutral luminance is preserved (white\n * balance doesn't change exposure).\n */\nexport function whiteBalanceMatrix(temperature: number, tint = 0): Matrix3 {\n if (!temperature && !tint) return [...IDENTITY_MATRIX3];\n const set = wbMatrix(tempTintToXy(temperatureToKelvin(temperature), tint));\n const m = mulMatrix3(set, AS_SHOT_WB_INV);\n const g = applyMatrix3(m, [1, 1, 1]);\n const y = g[0] * LUMA_709[0] + g[1] * LUMA_709[1] + g[2] * LUMA_709[2];\n return mulMatrix3(m, diag3(1 / y, 1 / y, 1 / y));\n}\n\n/**\n * Neutral-point eyedropper: the `temperature` / `tint` that render the sampled sRGB colour\n * (0–255 per channel, as it appears in the unadjusted image) neutral. Results are clamped to\n * the slider range −100…100.\n */\nexport function whiteBalanceFromSample(\n rgb: readonly [number, number, number] | { r: number; g: number; b: number }\n): { temperature: number; tint: number } {\n const [r, g, b] = Array.isArray(rgb) ? rgb : [(rgb as any).r, (rgb as any).g, (rgb as any).b];\n const lin: [number, number, number] = [r, g, b].map((c) => srgbToLinear(Math.min(255, Math.max(0, c)) / 255)) as [\n number,\n number,\n number,\n ];\n if (lin[0] + lin[1] + lin[2] <= 1e-6) return { temperature: 0, tint: 0 };\n // Under relative WB the matrix is wb(set) · wb(asShot)⁻¹, so the sample is neutral when the\n // chosen source white is the chromaticity of wb(asShot)⁻¹ · sample.\n const src = linearSrgbToXy(applyMatrix3(AS_SHOT_WB_INV, lin));\n const { kelvin, tint } = xyToTempTint(src);\n const clamp = (v: number) => Math.max(-100, Math.min(100, v));\n return { temperature: clamp(kelvinToTemperature(kelvin)), tint: clamp(tint) };\n}\n\n// ---------------------------------------------------------------------------\n// Tone curve (display-referred, identity at neutral)\n// ---------------------------------------------------------------------------\n\n/** Number of intervals in a tone LUT (the table has `TONE_LUT_SIZE + 1` entries). */\nexport const TONE_LUT_SIZE = 4096;\nconst GREY = 0.18;\nconst GREY_P = Math.pow(GREY, 1 / 2.2);\n/** Linear value where the highlight shoulder starts when there is headroom above 1. */\nconst SHOULDER_KNEE = 0.6;\n\n/**\n * The process 2 tone curve for one linear input value in `[0, headroom]`:\n *\n * 1. A highlight shoulder (extended Reinhard with white point `headroom`) above a knee of 0.6\n * rolls values above 1 (after exposure / white balance) back into range instead of clipping.\n * With no headroom (`headroom = 1`) it is exactly the identity.\n * 2. In a gamma-2.2 perceptual domain, contrast is an S-curve anchored at 0, middle grey and 1\n * (as `ToneMap::display`), and brightness a midtone lift anchored at 0 and 1.\n *\n * `contrast` and `brightness` are −100…100; both 0 with `headroom = 1` is the identity.\n */\nexport function toneCurve(y: number, contrast: number, brightness: number, headroom = 1): number {\n if (!(y > 0)) return 0;\n const h = Math.max(1, headroom);\n let o = Math.min(y, h);\n if (h > 1 && o > SHOULDER_KNEE) {\n const span = 1 - SHOULDER_KNEE;\n const x = (o - SHOULDER_KNEE) / span;\n const X = (h - SHOULDER_KNEE) / span;\n o = SHOULDER_KNEE + span * ((x * (1 + x / (X * X))) / (1 + x));\n }\n if (contrast === 0 && brightness === 0) return Math.min(1, o);\n let p = Math.pow(Math.min(1, o), 1 / 2.2);\n const c = contrast / 100;\n if (c !== 0) {\n p += c * 0.4 * (p - GREY_P) * (1 - (2 * p - 1) ** 2);\n }\n const b = brightness / 100;\n if (b !== 0) {\n // Smooth midtone lift that leaves black and white in place (slope stays positive).\n p += b * 0.8 * p * (1 - p);\n }\n return Math.pow(Math.min(1, Math.max(0, p)), 2.2);\n}\n\n/** Tabulates {@link toneCurve} over `[0, headroom]` in {@link TONE_LUT_SIZE} steps. */\nexport function buildToneLut(contrast: number, brightness: number, headroom = 1): Float32Array {\n const h = Math.max(1, headroom);\n const lut = new Float32Array(TONE_LUT_SIZE + 1);\n for (let i = 0; i <= TONE_LUT_SIZE; i++) {\n lut[i] = toneCurve((h * i) / TONE_LUT_SIZE, contrast, brightness, h);\n }\n return lut;\n}\n\n/** Linearly interpolated lookup into a LUT from {@link buildToneLut}. */\nexport function sampleToneLut(lut: Float32Array, headroom: number, y: number): number {\n if (!(y > 0)) return lut[0];\n const n = lut.length - 1;\n const f = (y * n) / Math.max(1, headroom);\n if (f >= n) return lut[n];\n const i = f | 0;\n return lut[i] + (lut[i + 1] - lut[i]) * (f - i);\n}\n","import type { Annotation, CropBox, ImageEditorState, ImagePoint, ImageTransform } from './types';\n\n/*\n * Coordinate spaces\n * -----------------\n * The crop box and every annotation are stored in ONE canonical space: natural image\n * pixels of the current source image (origin top-left of the un-rotated, un-flipped,\n * un-cropped image). Rotation, flips, crop and the viewport fit/zoom/pan are pure view\n * transforms applied on top of that space by a single forward mapping:\n *\n * target = origin + scale * R(angle) * F(flipH, flipV) * (image - regionCenter)\n *\n * where `regionCenter` is the centre of the visible region (the crop box, or the whole\n * image while the crop tool is active). `projectImagePoint` / `unprojectCanvasPoint`\n * implement the mapping and its exact inverse, and `applyImageProjectionToContext`\n * applies the very same transform to a 2D context, so rendering, hit-testing, viewport\n * metrics, export and mask generation cannot drift apart.\n */\n\nexport type CropHandle = 'nw' | 'ne' | 'se' | 'sw' | 'n' | 's' | 'e' | 'w' | 'inside' | null;\n\nexport interface ViewportMetrics {\n vpWidth: number;\n vpHeight: number;\n centerX: number;\n centerY: number;\n baseW: number;\n baseH: number;\n fitScale: number;\n renderW: number;\n renderH: number;\n totalScale: number;\n imageRect: { x: number; y: number; width: number; height: number };\n cropRect: { x: number; y: number; width: number; height: number } | null;\n}\n\n/**\n * A complete description of the natural-image -> target mapping (see module comment).\n */\nexport interface ImageProjection {\n /** Visible region in natural image pixels (crop box or full image). */\n region: CropBox;\n /** Rotation in degrees, normalised to [0, 360). */\n angle: number;\n flipH: boolean;\n flipV: boolean;\n /** Uniform scale from image pixels to target pixels. */\n scale: number;\n /** Target-space point that the centre of `region` maps to. */\n originX: number;\n originY: number;\n}\n\n/** Normalises any rotation in degrees to [0, 360). */\nexport function normalizeAngle(degrees: number): number {\n const a = ((degrees % 360) + 360) % 360;\n return a === 360 ? 0 : a;\n}\n\nfunction cosSin(angle: number): [number, number] {\n // Exact values for quarter turns so 90deg steps never accumulate float noise.\n switch (angle) {\n case 0:\n return [1, 0];\n case 90:\n return [0, 1];\n case 180:\n return [-1, 0];\n case 270:\n return [0, -1];\n default: {\n const rad = (angle * Math.PI) / 180;\n return [Math.cos(rad), Math.sin(rad)];\n }\n }\n}\n\n/** Whether the rotation swaps the displayed width and height. */\nexport function isQuarterTurnSwapped(transform: Pick<ImageTransform, 'rotate'>): boolean {\n const angle = normalizeAngle(transform.rotate);\n return angle === 90 || angle === 270;\n}\n\n/** Size of a region once rotated (bounding box for arbitrary angles). */\nexport function getRotatedSize(\n width: number,\n height: number,\n transform: Pick<ImageTransform, 'rotate'>\n): { width: number; height: number } {\n const [c, s] = cosSin(normalizeAngle(transform.rotate));\n return {\n width: Math.abs(width * c) + Math.abs(height * s),\n height: Math.abs(width * s) + Math.abs(height * c),\n };\n}\n\n/** Maps a natural-image point to target space. */\nexport function projectImagePoint(pt: ImagePoint, p: ImageProjection): ImagePoint {\n const [c, s] = cosSin(p.angle);\n let x = pt.x - (p.region.x + p.region.width / 2);\n let y = pt.y - (p.region.y + p.region.height / 2);\n if (p.flipH) x = -x;\n if (p.flipV) y = -y;\n const rx = x * c - y * s;\n const ry = x * s + y * c;\n return { x: p.originX + rx * p.scale, y: p.originY + ry * p.scale };\n}\n\n/** Exact inverse of {@link projectImagePoint}. */\nexport function unprojectCanvasPoint(pt: ImagePoint, p: ImageProjection): ImagePoint {\n const [c, s] = cosSin(p.angle);\n const rx = (pt.x - p.originX) / p.scale;\n const ry = (pt.y - p.originY) / p.scale;\n let x = rx * c + ry * s;\n let y = -rx * s + ry * c;\n if (p.flipH) x = -x;\n if (p.flipV) y = -y;\n return { x: x + p.region.x + p.region.width / 2, y: y + p.region.y + p.region.height / 2 };\n}\n\n/**\n * Applies the projection to a 2D context so that subsequent drawing in natural image\n * pixels lands exactly where {@link projectImagePoint} says it does.\n */\nexport function applyImageProjectionToContext(\n ctx: Pick<CanvasRenderingContext2D, 'translate' | 'scale' | 'rotate'>,\n p: ImageProjection\n): void {\n ctx.translate(p.originX, p.originY);\n ctx.scale(p.scale, p.scale);\n if (p.angle !== 0) ctx.rotate((p.angle * Math.PI) / 180);\n if (p.flipH || p.flipV) ctx.scale(p.flipH ? -1 : 1, p.flipV ? -1 : 1);\n ctx.translate(-(p.region.x + p.region.width / 2), -(p.region.y + p.region.height / 2));\n}\n\n/** Projects an image-space rectangle and returns its target-space bounding box. */\nexport function projectImageRect(\n rect: CropBox,\n p: ImageProjection\n): { x: number; y: number; width: number; height: number } {\n const corners = [\n projectImagePoint({ x: rect.x, y: rect.y }, p),\n projectImagePoint({ x: rect.x + rect.width, y: rect.y }, p),\n projectImagePoint({ x: rect.x, y: rect.y + rect.height }, p),\n projectImagePoint({ x: rect.x + rect.width, y: rect.y + rect.height }, p),\n ];\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n const minX = Math.min(...xs);\n const minY = Math.min(...ys);\n return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };\n}\n\n/** Clamps a crop box to the image bounds. */\nexport function clampCropToImage(crop: CropBox, width: number, height: number): CropBox {\n const x = Math.max(0, Math.min(width - 1, crop.x));\n const y = Math.max(0, Math.min(height - 1, crop.y));\n return {\n x,\n y,\n width: Math.max(1, Math.min(width - x, crop.width)),\n height: Math.max(1, Math.min(height - y, crop.height)),\n };\n}\n\nfunction resolveViewport(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping: boolean,\n cropOverride?: CropBox | null\n) {\n const { imageDimensions, crop: stateCrop, transform, zoom, pan, fit = 'margin' } = state;\n const naturalWidth = imageDimensions.width || 800;\n const naturalHeight = imageDimensions.height || 600;\n const full: CropBox = { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const activeCrop =\n cropOverride !== undefined && cropOverride !== null\n ? cropOverride\n : (stateCrop ?? (isActivelyCropping ? full : null));\n\n const region =\n !isActivelyCropping && activeCrop ? clampCropToImage(activeCrop, naturalWidth, naturalHeight) : full;\n const { width: baseW, height: baseH } = getRotatedSize(region.width, region.height, transform);\n\n const fitScale =\n fit === 'contain'\n ? Math.min(vpWidth / baseW, vpHeight / baseH)\n : Math.min((vpWidth * 0.85) / baseW, (vpHeight * 0.85) / baseH, 1);\n const centerX = vpWidth / 2 + pan.x;\n const centerY = vpHeight / 2 + pan.y;\n\n const projection: ImageProjection = {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: zoom * fitScale,\n originX: centerX,\n originY: centerY,\n };\n\n return { activeCrop, baseW, baseH, fitScale, centerX, centerY, projection };\n}\n\n/**\n * Returns the natural-image -> canvas projection used by the interactive viewport.\n */\nexport function getViewportProjection(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false,\n cropOverride?: CropBox | null\n): ImageProjection {\n return resolveViewport(state, vpWidth, vpHeight, isActivelyCropping, cropOverride).projection;\n}\n\n/**\n * Calculates current viewport projection metrics for canvas <-> image coordinate mapping.\n * `cropRect` is the on-screen (rotated/flipped) rectangle of the crop box.\n */\nexport function getViewportMetrics(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false,\n cropOverride?: CropBox | null\n): ViewportMetrics {\n const { zoom } = state;\n const { activeCrop, baseW, baseH, fitScale, centerX, centerY, projection } = resolveViewport(\n state,\n vpWidth,\n vpHeight,\n isActivelyCropping,\n cropOverride\n );\n\n const renderW = baseW * fitScale;\n const renderH = baseH * fitScale;\n\n const imageRect = {\n x: centerX - (renderW * zoom) / 2,\n y: centerY - (renderH * zoom) / 2,\n width: renderW * zoom,\n height: renderH * zoom,\n };\n\n const cropRect = activeCrop ? projectImageRect(activeCrop, projection) : null;\n\n return {\n vpWidth,\n vpHeight,\n centerX,\n centerY,\n baseW,\n baseH,\n fitScale,\n renderW,\n renderH,\n totalScale: zoom * fitScale,\n imageRect,\n cropRect,\n };\n}\n\n/**\n * Converts a point on the canvas (in internal canvas pixel space) to natural image pixel\n * space, accounting for zoom, pan, fit scale, crop offset, rotation and flips.\n */\nexport function canvasToImagePoint(\n canvasPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false\n): ImagePoint {\n return unprojectCanvasPoint(canvasPt, getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping));\n}\n\n/**\n * Converts a point in natural image pixel space to internal canvas pixel space.\n * Exact inverse of {@link canvasToImagePoint}.\n */\nexport function imageToCanvasPoint(\n imgPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false\n): ImagePoint {\n return projectImagePoint(imgPt, getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping));\n}\n\nconst HANDLE_VECTORS: Record<Exclude<CropHandle, 'inside' | null>, [number, number]> = {\n nw: [-1, -1],\n n: [0, -1],\n ne: [1, -1],\n e: [1, 0],\n se: [1, 1],\n s: [0, 1],\n sw: [-1, 1],\n w: [-1, 0],\n};\n\nfunction vectorToHandle(x: number, y: number): CropHandle {\n const vx = Math.abs(x) < 0.38 ? 0 : Math.sign(x);\n const vy = Math.abs(y) < 0.38 ? 0 : Math.sign(y);\n for (const [name, [hx, hy]] of Object.entries(HANDLE_VECTORS)) {\n if (hx === vx && hy === vy) return name as CropHandle;\n }\n return null;\n}\n\n/**\n * Converts a crop handle in screen orientation to the equivalent edge/corner of the crop\n * box in natural image space, or the reverse with `toScreen = true`.\n */\nexport function mapCropHandle(handle: CropHandle, transform: ImageTransform, toScreen = false): CropHandle {\n if (handle === null || handle === 'inside') return handle;\n const [hx, hy] = HANDLE_VECTORS[handle];\n const [c, s] = cosSin(normalizeAngle(transform.rotate));\n const fx = transform.flipH ? -1 : 1;\n const fy = transform.flipV ? -1 : 1;\n if (toScreen) {\n const x = hx * fx;\n const y = hy * fy;\n return vectorToHandle(x * c - y * s, x * s + y * c);\n }\n const x = hx * c + hy * s;\n const y = -hx * s + hy * c;\n return vectorToHandle(x * fx, y * fy);\n}\n\n/**\n * Hit-tests the crop box and its corner/edge handles given a canvas point.\n * The returned handle names the edge/corner of the crop box in natural image space\n * (what {@link calculateCropDrag} expects); use `getCropCursor(handle, state.transform)`\n * to obtain the matching on-screen cursor.\n */\nexport function hitTestCrop(\n canvasPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n handleThreshold = 14,\n cropOverride?: CropBox | null\n): CropHandle {\n const metrics = getViewportMetrics(state, vpWidth, vpHeight, true, cropOverride);\n if (!metrics.cropRect) return null;\n\n const screenHandle = hitTestCropRect(canvasPt, metrics.cropRect, handleThreshold);\n return mapCropHandle(screenHandle, state.transform);\n}\n\nfunction hitTestCropRect(\n canvasPt: ImagePoint,\n rect: { x: number; y: number; width: number; height: number },\n handleThreshold: number\n): CropHandle {\n const { x, y, width: w, height: h } = rect;\n const px = canvasPt.x;\n const py = canvasPt.y;\n\n // 1. Corners (highest priority)\n const dist = (x1: number, y1: number, x2: number, y2: number) =>\n Math.hypot(x1 - x2, y1 - y2);\n\n if (dist(px, py, x, y) <= handleThreshold) return 'nw';\n if (dist(px, py, x + w, y) <= handleThreshold) return 'ne';\n if (dist(px, py, x + w, y + h) <= handleThreshold) return 'se';\n if (dist(px, py, x, y + h) <= handleThreshold) return 'sw';\n\n // 2. Edges\n const nearX = px >= x - handleThreshold && px <= x + w + handleThreshold;\n const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;\n\n if (nearX && Math.abs(py - y) <= handleThreshold) return 'n';\n if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return 's';\n if (nearY && Math.abs(px - x) <= handleThreshold) return 'w';\n if (nearY && Math.abs(px - (x + w)) <= handleThreshold) return 'e';\n\n // 3. Inside crop region\n if (px >= x && px <= x + w && py >= y && py <= y + h) {\n return 'inside';\n }\n\n return null;\n}\n\n/**\n * Returns CSS cursor style corresponding to a crop handle. Pass the current transform when\n * the handle comes from {@link hitTestCrop} (image space) so the cursor matches the\n * on-screen orientation under rotation/flip.\n */\nexport function getCropCursor(handle: CropHandle, transform?: ImageTransform): string {\n const screenHandle = transform ? mapCropHandle(handle, transform, true) : handle;\n switch (screenHandle) {\n case 'nw':\n case 'se':\n return 'nwse-resize';\n case 'ne':\n case 'sw':\n return 'nesw-resize';\n case 'n':\n case 's':\n return 'ns-resize';\n case 'e':\n case 'w':\n return 'ew-resize';\n case 'inside':\n return 'move';\n default:\n return 'crosshair';\n }\n}\n\n/**\n * Calculates updated crop rectangle given a drag interaction.\n */\nexport function calculateCropDrag(\n startCrop: CropBox,\n handle: CropHandle,\n startImgPt: ImagePoint,\n currentImgPt: ImagePoint,\n imgWidth: number,\n imgHeight: number,\n aspectRatio?: number | null,\n transform?: ImageTransform\n): CropBox {\n // `aspectRatio` is the on-screen ratio; under a quarter turn it is inverted in image space.\n if (aspectRatio && transform && isQuarterTurnSwapped(transform)) {\n aspectRatio = 1 / aspectRatio;\n }\n const dx = currentImgPt.x - startImgPt.x;\n const dy = currentImgPt.y - startImgPt.y;\n\n // Whole crop box move\n if (handle === 'inside') {\n const maxX = Math.max(0, imgWidth - startCrop.width);\n const maxY = Math.max(0, imgHeight - startCrop.height);\n const newX = Math.max(0, Math.min(maxX, startCrop.x + dx));\n const newY = Math.max(0, Math.min(maxY, startCrop.y + dy));\n return {\n x: Math.round(newX),\n y: Math.round(newY),\n width: startCrop.width,\n height: startCrop.height,\n };\n }\n\n // Handle resizing\n let left = startCrop.x;\n let top = startCrop.y;\n let right = startCrop.x + startCrop.width;\n let bottom = startCrop.y + startCrop.height;\n\n const minSize = 20;\n\n if (handle === 'e' || handle === 'ne' || handle === 'se') {\n right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));\n }\n if (handle === 'w' || handle === 'nw' || handle === 'sw') {\n left = Math.max(0, Math.min(right - minSize, startCrop.x + dx));\n }\n if (handle === 's' || handle === 'se' || handle === 'sw') {\n bottom = Math.min(imgHeight, Math.max(top + minSize, startCrop.y + startCrop.height + dy));\n }\n if (handle === 'n' || handle === 'nw' || handle === 'ne') {\n top = Math.max(0, Math.min(bottom - minSize, startCrop.y + dy));\n }\n\n // Maintain aspect ratio if requested\n if (aspectRatio && aspectRatio > 0) {\n let currentW = right - left;\n let currentH = bottom - top;\n\n if (handle === 'e' || handle === 'w') {\n currentH = Math.min(imgHeight, currentW / aspectRatio);\n bottom = Math.min(imgHeight, top + currentH);\n } else if (handle === 'n' || handle === 's') {\n currentW = Math.min(imgWidth, currentH * aspectRatio);\n right = Math.min(imgWidth, left + currentW);\n } else {\n // Corner resizing\n const targetH = currentW / aspectRatio;\n if (top + targetH <= imgHeight) {\n bottom = top + targetH;\n } else {\n const targetW = currentH * aspectRatio;\n right = Math.min(imgWidth, left + targetW);\n }\n }\n }\n\n return {\n x: Math.round(left),\n y: Math.round(top),\n width: Math.round(Math.max(minSize, right - left)),\n height: Math.round(Math.max(minSize, bottom - top)),\n };\n}\n\n/**\n * Calculates bounding box of an annotation.\n */\nexport function getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n switch (ann.type) {\n case 'rect':\n case 'image':\n return {\n x: Math.min(ann.x, ann.x + ann.width),\n y: Math.min(ann.y, ann.y + ann.height),\n width: Math.abs(ann.width),\n height: Math.abs(ann.height),\n };\n case 'circle':\n return {\n x: ann.x - Math.abs(ann.radiusX),\n y: ann.y - Math.abs(ann.radiusY),\n width: Math.abs(ann.radiusX) * 2,\n height: Math.abs(ann.radiusY) * 2,\n };\n case 'arrow':\n case 'line': {\n const minX = Math.min(ann.x, ann.endX);\n const minY = Math.min(ann.y, ann.endY);\n return {\n x: minX,\n y: minY,\n width: Math.max(Math.abs(ann.endX - ann.x), 10),\n height: Math.max(Math.abs(ann.endY - ann.y), 10),\n };\n }\n case 'pen': {\n if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };\n let minX = ann.points[0].x;\n let maxX = ann.points[0].x;\n let minY = ann.points[0].y;\n let maxY = ann.points[0].y;\n for (const pt of ann.points) {\n if (pt.x < minX) minX = pt.x;\n if (pt.x > maxX) maxX = pt.x;\n if (pt.y < minY) minY = pt.y;\n if (pt.y > maxY) maxY = pt.y;\n }\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n }\n case 'text':\n return {\n x: ann.x,\n y: ann.y,\n width: ann.text.length * ann.fontSize * 0.6,\n height: ann.fontSize * 1.2,\n };\n }\n}\n\n/**\n * Hit tests if a point in image space is within an annotation's bounds.\n */\nexport function hitTestAnnotation(imgPt: ImagePoint, ann: Annotation, padding = 10): boolean {\n const b = getAnnotationBounds(ann);\n return (\n imgPt.x >= b.x - padding &&\n imgPt.x <= b.x + b.width + padding &&\n imgPt.y >= b.y - padding &&\n imgPt.y <= b.y + b.height + padding\n );\n}\n\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\n\n/**\n * A selection: one 8-bit coverage value per image pixel (0 = not selected, 255 = fully\n * selected), row-major over the whole source image in natural pixels — the same space as the\n * crop box and annotations. Soft values are partial selection (feathered or anti-aliased edges).\n *\n * Treat a mask as immutable: every operation returns a new one, so history entries can share\n * them without copying.\n */\nexport interface SelectionMask {\n readonly width: number;\n readonly height: number;\n readonly data: Uint8Array;\n /** Bounding box of the non-zero coverage, or `null` when nothing is selected. */\n readonly bounds: CropBox | null;\n}\n\n/** How a new selection combines with the current one. */\nexport type SelectionMode = 'replace' | 'add' | 'subtract' | 'intersect';\n\nexport const SELECTION_MODES: readonly SelectionMode[] = ['replace', 'add', 'subtract', 'intersect'];\n\nexport function isSelectionMode(value: unknown): value is SelectionMode {\n return typeof value === 'string' && (SELECTION_MODES as readonly string[]).includes(value);\n}\n\n/** Parses `replace|add|subtract|intersect`; anything else is `replace`. */\nexport function parseSelectionMode(value: unknown): SelectionMode {\n return isSelectionMode(value) ? value : 'replace';\n}\n\n/** RGBA pixels, 8 bits per channel, straight alpha (an `ImageData` qualifies). */\nexport interface PixelBuffer {\n width: number;\n height: number;\n data: ArrayLike<number>;\n}\n\n/** Serialized selection: a run-length encoding of the 8-bit coverage. */\nexport interface SerializedSelection {\n width: number;\n height: number;\n /** `r1:` + base64 of (value byte, LEB128 run length) pairs. */\n rle: string;\n}\n\nconst MAX_SIDE = 1 << 15;\n\nfunction checkSize(width: number, height: number): void {\n if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > MAX_SIDE || height > MAX_SIDE) {\n throw new Error(`Invalid selection size ${width}×${height}`);\n }\n}\n\n/** Bounding box of non-zero bytes. */\nexport function computeSelectionBounds(width: number, height: number, data: ArrayLike<number>): CropBox | null {\n let x0 = width;\n let x1 = -1;\n let y0 = -1;\n let y1 = -1;\n for (let y = 0; y < height; y++) {\n const row = y * width;\n let first = -1;\n for (let x = 0; x < width; x++) {\n if (data[row + x] !== 0) {\n first = x;\n break;\n }\n }\n if (first < 0) continue;\n let last = first;\n for (let x = width - 1; x > first; x--) {\n if (data[row + x] !== 0) {\n last = x;\n break;\n }\n }\n if (y0 < 0) y0 = y;\n y1 = y;\n if (first < x0) x0 = first;\n if (last > x1) x1 = last;\n }\n if (y0 < 0) return null;\n return { x: x0, y: y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };\n}\n\n/** Wraps coverage bytes (taken over, not copied) as a selection, computing its bounds. */\nexport function createSelectionMask(width: number, height: number, data?: Uint8Array): SelectionMask {\n checkSize(width, height);\n const bytes = data ?? new Uint8Array(width * height);\n if (bytes.length !== width * height) throw new Error('Selection data does not match its size');\n return { width, height, data: bytes, bounds: data ? computeSelectionBounds(width, height, bytes) : null };\n}\n\n/** Select › All. */\nexport function fullSelection(width: number, height: number): SelectionMask {\n checkSize(width, height);\n return { width, height, data: new Uint8Array(width * height).fill(255), bounds: { x: 0, y: 0, width, height } };\n}\n\nexport function isSelectionEmpty(mask: SelectionMask | null | undefined): boolean {\n return !mask || mask.bounds === null;\n}\n\n/** Coverage at a pixel, 0..1 (0 outside the image). */\nexport function selectionValueAt(mask: SelectionMask, x: number, y: number): number {\n const xi = Math.floor(x);\n const yi = Math.floor(y);\n if (xi < 0 || yi < 0 || xi >= mask.width || yi >= mask.height) return 0;\n return mask.data[yi * mask.width + xi] / 255;\n}\n\n/** 8-bit value of a coverage (clamped and rounded; NaN → 0). */\nexport function coverageToByte(v: number): number {\n if (!(v > 0)) return 0;\n if (v >= 1) return 255;\n return Math.floor(v * 255 + 0.5);\n}\n\n/** A selection from 0..1 coverage. */\nexport function selectionFromCoverage(width: number, height: number, coverage: ArrayLike<number>): SelectionMask {\n const n = width * height;\n const out = new Uint8Array(n);\n for (let i = 0; i < n; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** The selection's coverage as 0..1 floats. */\nexport function selectionToCoverage(mask: SelectionMask): Float32Array {\n const out = new Float32Array(mask.data.length);\n for (let i = 0; i < out.length; i++) out[i] = mask.data[i] / 255;\n return out;\n}\n\n/** Number of fully-or-partly selected pixels, weighted by coverage (Σ coverage). */\nexport function selectionArea(mask: SelectionMask | null | undefined): number {\n if (!mask || !mask.bounds) return 0;\n let sum = 0;\n const { x, y, width, height } = mask.bounds;\n for (let yy = y; yy < y + height; yy++) {\n const row = yy * mask.width;\n for (let xx = x; xx < x + width; xx++) sum += mask.data[row + xx];\n }\n return sum / 255;\n}\n\n/**\n * Combines `next` with `prev` by `mode` (8-bit, exact: combining a mask with itself is a no-op).\n * Add is the maximum, Subtract `max(prev − next, 0)`, Intersect the minimum. A missing `prev`\n * reads as nothing selected. Returns `null` when nothing remains selected.\n */\nexport function combineSelections(\n prev: SelectionMask | null | undefined,\n next: SelectionMask,\n mode: SelectionMode\n): SelectionMask | null {\n let result: SelectionMask;\n if (mode === 'replace' || (!prev && mode === 'add')) {\n result = next;\n } else if (!prev) {\n return null;\n } else {\n if (prev.width !== next.width || prev.height !== next.height) throw new Error('Selections differ in size');\n const a = prev.data;\n const b = next.data;\n const out = new Uint8Array(a.length);\n if (mode === 'add') for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] : b[i];\n else if (mode === 'subtract') for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] - b[i] : 0;\n else for (let i = 0; i < a.length; i++) out[i] = a[i] < b[i] ? a[i] : b[i];\n result = createSelectionMask(prev.width, prev.height, out);\n }\n return result.bounds ? result : null;\n}\n\n/** Select › Inverse. */\nexport function invertSelection(mask: SelectionMask): SelectionMask {\n const out = new Uint8Array(mask.data.length);\n for (let i = 0; i < out.length; i++) out[i] = 255 - mask.data[i];\n return createSelectionMask(mask.width, mask.height, out);\n}\n\n/** Bilinear resample to another size (e.g. an AI mask returned at a different resolution). */\nexport function resizeSelection(mask: SelectionMask, width: number, height: number): SelectionMask {\n if (mask.width === width && mask.height === height) return mask;\n checkSize(width, height);\n const out = new Uint8Array(width * height);\n const sx = mask.width / width;\n const sy = mask.height / height;\n const src = mask.data;\n const mw = mask.width;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(mask.height - 1, Math.max(0, (y + 0.5) * sy - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(mask.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(mw - 1, Math.max(0, (x + 0.5) * sx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(mw - 1, x0 + 1);\n const tx = fx - x0;\n const top = src[y0 * mw + x0] * (1 - tx) + src[y0 * mw + x1] * tx;\n const bot = src[y1 * mw + x0] * (1 - tx) + src[y1 * mw + x1] * tx;\n out[y * width + x] = Math.round(top * (1 - ty) + bot * ty);\n }\n }\n return createSelectionMask(width, height, out);\n}\n\nexport type SelectionChannel = 'alpha' | 'luminance' | 'red' | 'green' | 'blue';\n\n/**\n * A selection from an image: by default its luminance (white = selected, the usual convention\n * of segmentation masks), or one channel. Partly transparent pixels count as black for\n * luminance, so a mask PNG with transparency works either way.\n */\nexport function selectionFromPixels(\n pixels: PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number } = {}\n): SelectionMask {\n const { width, height, data } = pixels;\n checkSize(width, height);\n if (data.length < width * height * 4) throw new Error('Pixel data is shorter than width × height × 4');\n const channel = options.channel ?? 'luminance';\n const out = new Uint8Array(width * height);\n const threshold = options.threshold;\n for (let i = 0, p = 0; i < out.length; i++, p += 4) {\n let v: number;\n switch (channel) {\n case 'alpha':\n v = data[p + 3];\n break;\n case 'red':\n v = data[p];\n break;\n case 'green':\n v = data[p + 1];\n break;\n case 'blue':\n v = data[p + 2];\n break;\n default:\n v = Math.round(((0.2126 * data[p] + 0.7152 * data[p + 1] + 0.0722 * data[p + 2]) * data[p + 3]) / 255);\n }\n if (options.invert) v = 255 - v;\n if (threshold !== undefined) v = v >= threshold ? 255 : 0;\n out[i] = v;\n }\n return createSelectionMask(width, height, out);\n}\n\n/** The selection as RGBA pixels: grey (white = selected, opaque) or white with coverage as alpha. */\nexport function selectionToPixels(mask: SelectionMask, as: 'gray' | 'alpha' = 'gray'): { width: number; height: number; data: Uint8ClampedArray } {\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n const v = mask.data[i];\n if (as === 'gray') {\n out[p] = out[p + 1] = out[p + 2] = v;\n out[p + 3] = 255;\n } else {\n out[p] = out[p + 1] = out[p + 2] = 255;\n out[p + 3] = v;\n }\n }\n return { width: mask.width, height: mask.height, data: out };\n}\n\n// ---------------------------------------------------------------------------\n// Run-length encoding\n// ---------------------------------------------------------------------------\n\nconst B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst B64_INDEX = (() => {\n const t = new Int16Array(128).fill(-1);\n for (let i = 0; i < B64.length; i++) t[B64.charCodeAt(i)] = i;\n return t;\n})();\n\nfunction toBase64(bytes: Uint8Array, length: number): string {\n const parts: string[] = [];\n let chunk = '';\n for (let i = 0; i < length; i += 3) {\n const a = bytes[i];\n const b = i + 1 < length ? bytes[i + 1] : 0;\n const c = i + 2 < length ? bytes[i + 2] : 0;\n chunk += B64[a >> 2] + B64[((a & 3) << 4) | (b >> 4)];\n chunk += i + 1 < length ? B64[((b & 15) << 2) | (c >> 6)] : '=';\n chunk += i + 2 < length ? B64[c & 63] : '=';\n if (chunk.length >= 8192) {\n parts.push(chunk);\n chunk = '';\n }\n }\n parts.push(chunk);\n return parts.join('');\n}\n\nfunction fromBase64(text: string): Uint8Array {\n if (text.length % 4 !== 0) throw new Error('Invalid base64 length');\n let pad = 0;\n if (text.endsWith('==')) pad = 2;\n else if (text.endsWith('=')) pad = 1;\n const out = new Uint8Array((text.length / 4) * 3 - pad);\n let o = 0;\n const digit = (i: number, k: number): number => {\n const ch = text.charCodeAt(i + k);\n if (ch === 61 /* = */ && i + 4 === text.length && k >= 4 - pad) return 0;\n const idx = ch < 128 ? B64_INDEX[ch] : -1;\n if (idx < 0) throw new Error('Invalid base64 character');\n return idx;\n };\n for (let i = 0; i < text.length; i += 4) {\n const n = (digit(i, 0) << 18) | (digit(i, 1) << 12) | (digit(i, 2) << 6) | digit(i, 3);\n if (o < out.length) out[o++] = (n >> 16) & 255;\n if (o < out.length) out[o++] = (n >> 8) & 255;\n if (o < out.length) out[o++] = n & 255;\n }\n return out;\n}\n\n/** Run-length encodes coverage bytes: `r1:` + base64 of (value, LEB128 run length) pairs. */\nexport function encodeSelectionRle(data: Uint8Array): string {\n let buf = new Uint8Array(64);\n let len = 0;\n const push = (b: number) => {\n if (len === buf.length) {\n const next = new Uint8Array(buf.length * 2);\n next.set(buf);\n buf = next;\n }\n buf[len++] = b;\n };\n let i = 0;\n while (i < data.length) {\n const v = data[i];\n let j = i + 1;\n while (j < data.length && data[j] === v) j++;\n push(v);\n let run = j - i;\n while (run >= 128) {\n push((run & 127) | 128);\n run = Math.floor(run / 128);\n }\n push(run);\n i = j;\n }\n return 'r1:' + toBase64(buf, len);\n}\n\n/** Decodes {@link encodeSelectionRle}; throws unless the runs cover exactly `length` bytes. */\nexport function decodeSelectionRle(rle: string, length: number): Uint8Array {\n if (typeof rle !== 'string' || !rle.startsWith('r1:')) throw new Error('Unknown selection encoding');\n const bytes = fromBase64(rle.slice(3));\n const out = new Uint8Array(length);\n let o = 0;\n let i = 0;\n while (i < bytes.length) {\n const v = bytes[i++];\n let run = 0;\n let scale = 1;\n for (;;) {\n if (i >= bytes.length) throw new Error('Truncated selection run');\n const b = bytes[i++];\n run += (b & 127) * scale;\n if (b < 128) break;\n scale *= 128;\n if (scale > 2 ** 35) throw new Error('Selection run too long');\n }\n if (run === 0 || o + run > length) throw new Error('Selection runs do not match its size');\n if (v !== 0) out.fill(v, o, o + run);\n o += run;\n }\n if (o !== length) throw new Error('Selection runs do not match its size');\n return out;\n}\n\nexport function serializeSelection(mask: SelectionMask): SerializedSelection {\n return { width: mask.width, height: mask.height, rle: encodeSelectionRle(mask.data) };\n}\n\n/** Restores a {@link SerializedSelection}; throws on damaged data. */\nexport function deserializeSelection(value: unknown): SelectionMask {\n if (!value || typeof value !== 'object') throw new Error('selection must be an object');\n const v = value as Record<string, unknown>;\n const width = v.width as number;\n const height = v.height as number;\n checkSize(width, height);\n return createSelectionMask(width, height, decodeSelectionRle(v.rle as string, width * height));\n}\n\n/** A short, stable hash of a string or bytes (FNV-1a, 32-bit, hex) for cache keys. */\nexport function hashMaskContent(content: string | Uint8Array): string {\n let h = 0x811c9dc5;\n if (typeof content === 'string') {\n for (let i = 0; i < content.length; i++) {\n h ^= content.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n } else {\n for (let i = 0; i < content.length; i++) {\n h ^= content[i];\n h = Math.imul(h, 0x01000193);\n }\n }\n return `${(h >>> 0).toString(16)}:${content.length}`;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs, photocraft/crates/algo/src/selection/distance.rs, photocraft/crates/algo/src/selection_blur.rs and photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport { createSelectionMask, coverageToByte, selectionToCoverage, type SelectionMask } from './mask';\nimport { gaussianBlur } from '../../raster/blur';\n\nexport { gaussianBlur, DIRECT_GAUSSIAN_MAX_SIGMA } from '../../raster/blur';\n\n/** Distance reported for every pixel when there is no seed at all. */\nexport const EMPTY_DISTANCE = 1e10;\n\n/**\n * Exact Euclidean distance from every pixel centre to the nearest seed pixel centre (row-major\n * `inside`, non-zero = seed): nearest seeds along columns, then the lower envelope of parabolas\n * along rows (Felzenszwalb & Huttenlocher). Linear in the pixel count.\n */\nexport function distanceTransform(inside: ArrayLike<number | boolean>, width: number, height: number): Float32Array {\n const n = width * height;\n const out = new Float32Array(n);\n let any = false;\n let all = true;\n for (let i = 0; i < n; i++) {\n if (inside[i]) any = true;\n else all = false;\n }\n if (!any) return out.fill(EMPTY_DISTANCE);\n if (all) return out;\n const NONE = 0xffffffff;\n // Vertical distance to the nearest seed in the same column.\n const vertical = new Uint32Array(n).fill(NONE);\n for (let x = 0; x < width; x++) {\n let nearest = -1;\n for (let y = 0; y < height; y++) {\n const i = y * width + x;\n if (inside[i]) nearest = y;\n if (nearest >= 0) vertical[i] = y - nearest;\n }\n nearest = -1;\n for (let y = height - 1; y >= 0; y--) {\n const i = y * width + x;\n if (vertical[i] === 0) nearest = y;\n if (nearest >= 0) vertical[i] = Math.min(vertical[i], nearest - y);\n }\n }\n const sites = new Int32Array(width);\n const starts = new Float64Array(width);\n for (let y = 0; y < height; y++) {\n const row = y * width;\n let count = 0;\n for (let q = 0; q < width; q++) {\n const dq = vertical[row + q];\n if (dq === NONE) continue;\n const fq = dq * dq;\n let start = -Infinity;\n while (count > 0) {\n const p = sites[count - 1];\n const dp = vertical[row + p];\n start = (fq - dp * dp) / (2 * (q - p)) + (q + p) * 0.5;\n if (start > starts[count - 1]) break;\n count--;\n }\n if (count === 0) start = -Infinity;\n sites[count] = q;\n starts[count] = start;\n count++;\n }\n if (count === 0) {\n out.fill(EMPTY_DISTANCE, row, row + width);\n continue;\n }\n let k = 0;\n for (let x = 0; x < width; x++) {\n while (k + 1 < count && starts[k + 1] < x) k++;\n const site = sites[k];\n const dx = x - site;\n const dv = vertical[row + site];\n out[row + x] = Math.sqrt(dx * dx + dv * dv);\n }\n }\n return out;\n}\n\nfunction thresholdInside(m: ArrayLike<number>, threshold = 0.5): Uint8Array {\n const out = new Uint8Array(m.length);\n for (let i = 0; i < m.length; i++) out[i] = m[i] >= threshold ? 1 : 0;\n return out;\n}\n\n/** Grows coverage (0..1) by `r` pixels: covered when the nearest selected centre is ≤ r away (fractional r → partial). */\nexport function expandCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const d = distanceTransform(thresholdInside(m), w, h);\n const out = new Float32Array(m.length);\n for (let i = 0; i < out.length; i++) out[i] = Math.max(m[i], Math.min(1, Math.max(0, r + 1 - d[i])));\n return out;\n}\n\n/** Shrinks coverage by `r` pixels (expands the inverse). */\nexport function contractCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const inv = new Float32Array(m.length);\n for (let i = 0; i < inv.length; i++) inv[i] = 1 - m[i];\n const e = expandCoverage(inv, w, h, r);\n for (let i = 0; i < e.length; i++) e[i] = 1 - e[i];\n return e;\n}\n\n/**\n * Select › Modify › Border: a soft band `2r` wide centred on the edge. A pixel `D` away (centre\n * to centre) from the nearest pixel on the other side of the edge is selected `1 − (D − 1) / r`.\n */\nexport function borderCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const inside = thresholdInside(m);\n const outside = new Uint8Array(inside.length);\n for (let i = 0; i < inside.length; i++) outside[i] = inside[i] ? 0 : 1;\n const out = new Float32Array(m.length);\n if (!(r > 0)) return out;\n const toInside = distanceTransform(inside, w, h);\n const toOutside = distanceTransform(outside, w, h);\n for (let i = 0; i < out.length; i++) {\n const d = inside[i] ? toOutside[i] : toInside[i];\n out[i] = Math.min(1, Math.max(0, 1 - (d - 1) / r));\n }\n return out;\n}\n\n/** Box average of radius `r` along one line, edges replicated, via a running sum (O(1) per sample). */\nfunction boxLineReplicate(src: Float32Array, srcOff: number, stride: number, len: number, r: number, dst: Float32Array) {\n const at = (i: number) => src[srcOff + Math.min(len - 1, Math.max(0, i)) * stride];\n let sum = 0;\n for (let i = -r; i <= r; i++) sum += at(i);\n const k = 2 * r + 1;\n for (let i = 0; i < len; i++) {\n dst[i] = sum / k;\n sum += at(i + r + 1) - at(i - r);\n }\n}\n\n/**\n * Select › Modify › Smooth: box average of radius `round(r)` with replicated edges, then\n * re-thresholded at ½ — specks drop, corners round, straight edges stay hard.\n */\nexport function smoothCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const rr = Number.isFinite(r) ? Math.max(0, Math.round(r)) : 0;\n const src = Float32Array.from(m);\n const tmp = new Float32Array(src.length);\n const line = new Float32Array(Math.max(w, h));\n if (rr > 0) {\n for (let y = 0; y < h; y++) {\n boxLineReplicate(src, y * w, 1, w, rr, line);\n tmp.set(line.subarray(0, w), y * w);\n }\n for (let x = 0; x < w; x++) {\n boxLineReplicate(tmp, x, w, h, rr, line);\n for (let y = 0; y < h; y++) src[y * w + x] = line[y];\n }\n }\n for (let i = 0; i < src.length; i++) src[i] = src[i] >= 0.5 ? 1 : 0;\n return src;\n}\n\n\n/** Select › Modify › Feather: Gaussian blur with σ = radius (as Photoshop), zero beyond the image. */\nexport function featherCoverage(m: ArrayLike<number>, w: number, h: number, radius: number): Float32Array {\n const out = gaussianBlur(m, w, h, Number.isFinite(radius) ? Math.max(0, radius) : 0, 'zero');\n for (let i = 0; i < out.length; i++) if (Math.abs(out[i]) < 1e-7) out[i] = 0;\n return out;\n}\n\n/**\n * Running max (or min) over a window of radius `r` along one line (edge-clipped), van Herk /\n * Gil–Werman: about three comparisons per sample whatever the radius.\n */\nfunction runningExtreme(src: Float32Array, off: number, stride: number, len: number, r: number, max: boolean, g: Float32Array, hb: Float32Array, dst: Float32Array) {\n const k = 2 * r + 1;\n const pick = max ? (a: number, b: number) => (a > b ? a : b) : (a: number, b: number) => (a < b ? a : b);\n const fill = max ? -Infinity : Infinity;\n // Padded line: r fills on each side.\n const plen = len + 2 * r;\n const at = (i: number) => (i < r || i >= r + len ? fill : src[off + (i - r) * stride]);\n for (let start = 0; start < plen; start += k) {\n const end = Math.min(plen, start + k);\n g[start] = at(start);\n for (let i = start + 1; i < end; i++) g[i] = pick(g[i - 1], at(i));\n hb[end - 1] = at(end - 1);\n for (let i = end - 2; i >= start; i--) hb[i] = pick(hb[i + 1], at(i));\n }\n for (let i = 0; i < len; i++) {\n // Window over padded indices i .. i + 2r.\n const a = hb[i];\n const b = g[i + 2 * r];\n dst[i] = pick(a, b);\n }\n}\n\n/** Grey dilation (`grow`) or erosion with a square of radius `r`, separable, O(1) per pixel. */\nexport function greyMorphology(src: ArrayLike<number>, w: number, h: number, r: number, grow: boolean): Float32Array {\n const out = Float32Array.from(src);\n if (r <= 0) return out;\n const n = Math.max(w, h);\n const g = new Float32Array(n + 2 * r);\n const hb = new Float32Array(n + 2 * r);\n const line = new Float32Array(n);\n for (let y = 0; y < h; y++) {\n runningExtreme(out, y * w, 1, w, r, grow, g, hb, line);\n out.set(line.subarray(0, w), y * w);\n }\n for (let x = 0; x < w; x++) {\n runningExtreme(out, x, w, h, r, grow, g, hb, line);\n for (let y = 0; y < h; y++) out[y * w + x] = line[y];\n }\n return out;\n}\n\n/**\n * Grows (positive) or shrinks (negative) soft coverage by a fractional distance: whole pixels\n * use one morphology, the remainder blends with the next pixel, so 0.3 px differs from 1 px.\n */\nexport function shiftCoverage(src: ArrayLike<number>, w: number, h: number, amount: number): Float32Array {\n const mag = Math.abs(amount);\n if (!Number.isFinite(mag) || mag < 1e-4) return Float32Array.from(src);\n const grow = amount > 0;\n const whole = Math.floor(mag);\n const frac = mag - whole;\n if (frac < 1e-4) return greyMorphology(src, w, h, whole, grow);\n const lo = whole === 0 ? Float32Array.from(src) : greyMorphology(src, w, h, whole, grow);\n const hi = greyMorphology(src, w, h, whole + 1, grow);\n for (let i = 0; i < lo.length; i++) lo[i] += (hi[i] - lo[i]) * frac;\n return lo;\n}\n\n// ---------------------------------------------------------------------------\n// Selection-level wrappers\n// ---------------------------------------------------------------------------\n\nexport type SelectionModifyOp = 'grow' | 'shrink' | 'feather' | 'border' | 'smooth';\n\nexport const SELECTION_MODIFY_OPS: readonly SelectionModifyOp[] = ['grow', 'shrink', 'feather', 'border', 'smooth'];\n\nfunction wrap(mask: SelectionMask, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(mask.width, mask.height, out);\n}\n\n/** Select › Modify (grow = Expand, shrink = Contract, feather, border, smooth) by `radius` pixels. */\nexport function modifySelection(mask: SelectionMask, op: SelectionModifyOp, radius: number): SelectionMask {\n const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;\n if (r === 0 && op !== 'border') return mask;\n const m = selectionToCoverage(mask);\n const { width: w, height: h } = mask;\n switch (op) {\n case 'grow':\n return wrap(mask, expandCoverage(m, w, h, r));\n case 'shrink':\n return wrap(mask, contractCoverage(m, w, h, r));\n case 'feather':\n return wrap(mask, featherCoverage(m, w, h, r));\n case 'border':\n return wrap(mask, borderCoverage(m, w, h, r));\n case 'smooth':\n return wrap(mask, smoothCoverage(m, w, h, r));\n default:\n throw new Error(`Unknown selection modification \"${op as string}\"`);\n }\n}\n","// Portions ported from photocraft/crates/engine/src/layer_mask_props_cmds.rs and photocraft/crates/compose/src/masks.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentLayer, AdjustmentLayerMask } from '../types';\nimport { unprojectCanvasPoint, type ImageProjection } from '../geometry';\nimport {\n createSelectionMask,\n decodeSelectionRle,\n encodeSelectionRle,\n hashMaskContent,\n type SelectionMask,\n} from './mask';\nimport { gaussianBlur } from './modify';\n\n/** Properties of a layer mask that leave its pixels intact (Photoshop's Properties › Masks). */\nexport interface LayerMaskProps {\n /** 0..1 (default 1): at 0.5 the masked-out areas still get half of the adjustment. */\n density?: number;\n /** Gaussian feather σ in pixels, 0..1000 (default 0). */\n feather?: number;\n /** Disabled masks are kept but ignored (the layer applies everywhere). Default true. */\n enabled?: boolean;\n /**\n * Layer masks only: `false` unlinks the mask, so moving or transforming the layer's pixels leaves\n * it in place. Default true. Adjustment-layer masks in canvas space ignore it.\n */\n linked?: boolean;\n}\n\nconst MAX_SIDE = 1 << 15;\n\n/**\n * A clean layer mask from stored data, or `null` when it is unusable. Defaults are left out\n * (density 1, feather 0, enabled), so a plain mask serializes as `{width, height, rle}`. The\n * RLE is only decoded when rendering.\n */\nexport function normalizeLayerMask(raw: unknown): AdjustmentLayerMask | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const { width, height, rle } = r;\n if (\n typeof width !== 'number' ||\n typeof height !== 'number' ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width < 1 ||\n height < 1 ||\n width > MAX_SIDE ||\n height > MAX_SIDE ||\n typeof rle !== 'string' ||\n !rle.startsWith('r1:')\n ) {\n return null;\n }\n const out: AdjustmentLayerMask = { width, height, rle };\n if (typeof r.density === 'number' && Number.isFinite(r.density)) {\n const d = Math.min(1, Math.max(0, r.density));\n if (d !== 1) out.density = d;\n }\n if (typeof r.feather === 'number' && Number.isFinite(r.feather)) {\n const f = Math.min(1000, Math.max(0, r.feather));\n if (f !== 0) out.feather = f;\n }\n if (r.enabled === false) out.enabled = false;\n return out;\n}\n\n/** A layer mask holding the selection's pixels (Add Layer Mask › Reveal Selection). */\nexport function layerMaskFromSelection(selection: SelectionMask, props: LayerMaskProps = {}): AdjustmentLayerMask {\n return normalizeLayerMask({ width: selection.width, height: selection.height, rle: encodeSelectionRle(selection.data), ...props })!;\n}\n\n/** Applies property changes; `undefined` fields keep their value. */\nexport function updateLayerMaskProps(mask: AdjustmentLayerMask, props: LayerMaskProps): AdjustmentLayerMask {\n return normalizeLayerMask({\n ...mask,\n ...(props.density !== undefined ? { density: props.density } : {}),\n ...(props.feather !== undefined ? { feather: props.feather } : {}),\n ...(props.enabled !== undefined ? { enabled: props.enabled } : {}),\n })!;\n}\n\n// Small LRU caches: masks are stored as RLE strings and layers are deep-copied often.\nconst DECODE_LIMIT = 8;\nconst decoded = new Map<string, SelectionMask | null>();\nconst coverageCache = new Map<string, Float32Array | null>();\n\nfunction lru<T>(cache: Map<string, T>, key: string, make: () => T): T {\n if (cache.has(key)) {\n const v = cache.get(key)!;\n cache.delete(key);\n cache.set(key, v);\n return v;\n }\n const v = make();\n cache.set(key, v);\n if (cache.size > DECODE_LIMIT) cache.delete(cache.keys().next().value as string);\n return v;\n}\n\n/** The mask's pixels as a selection (Load Selection from mask), or `null` when the data is damaged. */\nexport function decodeLayerMask(mask: AdjustmentLayerMask): SelectionMask | null {\n const key = `${mask.width}x${mask.height}:${mask.rle}`;\n return lru(decoded, key, () => {\n try {\n return createSelectionMask(mask.width, mask.height, decodeSelectionRle(mask.rle, mask.width * mask.height));\n } catch {\n return null;\n }\n });\n}\n\n/** Cache key of everything about a mask that changes pixels. */\nexport function layerMaskKey(mask: AdjustmentLayerMask | undefined): string {\n if (!mask) return '';\n return `${mask.width}x${mask.height}:${hashMaskContent(mask.rle)}:${mask.density ?? 1}:${mask.feather ?? 0}:${mask.enabled !== false}`;\n}\n\n/**\n * The mask's effective coverage (0..1, mask resolution): feathered (σ = feather, borders\n * replicated), then density applied as `1 − density·(1 − v)`. `null` for a disabled or damaged\n * mask, meaning \"no mask\".\n */\nexport function layerMaskCoverage(mask: AdjustmentLayerMask): Float32Array | null {\n if (mask.enabled === false) return null;\n return lru(coverageCache, layerMaskKey(mask), () => {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n let v: Float32Array = new Float32Array(sel.data.length);\n for (let i = 0; i < v.length; i++) v[i] = sel.data[i] / 255;\n const feather = mask.feather ?? 0;\n if (feather > 0) v = gaussianBlur(v, mask.width, mask.height, feather, 'clamp');\n const density = mask.density ?? 1;\n if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);\n return v;\n });\n}\n\n/**\n * Resamples mask coverage (natural-image aligned, any resolution) into a target raster drawn\n * through `projection` (the base canvas of the renderer: crop, rotation, flips): each target\n * pixel centre is unprojected to natural pixels and sampled bilinearly.\n */\nexport function projectMaskCoverage(\n coverage: Float32Array,\n maskWidth: number,\n maskHeight: number,\n naturalWidth: number,\n naturalHeight: number,\n projection: ImageProjection,\n targetWidth: number,\n targetHeight: number\n): Float32Array {\n const out = new Float32Array(targetWidth * targetHeight);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, projection);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, projection);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, projection);\n const kx = maskWidth / naturalWidth;\n const ky = maskHeight / naturalHeight;\n // Affine map target → mask coordinates (pixel centres at +0.5).\n const ux = (ox.x - o.x) * kx;\n const vx = (ox.y - o.y) * ky;\n const uy = (oy.x - o.x) * kx;\n const vy = (oy.y - o.y) * ky;\n const u0 = o.x * kx - 0.5;\n const v0 = o.y * ky - 0.5;\n const mw1 = maskWidth - 1;\n const mh1 = maskHeight - 1;\n for (let y = 0; y < targetHeight; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n const row = y * targetWidth;\n for (let x = 0; x < targetWidth; x++, u += ux, v += vx) {\n const fu = u < 0 ? 0 : u > mw1 ? mw1 : u;\n const fv = v < 0 ? 0 : v > mh1 ? mh1 : v;\n const x0 = fu | 0;\n const y0 = fv | 0;\n const x1 = x0 < mw1 ? x0 + 1 : x0;\n const y1 = y0 < mh1 ? y0 + 1 : y0;\n const tx = fu - x0;\n const ty = fv - y0;\n const a = coverage[y0 * maskWidth + x0] * (1 - tx) + coverage[y0 * maskWidth + x1] * tx;\n const b = coverage[y1 * maskWidth + x0] * (1 - tx) + coverage[y1 * maskWidth + x1] * tx;\n out[row + x] = a * (1 - ty) + b * ty;\n }\n }\n return out;\n}\n\n/**\n * Per-layer mask coverage for a render target, keyed by layer id; `undefined` when no layer has\n * an enabled, valid mask (so mask-free stacks take the exact same path as before).\n */\nexport function buildLayerMaskPlanes(\n layers: readonly AdjustmentLayer[] | undefined,\n naturalWidth: number,\n naturalHeight: number,\n projection: ImageProjection,\n targetWidth: number,\n targetHeight: number\n): Map<string, Float32Array> | undefined {\n if (!layers?.some((l) => l.mask && l.mask.enabled !== false)) return undefined;\n const planes = new Map<string, Float32Array>();\n for (const layer of layers) {\n if (!layer.mask) continue;\n const coverage = layerMaskCoverage(layer.mask);\n if (!coverage) continue;\n planes.set(\n layer.id,\n projectMaskCoverage(coverage, layer.mask.width, layer.mask.height, naturalWidth, naturalHeight, projection, targetWidth, targetHeight)\n );\n }\n return planes.size ? planes : undefined;\n}\n","// Portions ported from photocraft-main/crates/compose/src/adjust.rs, photocraft-main/crates/doc/src/adjust.rs and photocraft-main/crates/color/src/convert.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentLayerMask,\n AdjustmentLayerOf,\n AdjustmentParamsMap,\n BlackWhiteParams,\n ColorBalanceParams,\n ColorBalanceTone,\n CurvePoint,\n HueRange,\n HueSaturationParams,\n LevelsChannel,\n PhotoFilterParams,\n VibranceParams,\n} from './types';\nimport { type BlendMode, blendRgb, mixPigmentRgb, normalizeBlendMode, setLum } from './blend';\nimport { linearToSrgb, srgbToLinear } from './color';\nimport { layerMaskKey, normalizeLayerMask } from './selection/layer-mask';\nimport { curveLut } from '../raster/curve';\n\nexport { curveLut };\n\n/**\n * Adjustment layers: Photoshop-matching evaluation on encoded sRGB colours in 0..1.\n *\n * Every layer is compiled once per parameter change into a per-pixel function (per-channel kinds\n * into three 4096-entry LUTs); {@link applyAdjustmentLayers} runs the stack on an ImageData.\n */\n\ntype Rgb = [number, number, number];\n\n/** Entries in a tone LUT (curves, levels, colour balance). */\nexport const ADJUSTMENT_LUT_SIZE = 4096;\n\nconst clamp01 = (v: number) => (v <= 0 ? 0 : v >= 1 ? 1 : v);\nconst finite = (v: unknown, fallback: number) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);\nconst clampTo = (v: unknown, lo: number, hi: number, fallback: number) => Math.min(hi, Math.max(lo, finite(v, fallback)));\nconst remEuclid = (a: number, b: number) => ((a % b) + b) % b;\n\n/** Linear interpolation into a 0..1-domain LUT. */\nexport function sampleLut(table: ArrayLike<number>, v: number): number {\n const n = table.length - 1;\n const x = clamp01(v) * n;\n const i = Math.floor(x);\n const j = Math.min(i + 1, n);\n const f = x - i;\n return table[i] * (1 - f) + table[j] * f;\n}\n\n// ---------------------------------------------------------------------------\n// Kinds, labels, defaults\n// ---------------------------------------------------------------------------\n\nexport interface AdjustmentKindInfo {\n kind: AdjustmentKind;\n label: string;\n}\n\nexport const ADJUSTMENT_KINDS: readonly AdjustmentKindInfo[] = [\n { kind: 'curves', label: 'Curves' },\n { kind: 'levels', label: 'Levels' },\n { kind: 'hueSaturation', label: 'Hue/Saturation' },\n { kind: 'vibrance', label: 'Vibrance' },\n { kind: 'colorBalance', label: 'Color Balance' },\n { kind: 'blackWhite', label: 'Black & White' },\n { kind: 'photoFilter', label: 'Photo Filter' },\n { kind: 'invert', label: 'Invert' },\n { kind: 'posterize', label: 'Posterize' },\n { kind: 'threshold', label: 'Threshold' },\n];\n\nconst KIND_LABELS = new Map(ADJUSTMENT_KINDS.map((k) => [k.kind, k.label]));\n\nexport function isAdjustmentKind(value: unknown): value is AdjustmentKind {\n return typeof value === 'string' && KIND_LABELS.has(value as AdjustmentKind);\n}\n\nexport function adjustmentKindLabel(kind: AdjustmentKind): string {\n return KIND_LABELS.get(kind) ?? kind;\n}\n\nexport const HUE_RANGE_LABELS = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'] as const;\n\n/** The neutral range `i` (0 reds … 5 magentas): fully on for 30° around its centre, fading over 30° each side. */\nexport function neutralHueRange(i: number): HueRange {\n const c = 60 * i;\n return { hue: 0, saturation: 0, lightness: 0, bounds: [c - 45, c - 15, c + 15, c + 45] };\n}\n\nexport const identityCurve = (): CurvePoint[] => [\n { input: 0, output: 0 },\n { input: 255, output: 255 },\n];\n\nexport const identityLevels = (): LevelsChannel => ({ inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 });\n\n/** Neutral parameters of every kind (fresh objects on every call). */\nexport function defaultAdjustmentParams<K extends AdjustmentKind>(kind: K): AdjustmentParamsMap[K] {\n const defaults: { [P in AdjustmentKind]: () => AdjustmentParamsMap[P] } = {\n curves: () => ({ master: identityCurve(), red: identityCurve(), green: identityCurve(), blue: identityCurve() }),\n levels: () => ({ master: identityLevels(), red: identityLevels(), green: identityLevels(), blue: identityLevels() }),\n hueSaturation: () => ({\n hue: 0,\n saturation: 0,\n lightness: 0,\n colorize: false,\n ranges: [0, 1, 2, 3, 4, 5].map(neutralHueRange),\n }),\n vibrance: () => ({ vibrance: 0, saturation: 0 }),\n colorBalance: () => ({ shadows: [0, 0, 0], midtones: [0, 0, 0], highlights: [0, 0, 0], preserveLuminosity: true }),\n blackWhite: () => ({ reds: 40, yellows: 60, greens: 40, cyans: 60, blues: 20, magentas: 80, tint: null }),\n invert: () => ({}) as Record<string, never>,\n posterize: () => ({ levels: 4 }),\n threshold: () => ({ level: 128 }),\n photoFilter: () => ({ color: '#ec8a00', density: 25, preserveLuminosity: true }),\n };\n return defaults[kind]() as AdjustmentParamsMap[K];\n}\n\nlet layerCounter = 0;\n\n/** A new layer of `kind` with neutral (or the given) parameters, enabled, opacity 1, normal blend. */\nexport function createAdjustmentLayer<K extends AdjustmentKind>(\n kind: K,\n options: {\n id?: string;\n name?: string;\n params?: Partial<AdjustmentParamsMap[K]>;\n enabled?: boolean;\n opacity?: number;\n blend?: BlendMode;\n mask?: AdjustmentLayerMask;\n } = {}\n): AdjustmentLayerOf<K> {\n const raw = {\n id: options.id ?? `adj_${Date.now().toString(36)}_${(++layerCounter).toString(36)}`,\n kind,\n name: options.name,\n params: { ...defaultAdjustmentParams(kind), ...(options.params ?? {}) },\n enabled: options.enabled ?? true,\n opacity: options.opacity ?? 1,\n blend: options.blend ?? 'normal',\n mask: options.mask,\n };\n return normalizeAdjustmentLayer(raw) as AdjustmentLayerOf<K>;\n}\n\n// ---------------------------------------------------------------------------\n// Normalisation of stored layers\n// ---------------------------------------------------------------------------\n\nconst HEX = /^#[0-9a-f]{6}$/i;\n\nfunction normCurve(points: unknown): CurvePoint[] {\n if (!Array.isArray(points)) return identityCurve();\n const out = points\n .filter((p) => p && typeof p === 'object')\n .map((p) => ({ input: clampTo(p.input, 0, 255, 0), output: clampTo(p.output, 0, 255, 0) }))\n .sort((a, b) => a.input - b.input);\n return out.length >= 2 ? out : identityCurve();\n}\n\nfunction normLevels(ch: unknown): LevelsChannel {\n const c = (ch && typeof ch === 'object' ? ch : {}) as Partial<LevelsChannel>;\n return {\n inBlack: clampTo(c.inBlack, 0, 254, 0),\n inWhite: clampTo(c.inWhite, 1, 255, 255),\n gamma: clampTo(c.gamma, 0.1, 9.99, 1),\n outBlack: clampTo(c.outBlack, 0, 255, 0),\n outWhite: clampTo(c.outWhite, 0, 255, 255),\n };\n}\n\nfunction normTone(t: unknown): ColorBalanceTone {\n const a = Array.isArray(t) ? t : [];\n return [clampTo(a[0], -100, 100, 0), clampTo(a[1], -100, 100, 0), clampTo(a[2], -100, 100, 0)];\n}\n\nfunction normParams(kind: AdjustmentKind, raw: unknown): AdjustmentParamsMap[AdjustmentKind] {\n const p = (raw && typeof raw === 'object' ? raw : {}) as Record<string, any>;\n const d = defaultAdjustmentParams(kind) as Record<string, any>;\n switch (kind) {\n case 'curves':\n return { master: normCurve(p.master), red: normCurve(p.red), green: normCurve(p.green), blue: normCurve(p.blue) };\n case 'levels':\n return { master: normLevels(p.master), red: normLevels(p.red), green: normLevels(p.green), blue: normLevels(p.blue) };\n case 'hueSaturation': {\n const ranges: HueRange[] = [0, 1, 2, 3, 4, 5].map((i) => {\n const r = Array.isArray(p.ranges) && p.ranges[i] && typeof p.ranges[i] === 'object' ? p.ranges[i] : {};\n const n = neutralHueRange(i);\n const b = Array.isArray(r.bounds) && r.bounds.length === 4 && r.bounds.every((v: unknown) => Number.isFinite(v)) ? r.bounds : n.bounds;\n return {\n hue: clampTo(r.hue, -180, 180, 0),\n saturation: clampTo(r.saturation, -100, 100, 0),\n lightness: clampTo(r.lightness, -100, 100, 0),\n bounds: [b[0], b[1], b[2], b[3]] as [number, number, number, number],\n };\n });\n const colorize = p.colorize === true;\n return {\n hue: colorize ? clampTo(p.hue, 0, 360, 0) : clampTo(p.hue, -180, 180, 0),\n saturation: clampTo(p.saturation, -100, 100, 0),\n lightness: clampTo(p.lightness, -100, 100, 0),\n colorize,\n ranges,\n } satisfies HueSaturationParams;\n }\n case 'vibrance':\n return { vibrance: clampTo(p.vibrance, -100, 100, 0), saturation: clampTo(p.saturation, -100, 100, 0) } satisfies VibranceParams;\n case 'colorBalance':\n return {\n shadows: normTone(p.shadows),\n midtones: normTone(p.midtones),\n highlights: normTone(p.highlights),\n preserveLuminosity: p.preserveLuminosity !== false,\n } satisfies ColorBalanceParams;\n case 'blackWhite': {\n const out = {} as BlackWhiteParams;\n for (const k of ['reds', 'yellows', 'greens', 'cyans', 'blues', 'magentas'] as const) out[k] = clampTo(p[k], -200, 300, d[k]);\n out.tint = typeof p.tint === 'string' && HEX.test(p.tint) ? p.tint.toLowerCase() : null;\n return out;\n }\n case 'photoFilter':\n return {\n color: typeof p.color === 'string' && HEX.test(p.color) ? p.color.toLowerCase() : d.color,\n density: clampTo(p.density, 0, 100, d.density),\n preserveLuminosity: p.preserveLuminosity !== false,\n } satisfies PhotoFilterParams;\n case 'posterize':\n return { levels: Math.round(clampTo(p.levels, 2, 255, 4)) };\n case 'threshold':\n return { level: Math.round(clampTo(p.level, 1, 255, 128)) };\n case 'invert':\n return {} as Record<string, never>;\n }\n}\n\n/**\n * A clean, deep-copied layer from stored or hand-written data: missing fields get neutral\n * defaults, numbers are clamped to their ranges. `null` for an unknown kind.\n */\nexport function normalizeAdjustmentLayer(raw: unknown): AdjustmentLayer | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, any>;\n if (!isAdjustmentKind(r.kind)) return null;\n const layer: Record<string, unknown> = {\n id: typeof r.id === 'string' && r.id ? r.id : `adj_${Date.now().toString(36)}_${(++layerCounter).toString(36)}`,\n kind: r.kind,\n enabled: r.enabled !== false,\n opacity: clampTo(r.opacity, 0, 1, 1),\n blend: normalizeBlendMode(r.blend),\n params: normParams(r.kind, r.params),\n };\n if (typeof r.name === 'string' && r.name) layer.name = r.name;\n // Only present when the layer has a usable mask, so mask-free layers serialize as before.\n const mask = normalizeLayerMask(r.mask);\n if (mask) layer.mask = mask;\n return layer as unknown as AdjustmentLayer;\n}\n\n/** Normalises a stored layer list (drops unknown kinds, de-duplicates ids). */\nexport function normalizeAdjustmentLayers(raw: unknown): AdjustmentLayer[] {\n if (!Array.isArray(raw)) return [];\n const seen = new Set<string>();\n const out: AdjustmentLayer[] = [];\n for (const item of raw) {\n const layer = normalizeAdjustmentLayer(item);\n if (!layer) continue;\n while (seen.has(layer.id)) layer.id = `${layer.id}_${(++layerCounter).toString(36)}`;\n seen.add(layer.id);\n out.push(layer);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Curves\n// ---------------------------------------------------------------------------\n\n/** {@link curveLut} for Curves points in levels (0..255). */\nexport function curveLutFromLevels(points: readonly CurvePoint[]): Float32Array {\n return curveLut(points.map((p) => ({ input: p.input / 255, output: p.output / 255 })));\n}\n\n// ---------------------------------------------------------------------------\n// Levels\n// ---------------------------------------------------------------------------\n\n/** A Levels record with values in 0..1 (as `levelsValue` takes them). */\nexport interface UnitLevels {\n inBlack: number;\n inWhite: number;\n gamma: number;\n outBlack: number;\n outWhite: number;\n}\n\nconst toUnitLevels = (c: LevelsChannel): UnitLevels => ({\n inBlack: c.inBlack / 255,\n inWhite: c.inWhite / 255,\n gamma: c.gamma,\n outBlack: c.outBlack / 255,\n outWhite: c.outWhite / 255,\n});\n\n/**\n * One Levels record on `v` (0..1). With `quantum` (8-bit: 255) Photoshop works on whole levels:\n * the input is a level and the stretch is rounded (half up) before the gamma. A gamma above 1 has\n * Photoshop's soft shadow toe: a p-norm soft-min of the power curve and a line of slope ≈ 2^gamma.\n */\nexport function levelsValue(ch: UnitLevels, v: number, quantum?: number): number {\n const range = Math.max(1e-6, ch.inWhite - ch.inBlack);\n let u: number;\n if (quantum !== undefined && Number.isFinite(quantum) && quantum >= 1) {\n const q = quantum;\n const vq = Math.round(v * q) / q;\n u = Math.floor(clamp01((vq - ch.inBlack) / range) * q + 0.5 + 1e-3) / q;\n } else {\n u = clamp01((v - ch.inBlack) / range);\n }\n const g = Math.max(0.01, ch.gamma);\n let t: number;\n if (g > 1) {\n const power = Math.pow(u, 1 / g);\n const line = 0.93 * Math.pow(2, g) * u;\n if (power <= 1e-6 || line <= 1e-6) t = Math.min(power, line);\n else {\n const p = 10;\n t = Math.pow(Math.pow(power, -p) + Math.pow(line, -p), -1 / p);\n }\n } else {\n t = Math.pow(u, 1 / g);\n }\n return ch.outBlack + t * (ch.outWhite - ch.outBlack);\n}\n\n// ---------------------------------------------------------------------------\n// HSL and Hue/Saturation\n// ---------------------------------------------------------------------------\n\n/** RGB (0..1) → HSL with hue in turns (0..1). */\nexport function rgbToHsl(c: readonly number[]): [number, number, number] {\n const max = Math.max(c[0], c[1], c[2]);\n const min = Math.min(c[0], c[1], c[2]);\n const l = (max + min) / 2;\n if (Math.abs(max - min) < 1e-7) return [0, 0, l];\n const d = max - min;\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n let h: number;\n if (max === c[0]) h = remEuclid((c[1] - c[2]) / d, 6);\n else if (max === c[1]) h = (c[2] - c[0]) / d + 2;\n else h = (c[0] - c[1]) / d + 4;\n return [h / 6, s, l];\n}\n\nexport function hslToRgb(h: number, s: number, l: number): Rgb {\n if (s <= 0) return [l, l, l];\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n const f = (t: number) => {\n t = remEuclid(t, 1);\n if (t < 1 / 6) return p + (q - p) * 6 * t;\n if (t < 0.5) return q;\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n return p;\n };\n return [f(h + 1 / 3), f(h), f(h - 1 / 3)];\n}\n\n/** How much a pixel of hue `deg` belongs to `range` (0..1): a trapezoid that may wrap past 0/360. */\nexport function hueRangeWeight(range: Pick<HueRange, 'bounds'>, deg: number): number {\n const [a, b0, c0, d0] = range.bounds;\n const rel = (x: number) => (Number.isFinite(x) && Number.isFinite(a) ? remEuclid(x - a, 360) : 0);\n const b = rel(b0);\n const c = Math.max(rel(c0), b);\n const d = Math.max(rel(d0), c);\n const h = rel(deg);\n if (h <= b) return b <= 0 ? 1 : h / b;\n if (h <= c) return 1;\n if (h <= d) return d <= c ? 1 : (d - h) / (d - c);\n return 0;\n}\n\nconst isNeutralRange = (r: HueRange) => r.hue === 0 && r.saturation === 0 && r.lightness === 0;\n\n/** Range edits as three hue-indexed tables (hue shift in degrees, saturation and lightness as fractions). */\nexport function hueRangeTables(ranges: readonly HueRange[]): [Float32Array, Float32Array, Float32Array] {\n const n = ADJUSTMENT_LUT_SIZE;\n const t: [Float32Array, Float32Array, Float32Array] = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];\n const active = ranges.filter((r) => !isNeutralRange(r));\n for (let k = 0; k < n; k++) {\n const deg = (k / (n - 1)) * 360;\n for (const r of active) {\n const w = hueRangeWeight(r, deg);\n t[0][k] += w * r.hue;\n t[1][k] += (w * r.saturation) / 100;\n t[2][k] += (w * r.lightness) / 100;\n }\n }\n return t;\n}\n\n/**\n * One Hue/Saturation evaluation: hue shift in degrees, saturation and lightness in -1..1.\n * Lightness first, then hue and saturation (raising divides by 1 − s, lowering multiplies by\n * 1 + s). Colorize sets hue and saturation at the lightened lightness.\n */\nexport function hueSaturation(c: readonly number[], hue: number, s: number, l: number, colorize: boolean): Rgb {\n const lighten = (v: number) => (l > 0 ? v + (1 - v) * l : v * (1 + l));\n if (colorize) {\n const ll = rgbToHsl(c)[2];\n return hslToRgb(remEuclid(hue, 360) / 360, Math.min(1, Math.abs(s)), lighten(ll));\n }\n const [hh0, ss0, ll] = rgbToHsl([lighten(c[0]), lighten(c[1]), lighten(c[2])]);\n const hh = remEuclid(hh0 + hue / 360, 1);\n const ss = Math.min(1, Math.max(0, s > 0 ? ss0 / Math.max(1 / 512, 1 - s) : ss0 * (1 + s)));\n return hslToRgb(hh, ss, ll);\n}\n\n/** A colour range's Lightness: towards the brightest channel when raised, the darkest when lowered. */\nexport function rangeLightness(c: readonly number[], l: number): Rgb {\n const k = Math.min(1, Math.max(-1, l));\n const target = k > 0 ? Math.max(c[0], c[1], c[2]) : Math.min(c[0], c[1], c[2]);\n const a = Math.abs(k);\n return [c[0] + (target - c[0]) * a, c[1] + (target - c[1]) * a, c[2] + (target - c[2]) * a];\n}\n\n// ---------------------------------------------------------------------------\n// Vibrance\n// ---------------------------------------------------------------------------\n\n/** Grey Vibrance › Saturation desaturates to (linear light; blue has no weight). */\nexport const VIBRANCE_GREY: readonly [number, number] = [0.288, 0.712];\nconst VIBRANCE_BOOST = [0.5001, 0.0887, 0.4374, 0.7573, -0.8113, 0.5154, 2.8026] as const;\n\nfunction vibranceSkin(h: number): number {\n if (h <= 30) return 1;\n if (h < 45) return (45 - h) / 15;\n if (h < 300) return 0;\n return (h - 300) / 60;\n}\n\nfunction hsvHue(c: readonly number[], mx: number, mn: number): number {\n const d = mx - mn;\n let h: number;\n if (mx === c[0]) h = remEuclid((c[1] - c[2]) / d, 6);\n else if (mx === c[1]) h = (c[2] - c[0]) / d + 2;\n else h = (c[0] - c[1]) / d + 4;\n return h * 60;\n}\n\n/** The Vibrance slider (`v` in -1..1) on a linear-light pixel: an HSV change that keeps the hue. */\nexport function vibranceLinear(c: Rgb, v: number): Rgb {\n const mx = Math.max(c[0], c[1], c[2]);\n const mn = Math.min(c[0], c[1], c[2]);\n if (mx <= 0 || mx - mn <= 1e-9) return c;\n const t = 1 - mn / mx;\n const shape = t * (1 - t) * (2 - t + t * t) * mx * Math.max(0, 1 - mx);\n let t2: number, v2: number;\n if (v < 0) {\n const a = -v;\n t2 = t * (1 - a / 4) * (1 - a + (a * t * (1 + t)) / 2);\n v2 = mx - a * shape;\n } else {\n const [c0, c1, p, q, r0, ra, rt] = VIBRANCE_BOOST;\n const w = vibranceSkin(hsvHue(c, mx, mn));\n const a = v * (1 - w) + 0.64 * Math.pow(v, 1.6) * w;\n const boost =\n (c0 * a + c1 * a * a) * Math.pow(t, p) * Math.pow(1 - t, q) * Math.exp((r0 + ra * a + rt * (1 - t)) * Math.max(0, 1 - mx));\n t2 = Math.min(1, t * (1 + boost));\n v2 = mx + (v / 4) * shape;\n }\n const m2 = v2 * (1 - t2);\n const k = (v2 - m2) / (mx - mn);\n return [m2 + (c[0] - mn) * k, m2 + (c[1] - mn) * k, m2 + (c[2] - mn) * k];\n}\n\n/** Photoshop's Vibrance on one encoded sRGB pixel (`v`, `s`: the sliders / 100). */\nexport function vibrancePixel(c: readonly number[], v: number, s: number): Rgb {\n let l: Rgb = [srgbToLinear(Math.max(0, c[0])), srgbToLinear(Math.max(0, c[1])), srgbToLinear(Math.max(0, c[2]))];\n if (v !== 0) l = vibranceLinear(l, v);\n if (s !== 0) {\n const g = VIBRANCE_GREY[0] * l[0] + VIBRANCE_GREY[1] * l[1];\n l = l.map((x) => Math.min(1, Math.max(0, g + (1 + s) * (x - g)))) as Rgb;\n }\n return l.map((x) => linearToSrgb(clamp01(x))) as Rgb;\n}\n\n// ---------------------------------------------------------------------------\n// Color Balance\n// ---------------------------------------------------------------------------\n\n/** How far one Color Balance slider at ±100 moves the black or white point. */\nconst BALANCE_LEVELS = 0.39;\n\n/**\n * Color Balance on channel `ch` (0 red, 1 green, 2 blue) of `v` (0..1); `tones` are the\n * shadows, midtones and highlights slider triples (-100..100). Highlights, shadows, then midtones.\n */\nexport function colorBalanceChannel(\n tones: readonly [readonly number[], readonly number[], readonly number[]],\n preserveLuminosity: boolean,\n ch: number,\n v: number\n): number {\n const [shadows, midtones, highlights] = tones;\n const black = (x: number, b: number) => Math.max(0, (x - b) / (1 - b));\n const white = (x: number, w: number) => Math.min(1, x / w);\n let out = clamp01(v);\n for (const [tone, sliders] of [\n [2, highlights],\n [0, shadows],\n [1, midtones],\n ] as const) {\n const a = [0, 1, 2].map((i) => Math.min(1, Math.max(-1, sliders[i] / 100)));\n if (preserveLuminosity) {\n const own = a[ch] ?? 0;\n const lo = Math.min(a[0], a[1], a[2]);\n const hi = Math.max(a[0], a[1], a[2]);\n if (tone === 0) out = black(out, BALANCE_LEVELS * (hi - own));\n else if (tone === 1) out = Math.pow(out, Math.pow(2, (hi + lo) / 2 - own));\n else out = white(out, 1 - BALANCE_LEVELS * (own - lo));\n continue;\n }\n const s = a[ch];\n if (s === 0) continue;\n if (tone === 1) out = Math.pow(out, Math.pow(2, -s));\n else if (tone === 0 && s < 0) out = Math.pow(black(out, BALANCE_LEVELS * -s), Math.pow(2, -s / 2));\n else if (tone === 2 && s > 0) out = Math.pow(white(out, 1 - BALANCE_LEVELS * s), Math.pow(2, -s / 2));\n else out = Math.pow(out, Math.pow(2, -s / 2));\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Black & White, Photo Filter, Posterize, Threshold\n// ---------------------------------------------------------------------------\n\n/**\n * Black & White grey: the smallest channel, plus the secondary part (middle − smallest) and the\n * primary part (largest − middle) weighted by the sliders (percent: reds, yellows, greens, cyans,\n * blues, magentas).\n */\nexport function blackWhiteGray(c: readonly number[], weights: readonly number[]): number {\n const [r, g, b] = c;\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n const mid = r + g + b - max - min;\n const primary = r >= g && r >= b ? 0 : g >= b ? 2 : 4;\n const secondary = b <= r && b <= g ? 1 : r <= g ? 3 : 5;\n return clamp01(min + ((mid - min) * weights[secondary]) / 100 + ((max - mid) * weights[primary]) / 100);\n}\n\n/** D50 white (ICC PCS). */\nexport const D50 = [0.96422, 1.0, 0.82521] as const;\n/** Linear sRGB → D50 XYZ (Bradford-adapted, as in the ICC sRGB profile). */\nconst SRGB_TO_XYZ_D50 = [\n [0.436074, 0.385064, 0.14308],\n [0.222504, 0.716878, 0.060618],\n [0.013932, 0.097104, 0.714173],\n] as const;\n/** D50 XYZ → linear sRGB. */\nexport const XYZ_D50_TO_SRGB = [\n [3.133856, -1.616867, -0.490615],\n [-0.978768, 1.916142, 0.033454],\n [0.071945, -0.228991, 1.405243],\n] as const;\n\n/**\n * Photo Filter's linear-light RGB matrix (row-major 3×3): the pixel's D50 XYZ, relative to white,\n * multiplied by the filter colour's mixed in by `density` (0..1). `normalizeY` keeps luminance Y.\n */\nexport function photoFilterMatrix(color: readonly number[], density: number, normalizeY = false): number[][] {\n const d = Number.isFinite(density) ? clamp01(density) : 0;\n const lin = [0, 1, 2].map((i) => srgbToLinear(clamp01(color[i])));\n const xyz = SRGB_TO_XYZ_D50.map((r) => r[0] * lin[0] + r[1] * lin[1] + r[2] * lin[2]);\n let s = [0, 1, 2].map((i) => 1 - d + (d * xyz[i]) / D50[i]);\n if (normalizeY && s[1] > 1e-6) {\n const y = s[1];\n s = s.map((v) => v / y);\n }\n return [0, 1, 2].map((r) =>\n [0, 1, 2].map((c) => XYZ_D50_TO_SRGB[r][0] * s[0] * SRGB_TO_XYZ_D50[0][c] + XYZ_D50_TO_SRGB[r][1] * s[1] * SRGB_TO_XYZ_D50[1][c] + XYZ_D50_TO_SRGB[r][2] * s[2] * SRGB_TO_XYZ_D50[2][c])\n );\n}\n\n/** Photoshop posterize: `levels` equal input bins over 0..255, outputs `floor(k·255 / (levels − 1))`. */\nexport function posterizeValue(v: number, levels: number): number {\n const n = Math.min(255, Math.max(2, Math.round(levels)));\n const x = Math.round(clamp01(v) * 255);\n const bin = Math.min(n - 1, Math.floor((x * n) / 256));\n return Math.floor((bin * 255) / (n - 1)) / 255;\n}\n\n/** Rec.601 luma (Photoshop's grey for Threshold). */\nexport const rgbToGray = (c: readonly number[]) => 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];\n\n/** `#rrggbb` → 0..1 RGB (black for anything else). */\nexport function hexToRgb(hex: string): Rgb {\n if (!HEX.test(hex)) return [0, 0, 0];\n const n = parseInt(hex.slice(1), 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\n// ---------------------------------------------------------------------------\n// Compilation and evaluation\n// ---------------------------------------------------------------------------\n\n/** Mutates an encoded sRGB colour (0..1) in place. */\nexport type PixelOp = (c: Rgb) => void;\n\nfunction lutOp(luts: [Float32Array, Float32Array, Float32Array]): PixelOp {\n const [lr, lg, lb] = luts;\n return (c) => {\n c[0] = sampleLut(lr, c[0]);\n c[1] = sampleLut(lg, c[1]);\n c[2] = sampleLut(lb, c[2]);\n };\n}\n\nconst isIdentityCurve = (pts: readonly CurvePoint[]) =>\n pts.length === 2 && pts[0].input === 0 && pts[0].output === 0 && pts[1].input === 255 && pts[1].output === 255;\n\nconst isIdentityLevels = (c: LevelsChannel) =>\n c.inBlack === 0 && c.inWhite === 255 && c.gamma === 1 && c.outBlack === 0 && c.outWhite === 255;\n\n/** Quantum Levels works in (8-bit documents: whole levels). */\nconst LEVELS_QUANTUM = 255;\n\n/**\n * Per-channel LUTs of a Curves or Levels layer: each channel's record first, then the master.\n * `null` for any other kind.\n */\nexport function toneLuts(layer: AdjustmentLayer): [Float32Array, Float32Array, Float32Array] | null {\n const n = ADJUSTMENT_LUT_SIZE;\n if (layer.kind === 'curves') {\n const m = curveLutFromLevels(layer.params.master);\n return [layer.params.red, layer.params.green, layer.params.blue].map((pts) => {\n const ch = curveLutFromLevels(pts);\n for (let k = 0; k < n; k++) ch[k] = sampleLut(m, ch[k]);\n return ch;\n }) as [Float32Array, Float32Array, Float32Array];\n }\n if (layer.kind === 'levels') {\n const m = toUnitLevels(layer.params.master);\n return [layer.params.red, layer.params.green, layer.params.blue].map((rec) => {\n const c = toUnitLevels(rec);\n const out = new Float32Array(n);\n for (let k = 0; k < n; k++) out[k] = levelsValue(m, levelsValue(c, k / (n - 1), LEVELS_QUANTUM), LEVELS_QUANTUM);\n return out;\n }) as [Float32Array, Float32Array, Float32Array];\n }\n return null;\n}\n\n/**\n * Compiles a layer into a pixel function, or `null` when it is the identity (neutral parameters).\n * Pure: depends on `kind` and `params` only.\n */\nexport function compileAdjustmentLayer(layer: AdjustmentLayer): PixelOp | null {\n switch (layer.kind) {\n case 'curves': {\n const p = layer.params;\n if ([p.master, p.red, p.green, p.blue].every(isIdentityCurve)) return null;\n return lutOp(toneLuts(layer)!);\n }\n case 'levels': {\n const p = layer.params;\n if ([p.master, p.red, p.green, p.blue].every(isIdentityLevels)) return null;\n return lutOp(toneLuts(layer)!);\n }\n case 'hueSaturation': {\n const p = layer.params;\n const rangesActive = !p.colorize && p.ranges.some((r) => !isNeutralRange(r));\n if (!p.colorize && !rangesActive && p.hue === 0 && p.saturation === 0 && p.lightness === 0) return null;\n const tables = rangesActive ? hueRangeTables(p.ranges) : null;\n const sat = p.saturation / 100;\n const light = Math.min(1, Math.max(-1, p.lightness / 100));\n return (c) => {\n let dh = 0, ds = 0, dl = 0;\n if (tables) {\n const h0 = rgbToHsl(c)[0];\n dh = sampleLut(tables[0], h0);\n ds = sampleLut(tables[1], h0);\n dl = sampleLut(tables[2], h0);\n }\n const out = rangeLightness(hueSaturation(c, p.hue + dh, Math.min(1, Math.max(-1, sat + ds)), light, p.colorize), dl);\n c[0] = out[0];\n c[1] = out[1];\n c[2] = out[2];\n };\n }\n case 'vibrance': {\n const v = layer.params.vibrance / 100;\n const s = layer.params.saturation / 100;\n if (v === 0 && s === 0) return null;\n return (c) => {\n const out = vibrancePixel(c, v, s);\n c[0] = out[0];\n c[1] = out[1];\n c[2] = out[2];\n };\n }\n case 'colorBalance': {\n const p = layer.params;\n const tones = [p.shadows, p.midtones, p.highlights] as const;\n if (tones.every((t) => t[0] === 0 && t[1] === 0 && t[2] === 0)) return null;\n const n = ADJUSTMENT_LUT_SIZE;\n const luts = [0, 1, 2].map((ch) => {\n const out = new Float32Array(n);\n for (let k = 0; k < n; k++) out[k] = colorBalanceChannel(tones, p.preserveLuminosity, ch, k / (n - 1));\n return out;\n }) as [Float32Array, Float32Array, Float32Array];\n return lutOp(luts);\n }\n case 'blackWhite': {\n const p = layer.params;\n const weights = [p.reds, p.yellows, p.greens, p.cyans, p.blues, p.magentas];\n const tint = p.tint ? hexToRgb(p.tint) : null;\n return (c) => {\n const g = blackWhiteGray(c, weights);\n if (tint) {\n const t = setLum(tint, g);\n c[0] = clamp01(t[0]);\n c[1] = clamp01(t[1]);\n c[2] = clamp01(t[2]);\n } else {\n c[0] = c[1] = c[2] = g;\n }\n };\n }\n case 'photoFilter': {\n const p = layer.params;\n if (p.density <= 0) return null;\n const m = photoFilterMatrix(hexToRgb(p.color), p.density / 100, false);\n const preserve = p.preserveLuminosity;\n return (c) => {\n const l = [srgbToLinear(Math.max(0, c[0])), srgbToLinear(Math.max(0, c[1])), srgbToLinear(Math.max(0, c[2]))];\n let f: Rgb = [0, 1, 2].map((r) => linearToSrgb(Math.max(0, m[r][0] * l[0] + m[r][1] * l[1] + m[r][2] * l[2]))) as Rgb;\n if (preserve) {\n const target = 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2];\n f = setLum(f, target);\n }\n c[0] = clamp01(f[0]);\n c[1] = clamp01(f[1]);\n c[2] = clamp01(f[2]);\n };\n }\n case 'invert':\n return (c) => {\n c[0] = 1 - c[0];\n c[1] = 1 - c[1];\n c[2] = 1 - c[2];\n };\n case 'posterize': {\n const levels = layer.params.levels;\n return (c) => {\n c[0] = posterizeValue(c[0], levels);\n c[1] = posterizeValue(c[1], levels);\n c[2] = posterizeValue(c[2], levels);\n };\n }\n case 'threshold': {\n const t = Math.round(layer.params.level);\n return (c) => {\n const v = Math.round(rgbToGray(c) * 255) >= t ? 1 : 0;\n c[0] = c[1] = c[2] = v;\n };\n }\n }\n return null;\n}\n\n// Compiled layers keyed by kind + params, so a stack is compiled once per parameter change.\nconst COMPILE_CACHE_LIMIT = 64;\nconst compileCache = new Map<string, PixelOp | null>();\n\nfunction compiledFor(layer: AdjustmentLayer): PixelOp | null {\n const key = `${layer.kind}:${JSON.stringify(layer.params)}`;\n if (compileCache.has(key)) {\n const op = compileCache.get(key)!;\n // Refresh recency.\n compileCache.delete(key);\n compileCache.set(key, op);\n return op;\n }\n const op = compileAdjustmentLayer(layer);\n compileCache.set(key, op);\n if (compileCache.size > COMPILE_CACHE_LIMIT) compileCache.delete(compileCache.keys().next().value as string);\n return op;\n}\n\n/** Layers that change pixels: enabled, visible, not the identity. */\nexport function activeAdjustmentLayers(layers: readonly AdjustmentLayer[] | undefined): AdjustmentLayer[] {\n if (!layers?.length) return [];\n return layers.filter((l) => l.enabled !== false && (l.opacity ?? 1) > 0 && compiledFor(l) !== null);\n}\n\n/**\n * Evaluates one colour (encoded sRGB, 0..1) through a layer stack: each layer adjusts the result\n * of the layers below and is blended over it with its blend mode and opacity (an opaque\n * backdrop, so `(1 − α)·Cb + α·B(Cb, Cs)`).\n */\nexport function evaluateAdjustmentLayers(color: readonly number[], layers: readonly AdjustmentLayer[]): Rgb {\n const c: Rgb = [color[0], color[1], color[2]];\n for (const layer of layers) applyLayer(c, layer, compiledFor(layer));\n return c;\n}\n\nfunction applyLayer(c: Rgb, layer: AdjustmentLayer, op: PixelOp | null, coverage = 1): void {\n if (!op || layer.enabled === false) return;\n const alpha = Math.min(1, Math.max(0, layer.opacity ?? 1)) * coverage;\n if (alpha <= 0) return;\n const blend = layer.blend ?? 'normal';\n if (blend === 'normal' && alpha >= 1) {\n op(c);\n return;\n }\n const cb0 = c[0], cb1 = c[1], cb2 = c[2];\n op(c);\n if (blend === 'pigment') {\n // Mixes the adjusted colour into the original like paint, `alpha` of the way.\n mixPigmentRgb([clamp01(cb0), clamp01(cb1), clamp01(cb2)], [clamp01(c[0]), clamp01(c[1]), clamp01(c[2])], alpha, c);\n return;\n }\n const b = blend === 'normal' ? c : blendRgb(blend, [cb0, cb1, cb2], c);\n c[0] = (1 - alpha) * cb0 + alpha * b[0];\n c[1] = (1 - alpha) * cb1 + alpha * b[1];\n c[2] = (1 - alpha) * cb2 + alpha * b[2];\n}\n\n/**\n * Applies an adjustment-layer stack to 8-bit straight RGBA pixels in place. Each pixel is\n * decoded once, run through every layer in float, and encoded once. Transparent pixels are\n * left alone; alpha is never changed.\n *\n * `masks` (layer id → 0..1 coverage per pixel of `imageData`, see `buildLayerMaskPlanes`)\n * scales each masked layer's opacity per pixel: coverage 0 leaves the pixel as the layers below\n * made it, 1 applies the layer fully.\n */\nexport function applyAdjustmentLayers(\n imageData: { data: Uint8ClampedArray },\n layers: readonly AdjustmentLayer[] | undefined,\n masks?: ReadonlyMap<string, ArrayLike<number>>\n): void {\n const active = activeAdjustmentLayers(layers);\n if (!active.length) return;\n const ops = active.map((l) => compiledFor(l));\n const planes = active.map((l) => masks?.get(l.id));\n const data = imageData.data;\n const len = data.length;\n const c: Rgb = [0, 0, 0];\n for (let i = 0; i < len; i += 4) {\n if (data[i + 3] === 0) continue;\n c[0] = data[i] / 255;\n c[1] = data[i + 1] / 255;\n c[2] = data[i + 2] / 255;\n for (let k = 0; k < active.length; k++) {\n const plane = planes[k];\n if (plane) {\n const cov = plane[i >> 2];\n if (cov > 0) applyLayer(c, active[k], ops[k], cov >= 1 ? 1 : cov);\n } else {\n applyLayer(c, active[k], ops[k]);\n }\n }\n data[i] = Math.round(clamp01(c[0]) * 255);\n data[i + 1] = Math.round(clamp01(c[1]) * 255);\n data[i + 2] = Math.round(clamp01(c[2]) * 255);\n }\n}\n\n/** Stable cache key of a layer stack's pixel effect (for renderer caches). */\nexport function adjustmentLayersKey(layers: readonly AdjustmentLayer[] | undefined): string {\n if (!layers?.length) return '';\n return JSON.stringify(\n layers.map((l) => {\n const key: unknown[] = [l.kind, l.enabled !== false, l.opacity ?? 1, l.blend ?? 'normal', l.params];\n if (l.mask) key.push(layerMaskKey(l.mask));\n return key;\n })\n );\n}\n","import type { DepthMaskOptions, ImageAdjustments } from './types';\nimport {\n LUMA_709,\n LATEST_PROCESS_VERSION,\n TONE_LUT_SIZE,\n buildToneLut,\n linearToSrgb,\n normalizeProcessVersion,\n srgbToLinear,\n whiteBalanceMatrix,\n} from './color';\nimport { applyAdjustmentLayers } from './adjustment-layers';\n\nfunction clamp(value: number, min = 0, max = 255): number {\n return Math.max(min, Math.min(max, value));\n}\n\n/**\n * Applies color and tone adjustments (brightness, contrast, saturation, exposure, temperature, tint,\n * opacity, then any adjustment `layers`, then blur) directly to the pixel data of an ImageData buffer.\n *\n * `adjustments.process` selects the rendering: `2` is the linear-light pipeline\n * ({@link applyProcess2Adjustments}); anything else (including a missing value, as in edits saved\n * before process versions existed) is the frozen legacy rendering ({@link applyLegacyAdjustments}).\n */\nexport function applyAdjustments(\n imageData: ImageData,\n adjustments: ImageAdjustments,\n options: { layerMasks?: ReadonlyMap<string, ArrayLike<number>> } = {}\n): void {\n if (normalizeProcessVersion(adjustments.process) === LATEST_PROCESS_VERSION) {\n applyProcess2Adjustments(imageData, adjustments);\n } else {\n applyLegacyAdjustments(imageData, adjustments);\n }\n\n // Adjustment layers, bottom to top, on the encoded result (no-op without layers).\n // Layer masks (see `buildLayerMaskPlanes`) are aligned with `imageData`.\n applyAdjustmentLayers(imageData, adjustments.layers, options.layerMasks);\n\n // Apply blur if requested\n if (adjustments.blur > 0) {\n applyFastBoxBlur(imageData, Math.min(Math.round(adjustments.blur), 40));\n }\n}\n\n/**\n * Process 1: the original 8-bit gamma-space rendering, clamping after every step. Frozen so\n * that edits saved without a `process` look exactly as they always have. `tint` is ignored.\n */\nexport function applyLegacyAdjustments(imageData: { data: Uint8ClampedArray }, adjustments: ImageAdjustments): void {\n const { brightness, contrast, saturation, exposure, temperature, opacity } = adjustments;\n const data = imageData.data;\n const len = data.length;\n\n const hasBrightness = brightness !== 0;\n const hasContrast = contrast !== 0;\n const hasSaturation = saturation !== 0;\n const hasExposure = exposure !== 0;\n const hasTemperature = temperature !== 0;\n const hasOpacity = opacity < 1;\n\n // Pre-calculate contrast factor\n const contrastFactor = hasContrast\n ? (259 * (contrast + 255)) / (255 * (259 - contrast))\n : 1;\n\n // Pre-calculate exposure multiplier\n const exposureMultiplier = hasExposure ? Math.pow(2, exposure / 50) : 1;\n\n // Pre-calculate saturation multiplier\n const satMultiplier = hasSaturation ? 1 + saturation / 100 : 1;\n\n // Pre-calculate temperature offsets\n const tempOffset = hasTemperature ? temperature * 0.8 : 0;\n\n if (hasBrightness || hasContrast || hasSaturation || hasExposure || hasTemperature || hasOpacity) {\n for (let i = 0; i < len; i += 4) {\n let r = data[i];\n let g = data[i + 1];\n let b = data[i + 2];\n let a = data[i + 3];\n\n // Exposure\n if (hasExposure) {\n r = clamp(r * exposureMultiplier);\n g = clamp(g * exposureMultiplier);\n b = clamp(b * exposureMultiplier);\n }\n\n // Brightness\n if (hasBrightness) {\n const bOffset = brightness * 2.55;\n r = clamp(r + bOffset);\n g = clamp(g + bOffset);\n b = clamp(b + bOffset);\n }\n\n // Contrast\n if (hasContrast) {\n r = clamp(contrastFactor * (r - 128) + 128);\n g = clamp(contrastFactor * (g - 128) + 128);\n b = clamp(contrastFactor * (b - 128) + 128);\n }\n\n // Saturation\n if (hasSaturation) {\n const luminance = 0.2989 * r + 0.587 * g + 0.114 * b;\n r = clamp(luminance + (r - luminance) * satMultiplier);\n g = clamp(luminance + (g - luminance) * satMultiplier);\n b = clamp(luminance + (b - luminance) * satMultiplier);\n }\n\n // Temperature / Warmth\n if (hasTemperature) {\n r = clamp(r + tempOffset);\n b = clamp(b - tempOffset);\n }\n\n // Opacity\n if (hasOpacity) {\n a = clamp(a * opacity);\n }\n\n data[i] = r;\n data[i + 1] = g;\n data[i + 2] = b;\n data[i + 3] = a;\n }\n }\n}\n\n/** sRGB byte → linear light (exact; shared by every process 2 render). */\nconst DECODE_LUT: Float32Array = (() => {\n const lut = new Float32Array(256);\n for (let i = 0; i < 256; i++) lut[i] = srgbToLinear(i / 255);\n return lut;\n})();\n\n/** Intervals of the linear → sRGB byte encode table over [0, 1]. */\nconst ENCODE_LUT_SIZE = 65535;\nlet encodeLut: Uint8Array | null = null;\nfunction getEncodeLut(): Uint8Array {\n if (!encodeLut) {\n encodeLut = new Uint8Array(ENCODE_LUT_SIZE + 1);\n for (let i = 0; i <= ENCODE_LUT_SIZE; i++) {\n encodeLut[i] = Math.round(linearToSrgb(i / ENCODE_LUT_SIZE) * 255);\n }\n }\n return encodeLut;\n}\n\ninterface Process2Plan {\n key: string;\n /** Exposure gain folded into the white-balance matrix (row-major). */\n m: Float64Array;\n /** Per-channel tone LUTs over [0, headroom[c]]. */\n luts: [Float32Array, Float32Array, Float32Array];\n /** LUT index scale per channel: TONE_LUT_SIZE / headroom[c]. */\n scale: [number, number, number];\n saturation: number;\n}\n\nlet cachedPlan: Process2Plan | null = null;\n\n/**\n * Precomputes everything per parameter change: the 3×3 colour matrix (exposure gain × white\n * balance) and one tone LUT per channel whose domain is that channel's largest possible value,\n * so the highlight shoulder absorbs it instead of clipping.\n */\nfunction getProcess2Plan(adjustments: ImageAdjustments): Process2Plan {\n const exposure = adjustments.exposure || 0;\n const temperature = adjustments.temperature || 0;\n const tint = adjustments.tint || 0;\n const contrast = adjustments.contrast || 0;\n const brightness = adjustments.brightness || 0;\n const saturation = 1 + (adjustments.saturation || 0) / 100;\n const key = `${exposure},${temperature},${tint},${contrast},${brightness},${saturation}`;\n if (cachedPlan && cachedPlan.key === key) return cachedPlan;\n\n const gain = Math.pow(2, exposure / 50);\n const wb = whiteBalanceMatrix(temperature, tint);\n const m = new Float64Array(9);\n for (let i = 0; i < 9; i++) m[i] = wb[i] * gain;\n const luts = [] as unknown as [Float32Array, Float32Array, Float32Array];\n const scale = [0, 0, 0] as [number, number, number];\n for (let c = 0; c < 3; c++) {\n // Largest value this channel can reach from inputs in [0, 1].\n let reach = 0;\n for (let k = 0; k < 3; k++) reach += Math.max(0, m[c * 3 + k]);\n const headroom = Math.max(1, reach);\n luts[c] = buildToneLut(contrast, brightness, headroom);\n scale[c] = TONE_LUT_SIZE / headroom;\n }\n cachedPlan = { key, m, luts, scale, saturation };\n return cachedPlan;\n}\n\n/**\n * Process 2: decode sRGB to linear float once; apply exposure (gain) and white balance (a\n * Bradford matrix relative to as-shot) in linear light; a per-channel tone LUT for the\n * highlight shoulder, contrast and brightness; luminance-preserving saturation in linear light;\n * then encode back to sRGB through a LUT. Nothing is clamped between stages, only at the final\n * encode. Neutral settings are the identity.\n */\nexport function applyProcess2Adjustments(imageData: { data: Uint8ClampedArray }, adjustments: ImageAdjustments): void {\n const data = imageData.data;\n const len = data.length;\n const opacity = adjustments.opacity ?? 1;\n const hasOpacity = opacity < 1;\n const hasColor =\n !!adjustments.brightness ||\n !!adjustments.contrast ||\n !!adjustments.saturation ||\n !!adjustments.exposure ||\n !!adjustments.temperature ||\n !!adjustments.tint;\n\n if (!hasColor) {\n if (hasOpacity) {\n for (let i = 3; i < len; i += 4) data[i] = clamp(data[i] * opacity);\n }\n return;\n }\n\n const plan = getProcess2Plan(adjustments);\n const dec = DECODE_LUT;\n const enc = getEncodeLut();\n const { m, luts, scale, saturation } = plan;\n const m0 = m[0], m1 = m[1], m2 = m[2], m3 = m[3], m4 = m[4], m5 = m[5], m6 = m[6], m7 = m[7], m8 = m[8];\n const lutR = luts[0], lutG = luts[1], lutB = luts[2];\n const sR = scale[0], sG = scale[1], sB = scale[2];\n const N = TONE_LUT_SIZE;\n const hasSaturation = saturation !== 1;\n const wr = LUMA_709[0], wg = LUMA_709[1], wb = LUMA_709[2];\n\n for (let i = 0; i < len; i += 4) {\n const r0 = dec[data[i]];\n const g0 = dec[data[i + 1]];\n const b0 = dec[data[i + 2]];\n\n // Exposure × white balance (linear light, unclamped).\n let r = m0 * r0 + m1 * g0 + m2 * b0;\n let g = m3 * r0 + m4 * g0 + m5 * b0;\n let b = m6 * r0 + m7 * g0 + m8 * b0;\n\n // Tone LUTs (linear interpolation).\n let f: number;\n let k: number;\n if (r <= 0) r = lutR[0];\n else if ((f = r * sR) >= N) r = lutR[N];\n else { k = f | 0; r = lutR[k] + (lutR[k + 1] - lutR[k]) * (f - k); }\n if (g <= 0) g = lutG[0];\n else if ((f = g * sG) >= N) g = lutG[N];\n else { k = f | 0; g = lutG[k] + (lutG[k + 1] - lutG[k]) * (f - k); }\n if (b <= 0) b = lutB[0];\n else if ((f = b * sB) >= N) b = lutB[N];\n else { k = f | 0; b = lutB[k] + (lutB[k + 1] - lutB[k]) * (f - k); }\n\n // Luminance-preserving saturation in linear light.\n if (hasSaturation) {\n const y = wr * r + wg * g + wb * b;\n r = y + (r - y) * saturation;\n g = y + (g - y) * saturation;\n b = y + (b - y) * saturation;\n }\n\n // Encode (the only clamp).\n data[i] = enc[r <= 0 ? 0 : r >= 1 ? ENCODE_LUT_SIZE : (r * ENCODE_LUT_SIZE + 0.5) | 0];\n data[i + 1] = enc[g <= 0 ? 0 : g >= 1 ? ENCODE_LUT_SIZE : (g * ENCODE_LUT_SIZE + 0.5) | 0];\n data[i + 2] = enc[b <= 0 ? 0 : b >= 1 ? ENCODE_LUT_SIZE : (b * ENCODE_LUT_SIZE + 0.5) | 0];\n if (hasOpacity) data[i + 3] = clamp(data[i + 3] * opacity);\n }\n}\n\n/**\n * Fast separable box blur running horizontally and vertically (`adjustments.blur` and the `blur`\n * layer filter): `radius` pixels each side, edges clamped, rounded after each pass.\n */\nexport function applyBoxBlur(imageData: { width: number; height: number; data: Uint8ClampedArray }, radius: number): void {\n applyFastBoxBlur(imageData, radius);\n}\n\nfunction applyFastBoxBlur(imageData: { width: number; height: number; data: Uint8ClampedArray }, radius: number): void {\n if (radius < 1) return;\n const w = imageData.width;\n const h = imageData.height;\n const src = imageData.data;\n const temp = new Uint8ClampedArray(src.length);\n\n // Horizontal blur pass\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n let r = 0, g = 0, b = 0, a = 0;\n let count = 0;\n for (let k = -radius; k <= radius; k++) {\n const px = Math.min(w - 1, Math.max(0, x + k));\n const idx = (y * w + px) * 4;\n r += src[idx];\n g += src[idx + 1];\n b += src[idx + 2];\n a += src[idx + 3];\n count++;\n }\n const dstIdx = (y * w + x) * 4;\n temp[dstIdx] = Math.round(r / count);\n temp[dstIdx + 1] = Math.round(g / count);\n temp[dstIdx + 2] = Math.round(b / count);\n temp[dstIdx + 3] = Math.round(a / count);\n }\n }\n\n // Vertical blur pass back into src\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) {\n let r = 0, g = 0, b = 0, a = 0;\n let count = 0;\n for (let k = -radius; k <= radius; k++) {\n const py = Math.min(h - 1, Math.max(0, y + k));\n const idx = (py * w + x) * 4;\n r += temp[idx];\n g += temp[idx + 1];\n b += temp[idx + 2];\n a += temp[idx + 3];\n count++;\n }\n const dstIdx = (y * w + x) * 4;\n src[dstIdx] = Math.round(r / count);\n src[dstIdx + 1] = Math.round(g / count);\n src[dstIdx + 2] = Math.round(b / count);\n src[dstIdx + 3] = Math.round(a / count);\n }\n }\n}\n\n/**\n * Blends a depth/alpha mask into the main image data based on Gleamforge depth masking.\n * Supports threshold range with softness gradation, brightness mode, and optional inversion.\n */\nexport function applyDepthMask(\n mainImageData: ImageData,\n maskImageData: ImageData,\n options: DepthMaskOptions\n): void {\n const { mode, depthRange, softness, invert } = options;\n const mainData = mainImageData.data;\n const maskData = maskImageData.data;\n const len = Math.min(mainData.length, maskData.length);\n const effectiveSoftness = Math.max(1, softness || 10);\n const minDepth = depthRange[0];\n const maxDepth = depthRange[1];\n\n for (let i = 0; i < len; i += 4) {\n let maskValue = maskData[i]; // Use red channel of mask (or grayscale)\n if (invert) {\n maskValue = 255 - maskValue;\n }\n\n let alphaValue: number;\n\n if (mode === 'threshold') {\n if (maskValue >= maxDepth + effectiveSoftness) {\n alphaValue = 0;\n } else if (maskValue > maxDepth) {\n alphaValue = Math.round(((maxDepth + effectiveSoftness - maskValue) / effectiveSoftness) * 255);\n } else if (maskValue >= minDepth) {\n alphaValue = 255;\n } else if (maskValue < minDepth - effectiveSoftness) {\n alphaValue = 0;\n } else {\n alphaValue = Math.round(((maskValue - (minDepth - effectiveSoftness)) / effectiveSoftness) * 255);\n }\n } else {\n // Brightness mode\n alphaValue = clamp(maskValue + minDepth);\n }\n\n // Multiply existing alpha with mask alpha\n mainData[i + 3] = clamp((mainData[i + 3] * alphaValue) / 255);\n }\n}\n\n/**\n * Removes alpha channel by setting all alpha bytes to 255 (completely opaque).\n * From Gleamforge image-filters.ts.\n */\nexport function removeAlpha(imageData: ImageData): void {\n const data = imageData.data;\n const len = data.length;\n for (let i = 3; i < len; i += 4) {\n data[i] = 255;\n }\n}\n","// Portions ported from photocraft-main/crates/algo/src/histogram.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Display bins of a histogram. */\nexport const HISTOGRAM_BINS = 256;\n\nexport interface ImageHistogram {\n /** 256-bin counts per channel. */\n red: Uint32Array;\n green: Uint32Array;\n blue: Uint32Array;\n /** 256-bin counts of Rec.709 luma `round(0.2126 R + 0.7152 G + 0.0722 B)` of the encoded values. */\n luminance: Uint32Array;\n /** Visible pixels counted (alpha > 0). Each counts once, partially transparent ones included. */\n samples: number;\n /** Fully transparent pixels, excluded from every bin. */\n transparent: number;\n /** Per channel (R, G, B): visible pixels at exactly 0. */\n shadows: [number, number, number];\n /** Per channel (R, G, B): visible pixels at exactly 255. */\n highlights: [number, number, number];\n /** Largest bin count over R, G, B and luminance (for scaling a plot). */\n max: number;\n}\n\ninterface PixelSource {\n data: ArrayLike<number>;\n width?: number;\n height?: number;\n}\n\n/**\n * Per-channel and luminance histograms of straight 8-bit RGBA pixels (an `ImageData` or anything\n * with `data`), with clipping counts at the exact endpoints. Transparent pixels cannot add\n * phantom peaks: they are counted separately and excluded.\n */\nexport function computeHistogram(imageData: PixelSource): ImageHistogram {\n const red = new Uint32Array(HISTOGRAM_BINS);\n const green = new Uint32Array(HISTOGRAM_BINS);\n const blue = new Uint32Array(HISTOGRAM_BINS);\n const luminance = new Uint32Array(HISTOGRAM_BINS);\n const shadows: [number, number, number] = [0, 0, 0];\n const highlights: [number, number, number] = [0, 0, 0];\n let samples = 0;\n let transparent = 0;\n const data = imageData.data;\n const len = data.length - (data.length % 4);\n for (let i = 0; i < len; i += 4) {\n if (!(data[i + 3] > 0)) {\n transparent++;\n continue;\n }\n samples++;\n const r = clampByte(data[i]);\n const g = clampByte(data[i + 1]);\n const b = clampByte(data[i + 2]);\n red[r]++;\n green[g]++;\n blue[b]++;\n luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) shadows[0]++;\n if (g === 0) shadows[1]++;\n if (b === 0) shadows[2]++;\n if (r === 255) highlights[0]++;\n if (g === 255) highlights[1]++;\n if (b === 255) highlights[2]++;\n }\n let max = 0;\n for (const bins of [red, green, blue, luminance]) {\n for (let k = 0; k < HISTOGRAM_BINS; k++) if (bins[k] > max) max = bins[k];\n }\n return { red, green, blue, luminance, samples, transparent, shadows, highlights, max };\n}\n\nfunction clampByte(v: number): number {\n return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);\n}\n\nexport type HistogramChannel = 'red' | 'green' | 'blue' | 'luminance';\n\n/**\n * An SVG path (`M … Z`) of one channel scaled into a `width` × `height` box, bins left to right,\n * counts up from the bottom. `scale: 'sqrt'` (default) keeps small populations visible.\n */\nexport function histogramPath(\n histogram: ImageHistogram,\n channel: HistogramChannel,\n width: number,\n height: number,\n options: { scale?: 'linear' | 'sqrt'; max?: number } = {}\n): string {\n const bins = histogram[channel];\n const sqrt = (options.scale ?? 'sqrt') === 'sqrt';\n const f = (v: number) => (sqrt ? Math.sqrt(v) : v);\n const top = f(options.max ?? histogram.max) || 1;\n const dx = width / (HISTOGRAM_BINS - 1);\n let d = `M0 ${height}`;\n for (let k = 0; k < HISTOGRAM_BINS; k++) {\n const y = height - (f(bins[k]) / top) * height;\n d += ` L${round2(k * dx)} ${round2(y)}`;\n }\n return `${d} L${round2(width)} ${height} Z`;\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { SelectionMask } from './mask';\n\n/**\n * The selection's outline at `threshold` (0..255, default 128) as axis-aligned segments on pixel\n * boundaries, `[x0, y0, x1, y1, …]` in natural image pixels, with collinear neighbours merged.\n * This is what marching ants are drawn from. Linear in the pixel count.\n */\nexport function selectionOutline(mask: SelectionMask, threshold = 128): Float32Array {\n const { width: w, height: h, data } = mask;\n const segs: number[] = [];\n if (!mask.bounds) return new Float32Array(0);\n const b = mask.bounds;\n const inside = (x: number, y: number) => x >= 0 && y >= 0 && x < w && y < h && data[y * w + x] >= threshold;\n // Horizontal boundaries: the line y separates rows y − 1 and y.\n for (let y = b.y; y <= b.y + b.height; y++) {\n let start = -1;\n for (let x = b.x; x <= b.x + b.width; x++) {\n const edge = x < b.x + b.width && inside(x, y - 1) !== inside(x, y);\n if (edge && start < 0) start = x;\n else if (!edge && start >= 0) {\n segs.push(start, y, x, y);\n start = -1;\n }\n }\n }\n // Vertical boundaries: the line x separates columns x − 1 and x.\n for (let x = b.x; x <= b.x + b.width; x++) {\n let start = -1;\n for (let y = b.y; y <= b.y + b.height; y++) {\n const edge = y < b.y + b.height && inside(x - 1, y) !== inside(x, y);\n if (edge && start < 0) start = y;\n else if (!edge && start >= 0) {\n segs.push(x, start, x, y);\n start = -1;\n }\n }\n }\n return Float32Array.from(segs);\n}\n\nexport interface QuickMaskStyle {\n /** Overlay colour, `[r, g, b]` 0..255 (default Photoshop's rubylith red). */\n color?: readonly [number, number, number];\n /** 0..1 (default 0.5): opacity over fully unselected pixels. */\n opacity?: number;\n}\n\n/**\n * Quick Mask view: RGBA pixels (natural image size) tinting the *unselected* area, so the\n * selection shows through clear, as Photoshop's rubylith overlay.\n */\nexport function quickMaskPixels(mask: SelectionMask, style: QuickMaskStyle = {}): Uint8ClampedArray {\n const [r, g, b] = style.color ?? [255, 0, 0];\n const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n out[p] = r;\n out[p + 1] = g;\n out[p + 2] = b;\n out[p + 3] = Math.round((255 - mask.data[i]) * k);\n }\n return out;\n}\n","/**\n * The image editor's layer stack: pixel, adjustment, group and annotation layers in one ordered\n * tree (bottom to top), all in document space (natural pixels of the document).\n *\n * Layer objects are immutable. Every change builds new objects along the path from the root to\n * the changed layer and shares everything else, so undo history, the compositor's caches and\n * serialization can compare layers by identity.\n */\n\nimport type {\n AdjustmentKind,\n AdjustmentParamsMap,\n Annotation,\n CropBox,\n DepthMaskOptions,\n} from '../types';\nimport type { PixelSurface } from '../../raster/surface';\nimport type { BlendMode } from '../../raster/blend';\nimport type { Interpolation } from '../../raster/transform';\nimport type { GlobalLight } from '../../raster/effects/model';\nimport type { EffectPattern } from '../../raster/effects/patterns';\nimport type { Quad, WarpMesh } from '../../geom/types';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isLayerLocked,\n mapLayer,\n type GroupBlendMode,\n type LayerLocation as RasterLayerLocation,\n type LayerLocks,\n type RasterLayerBase,\n type RasterMask as BaseRasterMask,\n type RasterPixelLayer,\n} from '../../raster/layers';\n\n/** Blend modes of a layer; groups also accept `pass-through`. */\nexport type LayerBlendMode = GroupBlendMode;\n\nexport type { LayerLocks };\n\n/**\n * A layer mask (see the raster module's `RasterMask`) whose parametric `source` is a depth map:\n * a depth mask is generated from it instead of painted, and its surface is unused.\n */\nexport type RasterMask = BaseRasterMask<DepthMaskOptions>;\n\nexport interface LayerBase extends RasterLayerBase {\n mask?: RasterMask;\n}\n\n/** The base sliders of the old document-level adjustments, as a filter on one layer. */\nexport interface DevelopSettings {\n brightness: number;\n contrast: number;\n saturation: number;\n exposure: number;\n temperature: number;\n tint?: number;\n /** Rendering process (see `ImageAdjustments.process`). */\n process?: 1 | 2;\n}\n\nexport type DevelopFilter = { id: string; kind: 'develop'; enabled: boolean } & DevelopSettings;\n/** Box blur of `radius` pixels (the old `adjustments.blur`), clamped at the canvas edges. */\nexport interface BlurFilter {\n id: string;\n kind: 'blur';\n enabled: boolean;\n radius: number;\n}\nexport type LayerFilter = DevelopFilter | BlurFilter;\nexport type LayerFilterKind = LayerFilter['kind'];\n\nexport interface PixelLayer extends LayerBase, Omit<RasterPixelLayer, keyof RasterLayerBase> {\n /** Live filters, applied bottom (first) to top before the mask and blend. */\n filters?: LayerFilter[];\n /**\n * `'document'`: the pixels are the document's source image, unmodified (the Background of an\n * opened image). Such a layer's `surface` is a placeholder; read its pixels through the\n * controller. Painting it drops the flag.\n */\n source?: 'document';\n}\n\nexport type AdjustmentLayerNode<K extends AdjustmentKind = AdjustmentKind> = {\n [P in K]: LayerBase & { type: 'adjustment'; kind: P; params: AdjustmentParamsMap[P] };\n}[K];\n\nexport interface GroupLayer extends Omit<LayerBase, 'blend'> {\n type: 'group';\n blend: LayerBlendMode;\n /** Bottom to top. */\n children: ImageLayer[];\n /** UI only. */\n collapsed?: boolean;\n}\n\n/** One annotation (pen, shape, arrow, text, image) as a layer, drawn by the annotation renderer. */\nexport interface AnnotationLayer extends LayerBase {\n type: 'annotation';\n annotation: Annotation;\n}\n\n// ---------------------------------------------------------------------------\n// Smart objects\n// ---------------------------------------------------------------------------\n\n/** A nested layer document: the contents of a smart object made from layers (or an embedded PSD). */\nexport interface SmartDocument {\n /** Bottom to top, on a canvas of the contents' `width × height`. */\n layers: ImageLayer[];\n globalLight?: GlobalLight;\n patterns?: EffectPattern[];\n}\n\n/**\n * What a smart object places: raster pixels or a nested layer document, at their own pixel size.\n * Immutable and shared: every instance placed from the same contents (duplicates, linked\n * instances) holds the same `id`, and replacing or editing the contents updates them all.\n */\nexport interface SmartContents {\n /** Shared by linked instances; independent copies get a fresh one. */\n id: string;\n /** File name shown in the UI and written to PSD (`Layer 1.psb`, `photo.png`). */\n name: string;\n /** Pixel size of the contents (the placement maps `0..width × 0..height` into the document). */\n width: number;\n height: number;\n /** `raster`: `raster` holds the pixels at (0, 0); `document`: `document` holds the layers. */\n kind: 'raster' | 'document';\n raster?: PixelSurface;\n document?: SmartDocument;\n /**\n * The file the contents came from (a PSD's embedded file), written back by Export Contents and\n * PSD export while the contents are unedited. Without `raster` / `document` the file could not\n * be decoded here (a JPEG outside a browser): the layer keeps its rendered pixels, and changes\n * that re-render it are refused until `prepareSmartObjects` decodes it.\n */\n file?: { bytes: Uint8Array; type: string };\n /** Linked to an external file (Photoshop `liFE`): only the embedded preview is held. */\n linked?: { path: string };\n}\n\n/**\n * Where a smart object's contents land, non-destructively: a warp in the contents' own pixels\n * (optional) and then the map of the contents' rectangle onto `corners`. Every change re-renders\n * from the contents, so repeated transforms never degrade.\n */\nexport interface SmartPlacement {\n /** Where the contents' corners land (top-left, top-right, bottom-right, bottom-left), document pixels. */\n corners: Quad;\n /** A Bézier warp over the contents' rectangle, control points in contents pixels (missing = none). */\n warp?: WarpMesh | null;\n /** Resampling (default bicubic). */\n interpolation?: Interpolation;\n}\n\n/**\n * A Photoshop smart filter we do not model (Gaussian Blur, Camera Raw, …), kept from a PSD so it\n * is listed, can be hidden, reordered or deleted, and is written back unchanged. It draws nothing.\n */\nexport interface PsdSmartFilter {\n id: string;\n kind: 'psd';\n enabled: boolean;\n /** Photoshop's menu name (`Gaussian Blur...`). */\n name: string;\n /** The filter's `filterFX` item descriptor (versioned descriptor bytes), as read. */\n data: Uint8Array;\n}\n\n/** One smart filter: a layer filter (or a kept PSD filter) with its own blending over the result below it. */\nexport type SmartFilter = (LayerFilter | PsdSmartFilter) & {\n /** How the filter's result blends with the result below it (Photoshop's Blending Options). */\n blend: BlendMode;\n /** 0..1 */\n opacity: number;\n};\nexport type SmartFilterKind = SmartFilter['kind'];\n\n/** The smart filter stack of a smart object. */\nexport interface SmartFilterStack {\n /** Layer › Smart Filter › Disable Smart Filters (each filter keeps its own `enabled`). */\n enabled: boolean;\n /** Bottom (applied first) to top. */\n filters: SmartFilter[];\n /**\n * The one filter mask (document space): where the filtered result shows over the unfiltered\n * contents. Missing reveals everywhere.\n */\n mask?: RasterMask;\n}\n\n/** PSD data of a smart object kept for round trips (see the PSD module). */\nexport interface SmartPsdData {\n /** The placed-layer block as read (`SoLd`, `SoLE` or `PlLd`) and its key. */\n key: string;\n data: Uint8Array;\n /** The instance id (`placed`), which keys the filter-effects cache. */\n placed?: string;\n}\n\n/**\n * A smart object: `contents` placed through `placement`, then the smart filters. `surface` is the\n * rendered result in document space (offset 0), rebuilt by every change to the contents,\n * placement or filters; the compositor draws it like a pixel layer's pixels, and masks, effects,\n * clipping and blend apply on top. Never paint into it: rasterize the layer first.\n */\nexport interface SmartObjectLayer extends LayerBase {\n type: 'smart';\n contents: SmartContents;\n placement: SmartPlacement;\n smartFilters?: SmartFilterStack;\n /** The rendered pixels (a cache of contents → placement → filters), document space. */\n surface: PixelSurface;\n /** Always (0, 0): a smart object moves through its placement. */\n offset: { x: number; y: number };\n /** 0..1, PSD \"Fill\" (missing = 1). */\n fillOpacity?: number;\n psd?: SmartPsdData;\n}\n\nexport type ImageLayer = PixelLayer | AdjustmentLayerNode | GroupLayer | AnnotationLayer | SmartObjectLayer;\nexport type ImageLayerType = ImageLayer['type'];\n\n/** Which part of the active layer painting targets (`filter-mask`: a smart object's filter mask). */\nexport type LayerTarget = 'pixels' | 'mask' | 'filter-mask';\n\n// ---------------------------------------------------------------------------\n// Ids\n// ---------------------------------------------------------------------------\n\nlet idCounter = 0;\n\n/** A fresh id with `prefix` (`layer`, `flt`, …). */\nexport function newLayerId(prefix = 'layer'): string {\n return `${prefix}_${Date.now().toString(36)}_${(++idCounter).toString(36)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Tree helpers (pure)\n// ---------------------------------------------------------------------------\n\nexport type LayerLocation = RasterLayerLocation<ImageLayer>;\n\nexport { flattenLayers, findLayer, mapLayer, insertLayer, isLayerLocked };\n\n/** The top-level and nested pixel layers. */\nexport function pixelLayers(layers: readonly ImageLayer[]): PixelLayer[] {\n return flattenLayers(layers).filter((l): l is PixelLayer => l.type === 'pixel');\n}\n\n/** The develop filter of a pixel layer, if any. */\nexport function developFilter(layer: PixelLayer): DevelopFilter | undefined {\n return layer.filters?.find((f): f is DevelopFilter => f.kind === 'develop');\n}\n\nexport function blurFilter(layer: PixelLayer): BlurFilter | undefined {\n return layer.filters?.find((f): f is BlurFilter => f.kind === 'blur');\n}\n\n/** True for filters that change pixels (enabled, non-neutral). */\nexport function isActiveFilter(f: LayerFilter): boolean {\n if (!f.enabled) return false;\n if (f.kind === 'blur') return Math.min(Math.round(f.radius), 40) >= 1;\n return Boolean(f.brightness || f.contrast || f.saturation || f.exposure || f.temperature || f.tint);\n}\n\n/** True when a filter needs neighbouring pixels (it can't run tile by tile). */\nexport function isSpatialFilter(f: LayerFilter): boolean {\n return f.kind === 'blur';\n}\n\n/** Document-space bounds of a pixel layer's stored tiles, or `null`. */\nexport function pixelLayerBounds(layer: PixelLayer, surface: PixelSurface = layer.surface): CropBox | null {\n const b = surface.bounds;\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height };\n}\n\n/** Human label of a layer kind. */\nexport function layerTypeLabel(type: ImageLayerType): string {\n return type === 'pixel' ? 'Pixel layer' : type === 'adjustment' ? 'Adjustment layer' : type === 'group' ? 'Group' : type === 'smart' ? 'Smart object' : 'Annotation';\n}\n\n/** Layers whose pixels are a surface the compositor draws (pixel layers and smart objects). */\nexport function hasSurface(layer: ImageLayer): layer is PixelLayer | SmartObjectLayer {\n return layer.type === 'pixel' || layer.type === 'smart';\n}\n\n/** Every smart object in the stack (nested contents not included). */\nexport function smartObjectLayers(layers: readonly ImageLayer[]): SmartObjectLayer[] {\n return flattenLayers(layers).filter((l): l is SmartObjectLayer => l.type === 'smart');\n}\n","/**\n * The image editor's compositor: the raster engine (`@pixerate/editor/raster`) with the photo\n * layer kinds plugged in. Adjustment layers filter what is below them (a run of adjacent ones is\n * one float pass with one rounding at the end, exactly as `applyAdjustmentLayers` does, so a\n * document migrated from adjustment layers renders identically); annotation layers are drawn by\n * the host; pixel layers can carry develop and blur filters; masks can be depth masks.\n */\n\nimport { applyAdjustmentLayers } from '../adjustment-layers';\nimport { applyBoxBlur, applyLegacyAdjustments, applyProcess2Adjustments } from '../filters';\nimport { LATEST_PROCESS_VERSION, normalizeProcessVersion } from '../color';\nimport type { BlendMode } from '../blend';\nimport type { AdjustmentLayer, Annotation, CropBox, DepthMaskOptions, ImageAdjustments } from '../types';\nimport {\n RasterCompositor,\n type CompositeCallOptions,\n type CompositeStats,\n type CompositorOptions,\n type CompositorProfile,\n type PixelFilterSpec,\n type RasterSources,\n} from '../../raster/compositor';\nimport type { PixelSurface } from '../../raster/surface';\nimport { isActiveFilter, isSpatialFilter, type AdjustmentLayerNode, type ImageLayer, type LayerFilter, type PixelLayer, type SmartObjectLayer } from './model';\n\nexport { blendTile, changedTiles } from '../../raster/compositor';\nexport type { CompositeCallOptions, CompositeStats, CompositorOptions };\n\n/** What the compositor needs from its host besides the layers. */\nexport interface CompositorSources extends RasterSources {\n /** The pixels of a pixel layer or smart object (resolves layers whose pixels are the document source). */\n surfaceOf?(layer: PixelLayer | SmartObjectLayer): PixelSurface;\n /**\n * A parametric mask's depth map as RGBA at canvas size (the red channel is the depth), or\n * `null` while it is unavailable (the mask then reveals everything).\n */\n depthMap?(source: DepthMaskOptions): { width: number; height: number; data: ArrayLike<number> } | null;\n /** Draws one annotation into RGBA for `rect` (canvas hosts only); `null` when unavailable. */\n rasterizeAnnotation?(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null;\n}\n\nconst legacyViewCache = new WeakMap<AdjustmentLayerNode, AdjustmentLayer>();\n\n/** The `AdjustmentLayer` shape `applyAdjustmentLayers` evaluates, for a node (memoised). */\nexport function adjustmentNodeAsLayer(node: AdjustmentLayerNode): AdjustmentLayer {\n let view = legacyViewCache.get(node);\n if (!view) {\n view = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n } as AdjustmentLayer;\n legacyViewCache.set(node, view);\n }\n return view;\n}\n\n/** Develop settings as `ImageAdjustments` for the process functions (opacity is document-level). */\nfunction developAdjustments(f: Extract<LayerFilter, { kind: 'develop' }>): ImageAdjustments {\n return {\n brightness: f.brightness,\n contrast: f.contrast,\n saturation: f.saturation,\n exposure: f.exposure,\n temperature: f.temperature,\n tint: f.tint ?? 0,\n blur: 0,\n opacity: 1,\n process: f.process,\n };\n}\n\n/** Runs per-pixel filters over RGBA `data` in place. */\nfunction applyPixelFilters(data: Uint8ClampedArray, filters: readonly LayerFilter[]): void {\n for (const f of filters) {\n if (!isActiveFilter(f) || f.kind !== 'develop') continue;\n const adj = developAdjustments(f);\n if (normalizeProcessVersion(adj.process) === LATEST_PROCESS_VERSION) applyProcess2Adjustments({ data }, adj);\n else applyLegacyAdjustments({ data }, adj);\n }\n}\n\n/** Evaluates a depth mask (see `applyDepthMask`) into one coverage byte per depth sample. */\nexport function depthMaskAlpha(depth: number, options: DepthMaskOptions): number {\n const { mode, depthRange, softness, invert } = options;\n let v = invert ? 255 - depth : depth;\n const soft = Math.max(1, softness || 10);\n const [lo, hi] = depthRange;\n if (mode === 'threshold') {\n if (v >= hi + soft) return 0;\n if (v > hi) return Math.round(((hi + soft - v) / soft) * 255);\n if (v >= lo) return 255;\n if (v < lo - soft) return 0;\n return Math.round(((v - (lo - soft)) / soft) * 255);\n }\n v += lo;\n return v < 0 ? 0 : v > 255 ? 255 : v;\n}\n\n/** Runs layer filters (develop, blur) over RGBA `data` (`width × height`) in place, bottom first. */\nexport function runLayerFilters(data: Uint8ClampedArray, width: number, height: number, filters: readonly LayerFilter[]): void {\n for (const f of filters) {\n if (!isActiveFilter(f)) continue;\n if (f.kind === 'develop') applyPixelFilters(data, [f]);\n else if (f.kind === 'blur') applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));\n }\n}\n\nconst filterSpecCache = new WeakMap<object, PixelFilterSpec | null>();\n\n/** A pixel layer's active filters for the engine (memoised per filter list). */\nfunction pixelFilterSpec(layer: PixelLayer): PixelFilterSpec | null {\n const list = layer.filters;\n if (!list?.length) return null;\n if (filterSpecCache.has(list)) return filterSpecCache.get(list)!;\n const filters = list.filter(isActiveFilter);\n let spec: PixelFilterSpec | null = null;\n if (filters.length) {\n const spatial = filters.some(isSpatialFilter);\n spec = {\n key: JSON.stringify(filters),\n spatial,\n apply: (data, width, height) => runLayerFilters(data, width, height, filters),\n };\n }\n filterSpecCache.set(list, spec);\n return spec;\n}\n\nconst PHOTO_PROFILE: CompositorProfile<ImageLayer, CompositorSources> = {\n isFilterLayer: (l) => l.type === 'adjustment',\n applyFilterRun: (layers, acc, _r, masks) => {\n const views = (layers as AdjustmentLayerNode[]).map(adjustmentNodeAsLayer);\n applyAdjustmentLayers({ data: acc }, views, masks);\n },\n contentTile: (l, r, sources) => (l.type === 'annotation' ? (sources.rasterizeAnnotation?.(l.annotation, r) ?? null) : null),\n /** Annotation layers draw with their annotation's blend mode unless the layer sets one. */\n contentBlend: (l) => (l.type === 'annotation' && l.blend === 'normal' && l.annotation.blend ? l.annotation.blend : (l.blend as BlendMode)),\n layerKey: (l) =>\n l.type === 'pixel' ? [l.filters ?? null, l.source ?? null] : l.type === 'adjustment' ? [l.kind, l.params] : l.type === 'annotation' ? l.annotation : undefined,\n pixelFilters: (l) => (l.type === 'pixel' ? pixelFilterSpec(l) : null),\n /** A smart object draws its rendered pixels (contents → placement → smart filters) like a pixel layer. */\n isPixelLike: (l) => l.type === 'smart',\n maskSourcePlane: (source, W, H, sources) => {\n const opts = source as DepthMaskOptions;\n const depth = sources.depthMap?.(opts) ?? null;\n if (!depth || depth.width !== W || depth.height !== H) return null;\n const v = new Float32Array(W * H);\n for (let i = 0; i < v.length; i++) v[i] = depthMaskAlpha(depth.data[i * 4], opts) / 255;\n return v;\n },\n};\n\n/** The per-tile compositor for image editor layer stacks (see the raster module for the caches). */\nexport class LayerCompositor extends RasterCompositor<ImageLayer, CompositorSources> {\n constructor(options: CompositorOptions = {}) {\n super(PHOTO_PROFILE, options);\n }\n}\n","import { LATEST_PROCESS_VERSION } from '../color';\nimport type { DepthMaskOptions, ImageAdjustments, ImageTransform } from '../types';\n\n/** Neutral base adjustments, latest process (re-exported as `DEFAULT_ADJUSTMENTS`). */\nexport const DEFAULT_ADJUSTMENTS_VALUES: ImageAdjustments = {\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n blur: 0,\n opacity: 1,\n process: LATEST_PROCESS_VERSION,\n};\n\nexport const DEFAULT_TRANSFORM_VALUES: ImageTransform = {\n rotate: 0,\n flipH: false,\n flipV: false,\n};\n\nexport const DEFAULT_DEPTH_MASK_VALUES: DepthMaskOptions = {\n enabled: false,\n maskSource: null,\n mode: 'threshold',\n depthRange: [0, 255],\n softness: 10,\n invert: false,\n};\n","/**\n * Between the old document-level model (schema v1 / v2: base sliders, adjustment layers, blur,\n * depth mask and annotations) and the layer stack:\n *\n * - `layersFromLegacy` lifts a v1 / v2 state into layers: the source becomes the Background\n * pixel layer carrying the base sliders as its `develop` filter and the depth mask as its\n * parametric mask; adjustment layers go above it, annotations on top in their order. Blur\n * becomes a `blur` filter on the Background, or `legacyOutputBlur` when adjustment layers\n * exist (blur used to run after them).\n * - `legacyView` reads the stack back as `adjustments` / `depthMask` / `annotations` (the\n * compatibility views on the editor state) and says whether the stack still has exactly the\n * legacy shape, in which case it serializes as v1 / v2 byte for byte.\n */\n\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport { normalizeAdjustmentLayers } from '../adjustment-layers';\nimport { normalizeProcessVersion } from '../color';\nimport { decodeLayerMask, normalizeLayerMask } from '../selection/layer-mask';\nimport { encodeSelectionRle, resizeSelection } from '../selection/mask';\nimport type {\n AdjustmentLayer,\n AdjustmentLayerMask,\n Annotation,\n DepthMaskOptions,\n ImageAdjustments,\n} from '../types';\nimport {\n blurFilter,\n developFilter,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type ImageLayer,\n type LayerFilter,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { emptySurface, readRegion, surfaceFromPixels, type MaskSurface } from '../../raster/surface';\n\nexport const BACKGROUND_LAYER_ID = 'background';\nexport const BACKGROUND_LAYER_NAME = 'Background';\nexport const DEVELOP_FILTER_ID = 'develop';\nexport const BLUR_FILTER_ID = 'blur';\n\n/** The old document-level parts of the editor state. */\nexport interface LegacyParts {\n adjustments: ImageAdjustments;\n depthMask: DepthMaskOptions;\n annotations: Annotation[];\n}\n\n/** The stack as stored at document level next to the layers. */\nexport interface LayerStack {\n layers: ImageLayer[];\n /** Export alpha (the old `adjustments.opacity`), 0..1. */\n outputOpacity: number;\n /** Migrated blur that must run after the adjustment layers (never created by new edits). */\n legacyOutputBlur?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Masks\n// ---------------------------------------------------------------------------\n\n/**\n * Stored `r1:` encodings of masks lifted from v1 / v2, by surface, so an unchanged mask writes\n * back byte for byte (and a mask of another size than the document keeps its size).\n */\nconst legacyMaskMemo = new WeakMap<MaskSurface, { width: number; height: number; rle: string }>();\n\n/** A raster mask from an old adjustment-layer mask, resampled to the document if needed. */\nexport function rasterMaskFromLegacy(mask: AdjustmentLayerMask, width: number, height: number): RasterMask | null {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const surface = surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);\n legacyMaskMemo.set(surface, { width: mask.width, height: mask.height, rle: mask.rle });\n const out: RasterMask = { surface, defaultValue: 0 };\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n return out;\n}\n\n/**\n * True when an adjustment layer's mask came from a v1 / v2 mask of another size than the canvas\n * (the old renderer resampled it per render; the stack holds a canvas-sized copy).\n */\nexport function hasResampledLegacyMask(layers: readonly ImageLayer[], width: number, height: number): boolean {\n for (const l of layers) {\n if (l.type !== 'adjustment' || !l.mask) continue;\n const memo = legacyMaskMemo.get(l.mask.surface);\n if (memo && (memo.width !== width || memo.height !== height)) return true;\n }\n return false;\n}\n\n/** The old mask shape of a raster mask (its pixels over the canvas, run-length encoded). */\nexport function legacyMaskOf(mask: RasterMask, width: number, height: number): AdjustmentLayerMask {\n let memo = legacyMaskMemo.get(mask.surface);\n if (!memo) {\n const data = readRegion(mask.surface, { x: 0, y: 0, width, height });\n memo = { width, height, rle: encodeSelectionRle(new Uint8Array(data.buffer, data.byteOffset, data.length)) };\n legacyMaskMemo.set(mask.surface, memo);\n }\n return normalizeLayerMask({ ...memo, density: mask.density, feather: mask.feather, enabled: mask.enabled })!;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy → layers\n// ---------------------------------------------------------------------------\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/** True when the depth mask settings are the defaults (no mask at all). */\nexport function isDefaultDepthMask(mask: DepthMaskOptions | undefined): boolean {\n if (!mask) return true;\n return sameJson({ ...DEFAULT_DEPTH_MASK_VALUES, ...mask, depthRange: [...(mask.depthRange ?? [])] }, DEFAULT_DEPTH_MASK_VALUES);\n}\n\nexport function developFromAdjustments(adjustments: ImageAdjustments): DevelopFilter {\n return {\n id: DEVELOP_FILTER_ID,\n kind: 'develop',\n enabled: true,\n brightness: adjustments.brightness ?? 0,\n contrast: adjustments.contrast ?? 0,\n saturation: adjustments.saturation ?? 0,\n exposure: adjustments.exposure ?? 0,\n temperature: adjustments.temperature ?? 0,\n tint: adjustments.tint ?? 0,\n process: normalizeProcessVersion(adjustments.process),\n };\n}\n\n/** The default name of an annotation layer. */\nexport function annotationLayerName(annotation: Annotation): string {\n switch (annotation.type) {\n case 'pen':\n return 'Pen';\n case 'rect':\n return 'Rectangle';\n case 'circle':\n return 'Ellipse';\n case 'arrow':\n return 'Arrow';\n case 'line':\n return 'Line';\n case 'text':\n return annotation.text ? annotation.text.slice(0, 32) : 'Text';\n case 'image':\n return 'Image';\n }\n}\n\nexport function annotationLayerFor(annotation: Annotation): AnnotationLayer {\n return {\n type: 'annotation',\n id: annotation.id,\n name: annotationLayerName(annotation),\n visible: true,\n opacity: 1,\n blend: 'normal',\n annotation,\n };\n}\n\nexport function adjustmentNodeFromLegacy(layer: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const node = {\n type: 'adjustment',\n id: layer.id,\n name: layer.name ?? '',\n visible: layer.enabled !== false,\n opacity: layer.opacity ?? 1,\n blend: layer.blend ?? 'normal',\n kind: layer.kind,\n params: JSON.parse(JSON.stringify(layer.params)),\n } as AdjustmentLayerNode;\n if (layer.mask) {\n const mask = rasterMaskFromLegacy(layer.mask, width, height);\n if (mask) node.mask = mask;\n }\n return node;\n}\n\n/** The Background layer of an opened image (its pixels are the document source). */\nexport function backgroundLayer(filters: LayerFilter[] = [], mask?: RasterMask): PixelLayer {\n const layer: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: emptySurface(4),\n offset: { x: 0, y: 0 },\n source: 'document',\n filters,\n };\n if (mask) layer.mask = mask;\n return layer;\n}\n\n/** A parametric depth mask on a layer. */\nexport function depthRasterMask(source: DepthMaskOptions): RasterMask {\n const out: RasterMask = {\n surface: emptySurface(1, 255),\n defaultValue: 255,\n source: { ...source, depthRange: [...source.depthRange] as [number, number] },\n };\n if (!source.enabled) out.enabled = false;\n return out;\n}\n\n/** Lifts a v1 / v2 state into a layer stack (see the module comment). */\nexport function layersFromLegacy(parts: Partial<LegacyParts>, width: number, height: number): LayerStack {\n // As `normalizeStoredAdjustments`: stored values without `process` keep the legacy rendering.\n const stored = parts.adjustments;\n const adjustments = (\n stored ? { ...DEFAULT_ADJUSTMENTS_VALUES, ...stored, process: normalizeProcessVersion(stored.process) } : { ...DEFAULT_ADJUSTMENTS_VALUES }\n ) as ImageAdjustments;\n const adjLayers = normalizeAdjustmentLayers(adjustments.layers);\n const filters: LayerFilter[] = [developFromAdjustments(adjustments)];\n const blur = typeof adjustments.blur === 'number' && adjustments.blur > 0 ? adjustments.blur : 0;\n let legacyOutputBlur: number | undefined;\n if (blur > 0) {\n if (adjLayers.length) legacyOutputBlur = blur;\n else filters.push({ id: BLUR_FILTER_ID, kind: 'blur', enabled: true, radius: blur });\n }\n const depth = parts.depthMask;\n const mask = depth && !isDefaultDepthMask(depth) ? depthRasterMask({ ...DEFAULT_DEPTH_MASK_VALUES, ...depth }) : undefined;\n const layers: ImageLayer[] = [backgroundLayer(filters, mask)];\n for (const l of adjLayers) layers.push(adjustmentNodeFromLegacy(l, width, height));\n for (const a of parts.annotations ?? []) layers.push(annotationLayerFor(JSON.parse(JSON.stringify(a))));\n const out: LayerStack = { layers, outputOpacity: typeof adjustments.opacity === 'number' ? adjustments.opacity : 1 };\n if (legacyOutputBlur !== undefined) out.legacyOutputBlur = legacyOutputBlur;\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Layers → legacy views\n// ---------------------------------------------------------------------------\n\n/** The old `AdjustmentLayer` shape of a node, keys in the order `normalizeAdjustmentLayer` writes. */\nexport function legacyAdjustmentLayer(node: AdjustmentLayerNode, width: number, height: number): AdjustmentLayer {\n const out: Record<string, unknown> = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n };\n if (node.name) out.name = node.name;\n if (node.mask && !node.mask.source) out.mask = legacyMaskOf(node.mask, width, height);\n return out as unknown as AdjustmentLayer;\n}\n\n/** The pixel layer whose develop filter the legacy API edits: the active one, else the Background. */\nexport function developTarget(layers: readonly ImageLayer[], activeLayerId: string | null): PixelLayer | null {\n const pixels: PixelLayer[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'pixel') pixels.push(l);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (activeLayerId) {\n const active = pixels.find((l) => l.id === activeLayerId);\n if (active) return active;\n }\n return pixels.find((l) => l.source === 'document') ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;\n}\n\nexport interface LegacyViewResult extends LegacyParts {\n /**\n * True when the stack has exactly the shape a v1 / v2 document lifts to (an unpainted,\n * unmoved Background with develop / blur filters and an optional depth mask, then adjustment\n * layers, then plain annotation layers), so it can be written as v1 / v2.\n */\n exact: boolean;\n}\n\nconst isPlain = (l: ImageLayer) =>\n l.visible && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask && !l.locks && !l.effects;\n\n/** Reads the stack back as the old document-level parts (see `LegacyViewResult`). */\nexport function legacyView(stack: LayerStack, width: number, height: number, activeLayerId: string | null = null): LegacyViewResult {\n const { layers } = stack;\n const target = developTarget(layers, activeLayerId);\n const develop = target ? developFilter(target) : undefined;\n const blur = stack.legacyOutputBlur ?? (target ? blurFilter(target)?.radius : undefined) ?? 0;\n const adjustments: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS_VALUES,\n ...(develop\n ? {\n brightness: develop.brightness,\n contrast: develop.contrast,\n saturation: develop.saturation,\n exposure: develop.exposure,\n temperature: develop.temperature,\n tint: develop.tint ?? 0,\n }\n : {}),\n blur,\n opacity: stack.outputOpacity,\n process: develop ? normalizeProcessVersion(develop.process) : DEFAULT_ADJUSTMENTS_VALUES.process,\n };\n const adjNodes: AdjustmentLayerNode[] = [];\n const annotations: Annotation[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'adjustment') adjNodes.push(l);\n else if (l.type === 'annotation') annotations.push(l.annotation);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (adjNodes.length) adjustments.layers = adjNodes.map((n) => legacyAdjustmentLayer(n, width, height));\n const depthSource = target?.mask?.source;\n const depthMask: DepthMaskOptions = depthSource\n ? { ...depthSource, depthRange: [...depthSource.depthRange] as [number, number] }\n : { ...DEFAULT_DEPTH_MASK_VALUES, depthRange: [...DEFAULT_DEPTH_MASK_VALUES.depthRange] as [number, number] };\n return { adjustments, depthMask, annotations, exact: isLegacyShape(stack) };\n}\n\n/** See `LegacyViewResult.exact`. */\nexport function isLegacyShape(stack: LayerStack): boolean {\n const { layers } = stack;\n const bg = layers[0];\n if (!bg || bg.type !== 'pixel' || bg.source !== 'document') return false;\n if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;\n if (!bg.visible || bg.opacity !== 1 || bg.blend !== 'normal' || bg.clipped || bg.locks || bg.effects) return false;\n if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;\n const filters = bg.filters ?? [];\n if (filters.length < 1 || filters.length > 2) return false;\n const [develop, blur] = filters;\n if (develop.kind !== 'develop' || !develop.enabled || develop.id !== DEVELOP_FILTER_ID) return false;\n if (blur && (blur.kind !== 'blur' || !blur.enabled || blur.id !== BLUR_FILTER_ID || !(blur.radius > 0))) return false;\n if (bg.mask) {\n const m = bg.mask;\n if (!m.source || m.density !== undefined || m.feather !== undefined) return false;\n if ((m.enabled !== false) !== m.source.enabled) return false;\n }\n let i = 1;\n let adjustments = 0;\n for (; i < layers.length && layers[i].type === 'adjustment'; i++) {\n const l = layers[i] as AdjustmentLayerNode;\n if (l.clipped || l.locks || l.mask?.source || l.effects) return false;\n adjustments++;\n }\n for (; i < layers.length; i++) {\n const l = layers[i];\n if (l.type !== 'annotation' || !isPlain(l) || l.id !== l.annotation.id || l.name !== annotationLayerName(l.annotation)) return false;\n }\n if (blur && adjustments) return false;\n if (stack.legacyOutputBlur !== undefined && (!adjustments || !(stack.legacyOutputBlur > 0))) return false;\n return true;\n}\n","/**\n * Pure helpers between the compositor and the renderer: the output stage (projection to the\n * cropped, rotated frame, output opacity and the migrated blur), the routing rule that keeps\n * old-shape documents on the exact legacy path where document-space compositing cannot match it,\n * and per-tile histograms. No DOM.\n */\n\nimport { applyBoxBlur } from '../filters';\nimport { normalizeAngle, unprojectCanvasPoint, getRotatedSize, type ImageProjection } from '../geometry';\nimport { HISTOGRAM_BINS, type ImageHistogram } from '../histogram';\nimport type { CropBox, ImageTransform } from '../types';\nimport { blurFilter, type AnnotationLayer, type ImageLayer, type PixelLayer } from './model';\nimport { hasResampledLegacyMask, isLegacyShape, type LayerStack } from './migrate';\nimport { LayerCompositor, type CompositorSources } from './compositor';\n\n/** True for 0, 90, 180 and 270 degrees. */\nexport function isQuarterTurn(rotate: number): boolean {\n return normalizeAngle(rotate) % 90 === 0;\n}\n\nfunction isIntegerRect(r: CropBox): boolean {\n return Number.isInteger(r.x) && Number.isInteger(r.y) && Number.isInteger(r.width) && Number.isInteger(r.height);\n}\n\n/** The projection that draws `region` into a `baseW × baseH` buffer at scale 1 (the renderer's base canvas). */\nexport function baseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/**\n * Projects `src` (RGBA covering `srcRect` in document space) into the base frame of `region`\n * under a quarter-turn `transform`: an exact pixel permutation (crop, flips, 90° steps), the\n * same mapping the canvas draws with. Returns the base buffer and its size.\n */\nexport function projectQuarterTurn(\n src: ArrayLike<number>,\n srcRect: CropBox,\n region: CropBox,\n transform: ImageTransform\n): { width: number; height: number; data: Uint8ClampedArray } {\n const rotated = getRotatedSize(region.width, region.height, transform);\n const w = Math.max(1, Math.round(rotated.width));\n const h = Math.max(1, Math.round(rotated.height));\n const out = new Uint8ClampedArray(w * h * 4);\n const p = baseProjection(region, transform, w, h);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, p);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, p);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, p);\n const ux = Math.round(ox.x - o.x);\n const vx = Math.round(ox.y - o.y);\n const uy = Math.round(oy.x - o.x);\n const vy = Math.round(oy.y - o.y);\n const u0 = Math.floor(o.x) - srcRect.x;\n const v0 = Math.floor(o.y) - srcRect.y;\n const sw = srcRect.width;\n const sh = srcRect.height;\n for (let y = 0; y < h; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n for (let x = 0; x < w; x++, u += ux, v += vx) {\n if (u < 0 || v < 0 || u >= sw || v >= sh) continue;\n const s = (v * sw + u) * 4;\n const d = (y * w + x) * 4;\n out[d] = src[s];\n out[d + 1] = src[s + 1];\n out[d + 2] = src[s + 2];\n out[d + 3] = src[s + 3];\n }\n }\n return { width: w, height: h, data: out };\n}\n\n/** Output opacity as the old base adjustments applied it (`clamp(a · opacity)`), then the migrated blur. */\nexport function applyOutputStage(img: { width: number; height: number; data: Uint8ClampedArray }, opacity: number, legacyOutputBlur?: number): void {\n if (opacity < 1) {\n const d = img.data;\n for (let i = 3; i < d.length; i += 4) d[i] = d[i] * opacity;\n }\n if (legacyOutputBlur && legacyOutputBlur > 0) applyBoxBlur(img, Math.min(Math.round(legacyOutputBlur), 40));\n}\n\nexport interface RouteInput {\n stack: LayerStack;\n region: CropBox;\n canvas: { width: number; height: number };\n transform: ImageTransform;\n /** Whether the source image is fully opaque. */\n sourceOpaque: boolean;\n}\n\n/**\n * Whether a document must render through the legacy base path to look exactly as before. Only\n * documents that still have the v1 / v2 shape qualify, and only where compositing in document\n * space cannot reproduce the old order of operations:\n *\n * - a rotation that is not a quarter turn, or a crop with fractional coordinates (the old path\n * resampled before adjusting);\n * - an enabled depth mask (applied after the blur and resampled with the canvas);\n * - output opacity below 1 (it rounded alpha before the depth mask);\n * - a Background blur filter with a crop or a 90° / 270° turn (the old blur clamped at the crop\n * edge and ran on the rotated buffer);\n * - a migrated output blur over a source with transparency (canvas premultiplication would round\n * colours before blurring);\n * - an adjustment-layer mask stored at another size than the canvas (it was resampled through\n * the projection on every render).\n */\nexport function needsLegacyBase(input: RouteInput): boolean {\n const { stack, region, canvas, transform } = input;\n if (!isLegacyShape(stack)) return false;\n const bg = stack.layers[0] as PixelLayer;\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) return true;\n if (bg.mask?.source?.enabled && bg.mask.source.maskSource) return true;\n if (stack.outputOpacity < 1) return true;\n const blur = blurFilter(bg);\n if (blur && blur.enabled && blur.radius > 0) {\n const full = region.x === 0 && region.y === 0 && region.width === canvas.width && region.height === canvas.height;\n const angle = normalizeAngle(transform.rotate);\n if (!full || angle === 90 || angle === 270) return true;\n }\n if ((stack.legacyOutputBlur ?? 0) > 0 && !input.sourceOpaque) return true;\n if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;\n return false;\n}\n\nconst isPlainAnnotation = (l: ImageLayer): l is AnnotationLayer =>\n l.type === 'annotation' && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask;\n\n/**\n * Splits the stack into what the compositor rasterizes and the annotation layers at the top that\n * draw as crisp vectors over the result (as annotations always did): the trailing run of plain\n * top-level annotation layers (hidden ones are skipped).\n */\nexport function splitOverlayAnnotations(layers: readonly ImageLayer[]): { composite: ImageLayer[]; overlay: AnnotationLayer[] } {\n let i = layers.length;\n while (i > 0) {\n const l = layers[i - 1];\n if (l.type === 'annotation' && (!l.visible || isPlainAnnotation(l))) i--;\n else break;\n }\n return {\n composite: layers.slice(0, i) as ImageLayer[],\n overlay: (layers.slice(i) as AnnotationLayer[]).filter((l) => l.visible),\n };\n}\n\n/** The integer rectangle covering `region`. */\nexport function coveringRect(region: CropBox): CropBox {\n const x = Math.floor(region.x);\n const y = Math.floor(region.y);\n return { x, y, width: Math.ceil(region.x + region.width) - x, height: Math.ceil(region.y + region.height) - y };\n}\n\n/**\n * The base image of a layered document without a canvas: the stack composited over `region`\n * (integer), projected under a quarter-turn `transform`, then output opacity and the migrated\n * blur. Annotation layers need a canvas and are skipped unless `sources` can rasterize them.\n * This is what the renderer's base canvas holds for such documents.\n */\nexport function renderLayeredBase(\n stack: LayerStack,\n region: CropBox,\n transform: ImageTransform,\n sources: CompositorSources,\n compositor = new LayerCompositor()\n): { width: number; height: number; data: Uint8ClampedArray } {\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) {\n throw new Error('renderLayeredBase needs an integer region and a quarter-turn rotation.');\n }\n const composite = compositor.composite(stack.layers, region, sources);\n const out = projectQuarterTurn(composite, region, region, transform);\n applyOutputStage(out, stack.outputOpacity, stack.legacyOutputBlur);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Histograms\n// ---------------------------------------------------------------------------\n\n/** An empty histogram. */\nexport function emptyHistogram(): ImageHistogram {\n return {\n red: new Uint32Array(HISTOGRAM_BINS),\n green: new Uint32Array(HISTOGRAM_BINS),\n blue: new Uint32Array(HISTOGRAM_BINS),\n luminance: new Uint32Array(HISTOGRAM_BINS),\n samples: 0,\n transparent: 0,\n shadows: [0, 0, 0],\n highlights: [0, 0, 0],\n max: 0,\n };\n}\n\n/** Adds the pixels of `rect` (in tile-local coordinates of a `stride`-wide RGBA buffer) to `h`. */\nexport function addToHistogram(h: ImageHistogram, data: ArrayLike<number>, stride: number, rect: CropBox): void {\n for (let y = rect.y; y < rect.y + rect.height; y++) {\n let i = (y * stride + rect.x) * 4;\n for (let x = 0; x < rect.width; x++, i += 4) {\n if (!(data[i + 3] > 0)) {\n h.transparent++;\n continue;\n }\n h.samples++;\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n h.red[r]++;\n h.green[g]++;\n h.blue[b]++;\n h.luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) h.shadows[0]++;\n if (g === 0) h.shadows[1]++;\n if (b === 0) h.shadows[2]++;\n if (r === 255) h.highlights[0]++;\n if (g === 255) h.highlights[1]++;\n if (b === 255) h.highlights[2]++;\n }\n }\n}\n\n/** Sums histograms (and recomputes `max`). */\nexport function sumHistograms(parts: Iterable<ImageHistogram>): ImageHistogram {\n const out = emptyHistogram();\n for (const p of parts) {\n for (let i = 0; i < HISTOGRAM_BINS; i++) {\n out.red[i] += p.red[i];\n out.green[i] += p.green[i];\n out.blue[i] += p.blue[i];\n out.luminance[i] += p.luminance[i];\n }\n out.samples += p.samples;\n out.transparent += p.transparent;\n for (let c = 0; c < 3; c++) {\n out.shadows[c] += p.shadows[c];\n out.highlights[c] += p.highlights[c];\n }\n }\n let max = 0;\n for (let i = 0; i < HISTOGRAM_BINS; i++) max = Math.max(max, out.red[i], out.green[i], out.blue[i], out.luminance[i]);\n out.max = max;\n return out;\n}\n","import type {\n Annotation,\n CropBox,\n ImagePoint,\n ExportOptions,\n ImageEditorFit,\n ImageEditorState,\n ImageTransform,\n PenAnnotation,\n RectAnnotation,\n CircleAnnotation,\n ArrowAnnotation,\n LineAnnotation,\n TextAnnotation,\n ImageAnnotation,\n} from './types';\nimport { applyAdjustments, applyDepthMask } from './filters';\nimport { adjustmentLayersKey } from './adjustment-layers';\nimport { canvasCompositeOperation, blendImageData, normalizeBlendMode } from './blend';\nimport { computeHistogram, type ImageHistogram } from './histogram';\nimport { normalizeProcessVersion } from './color';\nimport { buildLayerMaskPlanes } from './selection/layer-mask';\nimport { selectionOutline, quickMaskPixels } from './selection/overlay';\nimport type { SelectionMask } from './selection/mask';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport { flattenLayers, type ImageLayer, type PixelLayer } from './layers/model';\nimport {\n addToHistogram,\n applyOutputStage,\n baseProjection,\n coveringRect,\n emptyHistogram,\n needsLegacyBase,\n splitOverlayAnnotations,\n sumHistograms,\n} from './layers/render';\nimport { TILE, tileKey, type PixelSurface, type TileKey } from '../raster/surface';\nimport type { GlobalLight } from '../raster/effects/model';\nimport type { EffectPattern } from '../raster/effects/patterns';\nimport { isLegacyShape as isLegacyStack } from './layers/migrate';\nimport {\n applyImageProjectionToContext,\n projectImagePoint,\n clampCropToImage,\n getAnnotationBounds,\n getRotatedSize,\n getViewportMetrics,\n getViewportProjection,\n normalizeAngle,\n type ImageProjection,\n type ViewportMetrics,\n} from './geometry';\n\nexport interface RenderOptions {\n viewportWidth?: number;\n viewportHeight?: number;\n showCropOverlay?: boolean;\n showSelectionOverlay?: boolean;\n interactive?: boolean;\n draftAnnotation?: Annotation | null;\n draftCrop?: CropBox | null;\n /** Viewport fit mode: 'contain' scales to full viewport bounds; 'margin' scales to 85% with 1.0 cap (default). */\n fit?: ImageEditorFit;\n /**\n * How the active selection is shown: marching ants (default when there is a selection),\n * a Quick Mask tint over the unselected area (default when `state.quickMask`), or not at all.\n */\n selectionDisplay?: 'ants' | 'quickMask' | 'none';\n /** Dash offset of the marching ants in screen pixels (animate it to make them march). */\n antsOffset?: number;\n /** A selection being drawn (e.g. a lasso), shown as a dashed outline in natural pixels. */\n draftSelectionPath?: ImagePoint[] | null;\n /** Document rectangles that are the only change since the last render (strokes); recomposites only there. */\n dirty?: CropBox[];\n /** The layer being edited: tiles keep a checkpoint below it so strokes and drags on it start there. */\n checkpointLayerId?: string | null;\n /** The paint tools' cursor: a circle of `diameter` px at (`x`, `y`) in natural pixels. */\n brushCursor?: { x: number; y: number; diameter: number } | null;\n /** Free Transform's frame, handles and reference point in natural pixels (`FreeTransformTool.overlay()`). */\n freeTransform?: {\n outline: ImagePoint[];\n handles: ImagePoint[];\n reference: ImagePoint;\n /** Warp mode: the mesh's grid lines as cubics, its anchors, and handles with their anchors. */\n warp?: { curves: ImagePoint[][]; anchors: ImagePoint[]; handles: Array<{ point: ImagePoint; anchor: ImagePoint }> };\n } | null;\n}\n\n/**\n * Image Cache to avoid refetching HTMLImageElement objects repeatedly during re-renders.\n */\nclass ImageCache {\n private cache = new Map<string, HTMLImageElement>();\n\n public async get(src: string): Promise<HTMLImageElement> {\n const existing = this.cache.get(src);\n if (existing && existing.complete) {\n return existing;\n }\n\n return new Promise((resolve, reject) => {\n if (typeof window === 'undefined' || typeof Image === 'undefined') {\n reject(new Error('Image loading is only supported in browser/DOM environments'));\n return;\n }\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n this.cache.set(src, img);\n resolve(img);\n };\n img.onerror = (e) => reject(new Error(`Failed to load image from: ${src}`));\n img.src = src;\n\n // In JSDOM or test environments where Image resource loading is inactive, resolve promptly\n if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {\n setTimeout(() => {\n try {\n Object.defineProperty(img, 'naturalWidth', { value: 800, configurable: true });\n Object.defineProperty(img, 'naturalHeight', { value: 600, configurable: true });\n Object.defineProperty(img, 'complete', { value: true, configurable: true });\n } catch {\n // Ignore definition errors in restrictive environments\n }\n this.cache.set(src, img);\n resolve(img);\n }, 10);\n }\n });\n }\n\n public set(src: string, img: HTMLImageElement): void {\n this.cache.set(src, img);\n }\n\n /** The loaded image for `src`, or `undefined` while it is not loaded yet. */\n public peek(src: string): HTMLImageElement | undefined {\n const img = this.cache.get(src);\n return img && img.complete ? img : undefined;\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\nexport const globalImageCache = new ImageCache();\n\n/**\n * Renders an image with all its transformations, filters, depth masks, and annotations\n * to a target HTMLCanvasElement.\n */\nexport class ImageEditorRenderer {\n public imageCache = globalImageCache;\n private cachedBaseCanvas: HTMLCanvasElement | null = null;\n private cachedBaseKey = '';\n /**\n * Histogram of the last viewport base render (after adjustments and layers, before\n * annotations); `null` until a render with pixel access has happened.\n */\n public previewHistogram: ImageHistogram | null = null;\n /** Called whenever the viewport base image is re-rendered with the new histogram. */\n public onPreviewHistogram?: (histogram: ImageHistogram | null) => void;\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 0;\n /**\n * The viewport's compositor. It only ever composites the viewport's stack, so its tile cache can\n * trust dirty rectangles; exports use `exportCompositor`.\n */\n public compositor = new LayerCompositor();\n private exportCompositor = new LayerCompositor({ maxCacheBytes: 64 * 1024 * 1024 });\n /** How the host resolves layer pixels (set by the controller). */\n public layerSources?: LayerSourcesHost;\n /**\n * Debug / test switch: `'legacy'` forces the old base path for documents that still have the\n * v1 / v2 shape, `'layers'` forces the compositor. Default: chosen per document.\n */\n public baseRoute: 'auto' | 'legacy' | 'layers' = 'auto';\n /** Which path the last base render took. */\n public lastBaseRoute: 'legacy' | 'layers' | null = null;\n private comp: CompositeCanvas | null = null;\n private depthMaps = new Map<string, { width: number; height: number; data: Uint8ClampedArray }>();\n private assetVersion = 0;\n private scratch: HTMLCanvasElement | null = null;\n private cachedPostCanvas: HTMLCanvasElement | null = null;\n private cachedPostKey = '';\n private cachedPostHistogram: ImageHistogram | null = null;\n\n /**\n * Helper to create a canvas element safely in browser/JSDOM.\n */\n public createCanvas(width: number, height: number): HTMLCanvasElement {\n if (typeof document !== 'undefined' && document.createElement) {\n const c = document.createElement('canvas');\n c.width = Math.max(1, Math.round(width));\n c.height = Math.max(1, Math.round(height));\n return c;\n }\n // Fallback for non-DOM / headless\n return {\n width: Math.max(1, Math.round(width)),\n height: Math.max(1, Math.round(height)),\n getContext: () => null,\n toDataURL: () => '',\n toBlob: () => {},\n } as unknown as HTMLCanvasElement;\n }\n\n /**\n * Renders the editor state onto a canvas (viewport or preview).\n */\n public async renderToCanvas(\n targetCanvas: HTMLCanvasElement,\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: RenderOptions = {}\n ): Promise<void> {\n const ctx = targetCanvas.getContext('2d');\n if (!ctx) return;\n\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const fit = options.fit ?? state.fit ?? 'margin';\n // Geometry helpers read `imageDimensions`; make sure they see the real source size so the\n // viewport drawn here and the hit-testing done by the UI share one projection.\n const geoState: ImageEditorState = {\n ...state,\n fit,\n imageDimensions:\n state.imageDimensions.width === naturalWidth && state.imageDimensions.height === naturalHeight\n ? state.imageDimensions\n : { width: naturalWidth, height: naturalHeight },\n };\n\n const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;\n\n // Viewport dimensions\n const vpWidth = options.viewportWidth || targetCanvas.width;\n const vpHeight = options.viewportHeight || targetCanvas.height;\n\n targetCanvas.width = vpWidth;\n targetCanvas.height = vpHeight;\n\n ctx.clearRect(0, 0, vpWidth, vpHeight);\n\n // If in compare mode, render original image directly and exit\n if (isOriginalCompared) {\n this.drawFittedImage(ctx, img, naturalWidth, naturalHeight, vpWidth, vpHeight, zoom, pan, fit);\n return;\n }\n\n // When actively in crop tool or overlay requested, show full image so user can adjust the crop box\n const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === 'crop');\n const projection = getViewportProjection(geoState, vpWidth, vpHeight, isActivelyCropping);\n const { region } = projection;\n const rotated = getRotatedSize(region.width, region.height, transform);\n\n const route = this.chooseRoute(state, region, naturalWidth, naturalHeight);\n let overlay: Annotation[] = annotations;\n if (route === 'legacy') {\n // 1. Offscreen canvas for Base Image + Transform (Flip/Rotate) + Crop + Adjustments + Depth mask\n const baseKey = [\n this.getSourceId(img),\n state.sourceUrl,\n `${naturalWidth}x${naturalHeight}`,\n `${region.x},${region.y},${region.width},${region.height}`,\n `${projection.angle},${transform.flipH},${transform.flipV}`,\n `${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.tint ?? 0},${adjustments.blur},${adjustments.opacity},p${normalizeProcessVersion(adjustments.process)}`,\n adjustmentLayersKey(adjustments.layers),\n `${depthMask.enabled},${depthMask.maskSource},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`,\n ].join('|');\n\n let baseCanvas = this.cachedBaseCanvas;\n if (!baseCanvas || this.cachedBaseKey !== baseKey) {\n let histogram: ImageHistogram | null = null;\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state, (pixels) => {\n histogram = computeHistogram(pixels);\n });\n this.cachedBaseCanvas = baseCanvas;\n this.cachedBaseKey = baseKey;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n // 2. Draw the base canvas into the viewport (centred, fitted, zoomed, panned)\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(baseCanvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n } else {\n // 1–2. The layer stack, composited per tile in document space, drawn through the projection.\n const split = splitOverlayAnnotations(state.layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const comp = this.updateComposite(split.composite, region, naturalWidth, naturalHeight, options);\n const base = this.layeredBase(comp, state, region, transform);\n if (base) {\n // Drawn exactly as the legacy base canvas is, so both paths look the same at any zoom.\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(base.canvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n this.publishHistogram(base.histogram ?? comp?.histogram() ?? null);\n }\n }\n\n // 3. Annotations live in natural image space: draw them through the same projection,\n // clipped to the visible (cropped) region.\n if (overlay.length > 0 || options.draftAnnotation) {\n ctx.save();\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {\n this.drawAnnotationSelection(ctx, ann);\n }\n }\n // A selected annotation drawn inside the composite still gets its handles.\n if (options.showSelectionOverlay && selectedAnnotationId && !overlay.some((a) => a.id === selectedAnnotationId)) {\n const sel = annotations.find((a) => a.id === selectedAnnotationId);\n if (sel) this.drawAnnotationSelection(ctx, sel);\n }\n\n // Render active live draft annotation\n if (options.draftAnnotation) {\n await this.drawAnnotation(ctx, options.draftAnnotation);\n }\n\n ctx.restore();\n }\n\n // 4. Selection: marching ants or Quick Mask, through the same projection.\n const display = options.selectionDisplay ?? (state.quickMask ? 'quickMask' : 'ants');\n if ((state.selection?.bounds && display !== 'none') || options.draftSelectionPath?.length) {\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n if (state.selection?.bounds && display === 'quickMask') this.drawQuickMask(ctx, state.selection);\n else if (state.selection?.bounds && display === 'ants') this.drawAnts(ctx, state.selection, projection.scale, options.antsOffset ?? 0);\n if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale);\n } catch {\n // Overlays are best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Brush cursor: the tip outline, one screen pixel wide, white over black.\n if (options.brushCursor) {\n const b = options.brushCursor;\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n const px = 1 / (projection.scale || 1);\n ctx.beginPath();\n ctx.arc(b.x, b.y, Math.max(px, b.diameter / 2), 0, Math.PI * 2);\n ctx.lineWidth = 2 * px;\n ctx.strokeStyle = 'rgba(0,0,0,0.6)';\n ctx.stroke();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Free Transform: the frame, square handles and the reference point, a fixed size on screen.\n if (options.freeTransform) {\n const t = options.freeTransform;\n ctx.save();\n try {\n const scr = (p: ImagePoint) => projectImagePoint(p, projection);\n ctx.beginPath();\n if (t.warp) {\n for (const c of t.warp.curves) {\n const [a, b, d, e] = c.map(scr);\n ctx.moveTo(a.x, a.y);\n ctx.bezierCurveTo(b.x, b.y, d.x, d.y, e.x, e.y);\n }\n for (const h of t.warp.handles) {\n const a = scr(h.anchor);\n const q = scr(h.point);\n ctx.moveTo(a.x, a.y);\n ctx.lineTo(q.x, q.y);\n }\n } else {\n const outline = t.outline.map(scr);\n outline.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));\n ctx.closePath();\n }\n ctx.lineWidth = 3;\n ctx.strokeStyle = 'rgba(0,0,0,0.5)';\n ctx.stroke();\n ctx.lineWidth = 1;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n for (const h of [...t.handles, ...(t.warp?.anchors ?? [])].map(scr)) {\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#1e293b';\n ctx.fillRect(h.x - 4, h.y - 4, 8, 8);\n ctx.strokeRect(h.x - 4, h.y - 4, 8, 8);\n }\n for (const h of (t.warp?.handles ?? []).map((w) => scr(w.point))) {\n ctx.beginPath();\n ctx.arc(h.x, h.y, 3, 0, Math.PI * 2);\n ctx.fillStyle = '#ffffff';\n ctx.fill();\n ctx.strokeStyle = '#1e293b';\n ctx.stroke();\n }\n const r = scr(t.reference);\n ctx.beginPath();\n ctx.arc(r.x, r.y, 5, 0, Math.PI * 2);\n ctx.moveTo(r.x - 8, r.y);\n ctx.lineTo(r.x + 8, r.y);\n ctx.moveTo(r.x, r.y - 8);\n ctx.lineTo(r.x, r.y + 8);\n ctx.lineWidth = 3;\n ctx.strokeStyle = 'rgba(0,0,0,0.5)';\n ctx.stroke();\n ctx.lineWidth = 1;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // 5. Draw Crop Grid Overlay if crop overlay is requested\n if (isActivelyCropping) {\n const activeCrop =\n options.draftCrop !== undefined && options.draftCrop !== null\n ? options.draftCrop\n : (crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight });\n this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));\n }\n }\n\n /**\n * Draws `region` of the source image with rotation/flip applied, then pixel adjustments\n * and the depth mask (which receives the identical transform so it stays aligned).\n */\n private async renderBaseImage(\n img: CanvasImageSource,\n naturalWidth: number,\n naturalHeight: number,\n region: CropBox,\n transform: ImageTransform,\n state: Pick<ImageEditorState, 'adjustments' | 'depthMask'>,\n onPixels?: (pixels: ImageData) => void\n ): Promise<HTMLCanvasElement> {\n const { adjustments, depthMask } = state;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const baseCanvas = this.createCanvas(baseW, baseH);\n const baseCtx = baseCanvas.getContext('2d');\n if (!baseCtx) return baseCanvas;\n\n const projection = getBaseProjection(region, transform, baseW, baseH);\n\n baseCtx.save();\n applyImageProjectionToContext(baseCtx, projection);\n baseCtx.drawImage(\n img,\n region.x, region.y, region.width, region.height,\n region.x, region.y, region.width, region.height\n );\n baseCtx.restore();\n\n const imageData = baseCtx.getImageData(0, 0, baseW, baseH);\n if (imageData) {\n // Layer masks live in natural pixels: resample them through the same projection.\n const layerMasks = buildLayerMaskPlanes(adjustments.layers, naturalWidth, naturalHeight, projection, baseW, baseH);\n applyAdjustments(imageData, adjustments, layerMasks ? { layerMasks } : {});\n\n if (depthMask.enabled && depthMask.maskSource) {\n try {\n const maskImg = await this.imageCache.get(depthMask.maskSource);\n const maskSize = getSourceSize(maskImg, naturalWidth, naturalHeight);\n const sx = maskSize.width / naturalWidth;\n const sy = maskSize.height / naturalHeight;\n const maskCanvas = this.createCanvas(baseW, baseH);\n const maskCtx = maskCanvas.getContext('2d');\n if (maskCtx) {\n // The depth map covers the whole source image; scale it to natural pixels and\n // apply the exact same rotate/flip/crop projection as the image itself.\n maskCtx.save();\n applyImageProjectionToContext(maskCtx, projection);\n maskCtx.drawImage(\n maskImg,\n region.x * sx, region.y * sy, region.width * sx, region.height * sy,\n region.x, region.y, region.width, region.height\n );\n maskCtx.restore();\n const maskData = maskCtx.getImageData(0, 0, baseW, baseH);\n if (maskData) {\n applyDepthMask(imageData, maskData, depthMask);\n }\n }\n } catch {\n // Mask failed to load; continue without crashing\n }\n }\n\n onPixels?.(imageData);\n baseCtx.putImageData(imageData, 0, 0);\n }\n\n return baseCanvas;\n }\n\n // --- Layered documents -------------------------------------------------------\n\n /** Which base path renders `state` (see `needsLegacyBase`). */\n private chooseRoute(state: ImageEditorState, region: CropBox, naturalWidth: number, naturalHeight: number): 'legacy' | 'layers' {\n let route: 'legacy' | 'layers';\n if (!state.layers?.length) route = 'legacy';\n else if (this.baseRoute === 'layers') route = 'layers';\n else {\n const stack = { layers: state.layers, outputOpacity: state.outputOpacity ?? 1, legacyOutputBlur: state.legacyOutputBlur };\n const shaped = this.baseRoute === 'legacy' && isLegacyStack(stack);\n route =\n shaped ||\n needsLegacyBase({\n stack,\n region,\n canvas: { width: naturalWidth, height: naturalHeight },\n transform: state.transform,\n sourceOpaque: this.layerSources?.sourceIsOpaque() ?? true,\n })\n ? 'legacy'\n : 'layers';\n }\n this.lastBaseRoute = route;\n return route;\n }\n\n /** Compositor sources for a `width × height` canvas: layer pixels, depth maps and annotation rasterizing. */\n public compositorSources(width: number, height: number): CompositorSources {\n const host = this.layerSources;\n return {\n width,\n height,\n surfaceOf: host ? (l: PixelLayer) => host.surfaceOf(l) : undefined,\n depthMap: (source) => {\n if (!source.enabled || !source.maskSource) return null;\n return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;\n },\n rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),\n version: `${host?.version() ?? ''}:${this.assetVersion}`,\n globalLight: host?.globalLight?.(),\n pattern: host?.pattern ? (id) => host.pattern!(id) : undefined,\n };\n }\n\n /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */\n /** Loads depth maps and annotation images the compositor reads synchronously. */\n public async prepareLayerAssets(layers: readonly ImageLayer[], width: number, height: number): Promise<void> {\n for (const l of flattenLayers(layers)) {\n const source = l.mask?.source;\n if (source?.enabled && source.maskSource) {\n const key = `${source.maskSource}|${width}x${height}`;\n if (this.depthMaps.has(key)) continue;\n try {\n const maskImg = await this.imageCache.get(source.maskSource);\n const canvas = this.createCanvas(width, height);\n const mctx = canvas.getContext('2d');\n const data = mctx ? (mctx.drawImage(maskImg, 0, 0, width, height), mctx.getImageData?.(0, 0, width, height)) : null;\n if (data) {\n this.depthMaps.set(key, { width, height, data: data.data });\n this.assetVersion++;\n }\n } catch {\n // A depth map that fails to load reveals the layer, as before.\n }\n }\n if (l.type === 'annotation' && l.annotation.type === 'image' && !this.imageCache.peek(l.annotation.src)) {\n try {\n await this.imageCache.get(l.annotation.src);\n this.assetVersion++;\n } catch {\n // Skipped, as a watermark that fails to load always was.\n }\n }\n }\n }\n\n /** Whether annotation layers can be rasterized here (a 2D canvas with pixel access). */\n public canRasterize(): boolean {\n const ctx = this.createCanvas(1, 1).getContext('2d');\n return Boolean(ctx && typeof ctx.getImageData === 'function');\n }\n\n /** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */\n private rasterizeAnnotation(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null {\n if (!this.scratch) this.scratch = this.createCanvas(rect.width, rect.height);\n const canvas = this.scratch;\n if (canvas.width !== rect.width || canvas.height !== rect.height) {\n canvas.width = rect.width;\n canvas.height = rect.height;\n }\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n try {\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, rect.width, rect.height);\n ctx.setTransform(1, 0, 0, 1, -rect.x, -rect.y);\n const img = annotation.type === 'image' ? this.imageCache.peek(annotation.src) ?? null : null;\n this.drawAnnotationShape(ctx, annotation, img);\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n return new Uint8ClampedArray(ctx.getImageData(0, 0, rect.width, rect.height).data);\n } catch {\n return null;\n }\n }\n\n private putTile(ctx: CanvasRenderingContext2D, data: Uint8ClampedArray, tx: number, ty: number, rect: CropBox): void {\n const ox = tx * TILE - rect.x;\n const oy = ty * TILE - rect.y;\n const x0 = Math.max(0, -ox);\n const y0 = Math.max(0, -oy);\n const x1 = Math.min(TILE, rect.width - ox);\n const y1 = Math.min(TILE, rect.height - oy);\n if (x1 <= x0 || y1 <= y0) return;\n const img = ctx.createImageData(TILE, TILE);\n img.data.set(data);\n ctx.putImageData(img, ox, oy, x0, y0, x1 - x0, y1 - y0);\n }\n\n /**\n * Brings the viewport's composite canvas (document space, covering `region`) up to date,\n * uploading only the tiles that changed since the last render.\n */\n private updateComposite(\n layers: readonly ImageLayer[],\n region: CropBox,\n width: number,\n height: number,\n options: Pick<RenderOptions, 'dirty' | 'checkpointLayerId'> = {}\n ): CompositeCanvas | null {\n const rect = coveringRect(region);\n let comp = this.comp;\n let fresh = false;\n if (!comp || comp.rect.x !== rect.x || comp.rect.y !== rect.y || comp.rect.width !== rect.width || comp.rect.height !== rect.height) {\n const canvas = this.createCanvas(rect.width, rect.height);\n comp = this.comp = new CompositeCanvas(canvas, rect);\n fresh = true;\n }\n const ctx = comp.canvas.getContext('2d');\n if (!ctx || typeof ctx.createImageData !== 'function') return null;\n let changed = false;\n this.compositor.compositeTiles(layers, rect, this.compositorSources(width, height), (tx, ty, data) => {\n const key = tileKey(tx, ty);\n if (comp!.tiles.get(key) === data) return;\n comp!.tiles.set(key, data);\n comp!.histograms.delete(key);\n this.putTile(ctx, data, tx, ty, rect);\n changed = true;\n }, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? undefined : options.dirty });\n if (changed) comp.version++;\n comp.region = region;\n return comp;\n }\n\n /**\n * The base canvas of a layered document: the composite drawn through the base projection (crop,\n * rotation, flips at scale 1), then output opacity and the migrated blur, which run in the output\n * frame. Cached until the composite or those settings change.\n */\n private layeredBase(\n comp: CompositeCanvas | null,\n state: ImageEditorState,\n region: CropBox,\n transform: ImageTransform\n ): { canvas: HTMLCanvasElement; histogram: ImageHistogram | null } | null {\n if (!comp) return null;\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const key = `${comp.version}|${region.x},${region.y},${region.width},${region.height}|${normalizeAngle(transform.rotate)},${transform.flipH},${transform.flipV}|${opacity}|${blur}`;\n if (this.cachedPostCanvas && this.cachedPostKey === key) return { canvas: this.cachedPostCanvas, histogram: this.cachedPostHistogram };\n const canvas =\n this.cachedPostCanvas && this.cachedPostCanvas.width === baseW && this.cachedPostCanvas.height === baseH\n ? this.cachedPostCanvas\n : this.createCanvas(baseW, baseH);\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n ctx.clearRect(0, 0, baseW, baseH);\n ctx.save();\n applyImageProjectionToContext(ctx, baseProjection(region, transform, baseW, baseH));\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n ctx.drawImage(comp.canvas, comp.rect.x, comp.rect.y);\n ctx.restore();\n const histogram = this.applyPost(ctx, baseW, baseH, state);\n this.cachedPostCanvas = canvas;\n this.cachedPostKey = key;\n this.cachedPostHistogram = histogram;\n return { canvas, histogram };\n }\n\n /** Output opacity then the migrated blur, in place on a base canvas; returns its histogram. */\n private applyPost(ctx: CanvasRenderingContext2D, w: number, h: number, state: ImageEditorState): ImageHistogram | null {\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n if (!(opacity < 1) && !(blur > 0)) return null;\n const data = ctx.getImageData?.(0, 0, w, h);\n if (!data) return null;\n applyOutputStage(data, opacity, blur);\n ctx.putImageData(data, 0, 0);\n return computeHistogram(data);\n }\n\n private publishHistogram(histogram: ImageHistogram | null): void {\n if (histogram === this.previewHistogram) return;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n private quickMaskCache = new WeakMap<SelectionMask, HTMLCanvasElement>();\n private outlineCache = new WeakMap<SelectionMask, Float32Array>();\n\n /** Rubylith over the unselected area, drawn in natural image space. */\n private drawQuickMask(ctx: CanvasRenderingContext2D, selection: SelectionMask): void {\n let canvas = this.quickMaskCache.get(selection);\n if (!canvas) {\n canvas = this.createCanvas(selection.width, selection.height);\n const mctx = canvas.getContext('2d');\n if (!mctx || typeof mctx.createImageData !== 'function') return;\n const img = mctx.createImageData(selection.width, selection.height);\n img.data.set(quickMaskPixels(selection));\n mctx.putImageData(img, 0, 0);\n this.quickMaskCache.set(selection, canvas);\n }\n ctx.drawImage(canvas, 0, 0, selection.width, selection.height);\n }\n\n /** Marching ants: a white line under a black dashed one, one screen pixel wide. */\n private drawAnts(ctx: CanvasRenderingContext2D, selection: SelectionMask, scale: number, offset: number): void {\n let segs = this.outlineCache.get(selection);\n if (!segs) {\n segs = selectionOutline(selection);\n this.outlineCache.set(selection, segs);\n }\n if (!segs.length) return;\n const px = 1 / (scale || 1);\n ctx.beginPath();\n for (let i = 0; i < segs.length; i += 4) {\n ctx.moveTo(segs[i], segs[i + 1]);\n ctx.lineTo(segs[i + 2], segs[i + 3]);\n }\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') {\n ctx.setLineDash([4 * px, 4 * px]);\n ctx.lineDashOffset = -offset * px;\n }\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private drawDraftPath(ctx: CanvasRenderingContext2D, points: ImagePoint[], scale: number): void {\n const px = 1 / (scale || 1);\n ctx.beginPath();\n points.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)));\n ctx.closePath();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([4 * px, 4 * px]);\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private getSourceId(img: CanvasImageSource): number {\n const key = img as unknown as object;\n let id = this.sourceIds.get(key);\n if (id === undefined) {\n id = ++this.nextSourceId;\n this.sourceIds.set(key, id);\n }\n return id;\n }\n\n /**\n * Draws an individual annotation onto the canvas.\n */\n public async drawAnnotation(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n const blend = normalizeBlendMode(ann.blend);\n if (blend === 'normal') {\n await this.drawAnnotationContent(ctx, ann);\n return;\n }\n const op = canvasCompositeOperation(blend);\n if (op) {\n ctx.save();\n ctx.globalCompositeOperation = op;\n await this.drawAnnotationContent(ctx, ann);\n ctx.restore();\n return;\n }\n if (!(await this.drawAnnotationBlended(ctx, ann))) {\n // No pixel access (e.g. a tainted or headless canvas): draw it normally.\n await this.drawAnnotationContent(ctx, ann);\n }\n }\n\n /**\n * Scalar fallback for blend modes the canvas cannot composite (Photoshop soft light, linear /\n * vivid / pin light, hard mix, subtract, divide, darker / lighter colour, linear burn / dodge):\n * draws the annotation on a same-size layer with the same transform, blends it into a copy of\n * the target's pixels with `blendImageData`, and copies the result back inside the current clip.\n */\n private async drawAnnotationBlended(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<boolean> {\n const canvas = ctx.canvas as HTMLCanvasElement | undefined;\n if (!canvas || typeof ctx.getTransform !== 'function' || typeof ctx.getImageData !== 'function') return false;\n const w = canvas.width;\n const h = canvas.height;\n const layer = this.createCanvas(w, h);\n const lctx = layer.getContext('2d');\n if (!lctx) return false;\n try {\n lctx.setTransform(ctx.getTransform());\n lctx.globalAlpha = ctx.globalAlpha;\n await this.drawAnnotationContent(lctx, ann);\n const backdrop = ctx.getImageData(0, 0, w, h);\n const source = lctx.getImageData(0, 0, w, h);\n if (!backdrop || !source) return false;\n blendImageData(backdrop, source, normalizeBlendMode(ann.blend));\n lctx.setTransform(1, 0, 0, 1, 0, 0);\n lctx.putImageData(backdrop, 0, 0);\n } catch {\n return false;\n }\n ctx.save();\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.globalAlpha = 1;\n ctx.globalCompositeOperation = 'copy';\n ctx.drawImage(layer, 0, 0);\n ctx.restore();\n return true;\n }\n\n private async drawAnnotationContent(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n let img: CanvasImageSource | null = null;\n if (ann.type === 'image') {\n try {\n img = await this.imageCache.get(ann.src);\n } catch {\n // Skip if watermark image failed to load\n }\n }\n this.drawAnnotationShape(ctx, ann, img);\n }\n\n /** Draws an annotation synchronously; an image annotation draws `img` (skipped when `null`). */\n public drawAnnotationShape(ctx: CanvasRenderingContext2D, ann: Annotation, img: CanvasImageSource | null): void {\n ctx.save();\n if (ann.opacity !== undefined) {\n ctx.globalAlpha = ann.opacity;\n }\n\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n const pen = ann as PenAnnotation;\n if (pen.points.length > 0) {\n ctx.beginPath();\n ctx.strokeStyle = pen.strokeColor;\n ctx.lineWidth = pen.strokeWidth;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n ctx.moveTo(pen.points[0].x, pen.points[0].y);\n for (let i = 1; i < pen.points.length; i++) {\n ctx.lineTo(pen.points[i].x, pen.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n\n case 'rect': {\n const rect = ann as RectAnnotation;\n ctx.strokeStyle = rect.strokeColor;\n ctx.lineWidth = rect.strokeWidth;\n if (rect.fillColor) {\n ctx.fillStyle = rect.fillColor;\n ctx.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n if (rect.strokeWidth > 0) {\n ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);\n }\n break;\n }\n\n case 'circle': {\n const circle = ann as CircleAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = circle.strokeColor;\n ctx.lineWidth = circle.strokeWidth;\n ctx.ellipse(circle.x, circle.y, Math.abs(circle.radiusX), Math.abs(circle.radiusY), 0, 0, 2 * Math.PI);\n if (circle.fillColor) {\n ctx.fillStyle = circle.fillColor;\n ctx.fill();\n }\n if (circle.strokeWidth > 0) {\n ctx.stroke();\n }\n break;\n }\n\n case 'arrow': {\n const arrow = ann as ArrowAnnotation;\n ctx.strokeStyle = arrow.strokeColor;\n ctx.fillStyle = arrow.strokeColor;\n ctx.lineWidth = arrow.strokeWidth;\n\n // Line body\n ctx.beginPath();\n ctx.moveTo(arrow.x, arrow.y);\n ctx.lineTo(arrow.endX, arrow.endY);\n ctx.stroke();\n\n // Arrow head\n const angle = Math.atan2(arrow.endY - arrow.y, arrow.endX - arrow.x);\n const headLen = Math.max(12, arrow.strokeWidth * 3);\n ctx.beginPath();\n ctx.moveTo(arrow.endX, arrow.endY);\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle - Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle - Math.PI / 6)\n );\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle + Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle + Math.PI / 6)\n );\n ctx.closePath();\n ctx.fill();\n break;\n }\n\n case 'line': {\n const line = ann as LineAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = line.strokeColor;\n ctx.lineWidth = line.strokeWidth;\n ctx.moveTo(line.x, line.y);\n ctx.lineTo(line.endX, line.endY);\n ctx.stroke();\n break;\n }\n\n case 'text': {\n const textAnn = ann as TextAnnotation;\n ctx.font = `${textAnn.fontWeight || 'normal'} ${textAnn.fontSize}px ${textAnn.fontFamily || 'sans-serif'}`;\n ctx.fillStyle = textAnn.color;\n ctx.textBaseline = 'top';\n ctx.fillText(textAnn.text, textAnn.x, textAnn.y);\n break;\n }\n\n case 'image': {\n const imgAnn = ann as ImageAnnotation;\n if (img) ctx.drawImage(img, imgAnn.x, imgAnn.y, imgAnn.width, imgAnn.height);\n break;\n }\n }\n\n ctx.restore();\n }\n\n /**\n * Draws selection box around active annotation.\n */\n private drawAnnotationSelection(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.strokeStyle = '#3b82f6'; // Tailwind blue-500\n ctx.lineWidth = 1.5;\n ctx.setLineDash([4, 4]);\n\n const bounds = getAnnotationBounds(ann);\n const pad = 4;\n ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);\n\n // Draw handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#3b82f6';\n ctx.setLineDash([]);\n const handleSize = 6;\n const corners = [\n { x: bounds.x - pad, y: bounds.y - pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y - pad },\n { x: bounds.x - pad, y: bounds.y + bounds.height + pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y + bounds.height + pad },\n ];\n for (const c of corners) {\n ctx.fillRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n /** @deprecated Use the standalone `getAnnotationBounds` from the geometry module. */\n public getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n return getAnnotationBounds(ann);\n }\n\n /**\n * Draws a rule-of-thirds crop grid overlay. All rectangles come from\n * `getViewportMetrics`, i.e. the crop box projected through rotation/flip.\n */\n private drawCropOverlay(ctx: CanvasRenderingContext2D, metrics: ViewportMetrics): void {\n const { imageRect, cropRect } = metrics;\n if (!cropRect) return;\n\n const imgL = imageRect.x;\n const imgT = imageRect.y;\n const imgR = imageRect.x + imageRect.width;\n const imgB = imageRect.y + imageRect.height;\n const startX = cropRect.x;\n const startY = cropRect.y;\n const cropBoxW = cropRect.width;\n const cropBoxH = cropRect.height;\n\n ctx.save();\n\n // Dark semi-transparent scrim outside crop box (within the image)\n ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';\n // Top\n ctx.fillRect(imgL, imgT, imageRect.width, Math.max(0, startY - imgT));\n // Bottom\n ctx.fillRect(imgL, startY + cropBoxH, imageRect.width, Math.max(0, imgB - (startY + cropBoxH)));\n // Left\n ctx.fillRect(imgL, startY, Math.max(0, startX - imgL), cropBoxH);\n // Right\n ctx.fillRect(startX + cropBoxW, startY, Math.max(0, imgR - (startX + cropBoxW)), cropBoxH);\n\n // Crop border\n ctx.strokeStyle = '#ffffff';\n ctx.lineWidth = 2;\n ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);\n\n // Rule of thirds lines\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';\n ctx.lineWidth = 1;\n ctx.beginPath();\n // Vertical lines\n ctx.moveTo(startX + cropBoxW / 3, startY);\n ctx.lineTo(startX + cropBoxW / 3, startY + cropBoxH);\n ctx.moveTo(startX + (cropBoxW * 2) / 3, startY);\n ctx.lineTo(startX + (cropBoxW * 2) / 3, startY + cropBoxH);\n // Horizontal lines\n ctx.moveTo(startX, startY + cropBoxH / 3);\n ctx.lineTo(startX + cropBoxW, startY + cropBoxH / 3);\n ctx.moveTo(startX, startY + (cropBoxH * 2) / 3);\n ctx.lineTo(startX + cropBoxW, startY + (cropBoxH * 2) / 3);\n ctx.stroke();\n\n // Corner and edge handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#0284c7';\n ctx.lineWidth = 1.5;\n const handleSize = 10;\n const handles = [\n // Corners\n { x: startX, y: startY },\n { x: startX + cropBoxW, y: startY },\n { x: startX, y: startY + cropBoxH },\n { x: startX + cropBoxW, y: startY + cropBoxH },\n // Edges\n { x: startX + cropBoxW / 2, y: startY },\n { x: startX + cropBoxW / 2, y: startY + cropBoxH },\n { x: startX, y: startY + cropBoxH / 2 },\n { x: startX + cropBoxW, y: startY + cropBoxH / 2 },\n ];\n for (const h of handles) {\n ctx.fillRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n private drawFittedImage(\n ctx: CanvasRenderingContext2D,\n img: CanvasImageSource,\n imgW: number,\n imgH: number,\n vpW: number,\n vpH: number,\n zoom: number,\n pan: { x: number; y: number },\n fit: ImageEditorFit = 'margin'\n ): void {\n ctx.save();\n ctx.translate(vpW / 2 + pan.x, vpH / 2 + pan.y);\n ctx.scale(zoom, zoom);\n const fitScale =\n fit === 'contain'\n ? Math.min(vpW / imgW, vpH / imgH)\n : Math.min((vpW * 0.85) / imgW, (vpH * 0.85) / imgH, 1);\n const renderW = imgW * fitScale;\n const renderH = imgH * fitScale;\n ctx.drawImage(img, -renderW / 2, -renderH / 2, renderW, renderH);\n ctx.restore();\n }\n\n /**\n * Performs an export render at exact pixel dimensions without zoom/pan viewport offsets.\n */\n public async renderExport(\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: ExportOptions = {}\n ): Promise<HTMLCanvasElement> {\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const { crop, transform, annotations } = state;\n\n const region = crop\n ? clampCropToImage(crop, naturalWidth, naturalHeight)\n : { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n\n const exportW = options.width || baseW;\n const exportH = options.height || baseH;\n\n const exportCanvas = this.createCanvas(exportW, exportH);\n const ctx = exportCanvas.getContext('2d');\n if (!ctx) return exportCanvas;\n\n let overlay: Annotation[] = annotations;\n let baseCanvas: HTMLCanvasElement;\n // A render of chosen layers (`layerIds`) always composites them; the legacy views describe the whole stack.\n if (!options.layerIds && this.chooseRoute(state, region, naturalWidth, naturalHeight) === 'legacy') {\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state);\n } else {\n const layers = options.includeAnnotations === false ? removeAnnotationLayers(state.layers) : state.layers;\n const split = splitOverlayAnnotations(layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const rect = coveringRect(region);\n const comp = this.createCanvas(rect.width, rect.height);\n const cctx = comp.getContext('2d');\n if (cctx && typeof cctx.createImageData === 'function') {\n this.exportCompositor.compositeTiles(split.composite, rect, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {\n this.putTile(cctx, data, tx, ty, rect);\n });\n }\n baseCanvas = this.createCanvas(baseW, baseH);\n const bctx = baseCanvas.getContext('2d');\n if (bctx) {\n bctx.save();\n applyImageProjectionToContext(bctx, baseProjection(region, transform, baseW, baseH));\n bctx.beginPath();\n bctx.rect(region.x, region.y, region.width, region.height);\n bctx.clip();\n bctx.drawImage(comp, rect.x, rect.y);\n bctx.restore();\n this.applyPost(bctx, baseW, baseH, state);\n }\n }\n\n // Scale to target export dimensions\n ctx.drawImage(baseCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);\n\n // Draw annotations (natural image space) through the same rotate/flip/crop projection\n if (options.includeAnnotations !== false && overlay.length > 0) {\n ctx.save();\n ctx.scale(exportW / baseW, exportH / baseH);\n applyImageProjectionToContext(ctx, getBaseProjection(region, transform, baseW, baseH));\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n }\n ctx.restore();\n }\n\n return exportCanvas;\n }\n\n /**\n * Generates a strictly binary mask canvas (white = area to regenerate, black = keep) in\n * natural image pixel space of the current source image — the same space the crop box\n * and annotations are stored in. Pair it with `toSourceDataURL()` for AI inpainting.\n */\n public async renderMask(\n state: ImageEditorState,\n options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }\n ): Promise<HTMLCanvasElement> {\n const naturalWidth = Math.max(1, Math.round(state.imageDimensions.width));\n const naturalHeight = Math.max(1, Math.round(state.imageDimensions.height));\n const maskCanvas = this.createCanvas(naturalWidth, naturalHeight);\n const ctx = maskCanvas.getContext('2d');\n if (!ctx) return maskCanvas;\n\n // Background: unmasked (black)\n ctx.fillStyle = MASK_BLACK;\n ctx.fillRect(0, 0, naturalWidth, naturalHeight);\n\n const useCrop = options?.useCrop ?? true;\n const useAnnotations = options?.useAnnotations ?? true;\n\n // Crop box mask (if active)\n if (useCrop && state.crop) {\n ctx.fillStyle = MASK_WHITE;\n ctx.fillRect(state.crop.x, state.crop.y, state.crop.width, state.crop.height);\n }\n\n // Annotation masks (drawn in solid white, never in annotation colours)\n if (useAnnotations && state.annotations.length > 0) {\n for (const ann of state.annotations) {\n this.drawAnnotationMask(ctx, ann);\n }\n }\n\n // Anti-aliased edges produce grey pixels: threshold to pure black/white.\n const imageData = ctx.getImageData?.(0, 0, naturalWidth, naturalHeight);\n // The active selection (same natural space) is added to the mask.\n const selection =\n (options?.useSelection ?? true) &&\n state.selection?.bounds &&\n state.selection.width === naturalWidth &&\n state.selection.height === naturalHeight\n ? state.selection\n : null;\n if (imageData) {\n const d = imageData.data;\n for (let i = 0; i < d.length; i += 4) {\n const v = d[i] >= 128 || (selection !== null && selection.data[i >> 2] >= 128) ? 255 : 0;\n d[i] = v;\n d[i + 1] = v;\n d[i + 2] = v;\n d[i + 3] = 255;\n }\n ctx.putImageData(imageData, 0, 0);\n }\n\n return maskCanvas;\n }\n\n private drawAnnotationMask(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.globalAlpha = 1;\n ctx.fillStyle = MASK_WHITE;\n ctx.strokeStyle = MASK_WHITE;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n if (ann.points.length > 0) {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.points[0].x, ann.points[0].y);\n for (let i = 1; i < ann.points.length; i++) {\n ctx.lineTo(ann.points[i].x, ann.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n case 'circle': {\n ctx.beginPath();\n ctx.ellipse(ann.x, ann.y, Math.abs(ann.radiusX), Math.abs(ann.radiusY), 0, 0, Math.PI * 2);\n ctx.fill();\n break;\n }\n case 'arrow':\n case 'line': {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.x, ann.y);\n ctx.lineTo(ann.endX, ann.endY);\n ctx.stroke();\n break;\n }\n case 'rect':\n case 'image':\n case 'text': {\n const b = getAnnotationBounds(ann);\n ctx.fillRect(b.x, b.y, b.width, b.height);\n break;\n }\n }\n ctx.restore();\n }\n}\n\nconst MASK_WHITE = '#ffffff';\nconst MASK_BLACK = '#000000';\n\nfunction getSourceSize(img: CanvasImageSource, fallbackW = 800, fallbackH = 600): { width: number; height: number } {\n const anyImg = img as any;\n return {\n width: anyImg.naturalWidth || anyImg.videoWidth || anyImg.width || fallbackW,\n height: anyImg.naturalHeight || anyImg.videoHeight || anyImg.height || fallbackH,\n };\n}\n\nfunction getBaseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/** What the renderer asks its host (the controller) about layer pixels. */\nexport interface LayerSourcesHost {\n surfaceOf(layer: PixelLayer): PixelSurface;\n sourceIsOpaque(): boolean;\n version(): string;\n /** The document's global light for layer effects. */\n globalLight?(): GlobalLight | undefined;\n /** A document pattern for pattern effects (built-in patterns are found without it). */\n pattern?(id: string): EffectPattern | null;\n}\n\n/** The viewport's composite canvas and what each of its tiles holds. */\nclass CompositeCanvas {\n readonly tiles = new Map<TileKey, Uint8ClampedArray>();\n readonly histograms = new Map<TileKey, ImageHistogram>();\n version = 0;\n region: CropBox;\n constructor(readonly canvas: HTMLCanvasElement, readonly rect: CropBox) {\n this.region = rect;\n }\n\n /** Histogram of the composite over the region (per-tile parts are cached). */\n histogram(): ImageHistogram | null {\n if (!this.tiles.size) return null;\n const r = coveringRect(this.region);\n const parts: ImageHistogram[] = [];\n for (const [key, data] of this.tiles) {\n let h = this.histograms.get(key);\n if (!h) {\n const tx = (key % 65536) - 32768;\n const ty = Math.floor(key / 65536) - 32768;\n const x0 = Math.max(r.x, tx * TILE);\n const y0 = Math.max(r.y, ty * TILE);\n const x1 = Math.min(r.x + r.width, tx * TILE + TILE);\n const y1 = Math.min(r.y + r.height, ty * TILE + TILE);\n h = emptyHistogram();\n if (x1 > x0 && y1 > y0) addToHistogram(h, data, TILE, { x: x0 - tx * TILE, y: y0 - ty * TILE, width: x1 - x0, height: y1 - y0 });\n this.histograms.set(key, h);\n }\n parts.push(h);\n }\n return sumHistograms(parts);\n }\n}\n\nfunction removeAnnotationLayers(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: removeAnnotationLayers(l.children) } : l);\n }\n return out;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox, ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type SelectionMask } from './mask';\n\nexport interface ShapeSelectionOptions {\n /** Partial coverage along the edge (default true). */\n antiAlias?: boolean;\n}\n\ninterface Edge {\n top: number;\n bottom: number;\n ax: number;\n ay: number;\n bx: number;\n by: number;\n}\n\n/**\n * Even-odd polygon coverage over a `width × height` image: exact horizontal coverage on 4\n * sub-rows per pixel row (1 without anti-aliasing, where a pixel is in when its centre is).\n * Each sub-row only tests the edges that cross it (an active edge list over edges sorted by\n * their top), so the cost is about pixels + edges × rows they span.\n */\nexport function polygonCoverage(\n points: readonly ImagePoint[],\n width: number,\n height: number,\n antiAlias = true\n): Float32Array {\n const m = new Float32Array(width * height);\n if (points.length < 3 || width <= 0 || height <= 0) return m;\n const sub = antiAlias ? 4 : 1;\n const edges: Edge[] = [];\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;\n if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n }\n edges.sort((p, q) => p.top - q.top);\n let next = 0;\n let active: Edge[] = [];\n const xs: number[] = [];\n for (let py = 0; py < height; py++) {\n const row = py * width;\n for (let s = 0; s < sub; s++) {\n const y = py + (s + 0.5) / sub;\n while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);\n if (active.length) active = active.filter((e) => e.bottom > y);\n if (!active.length) continue;\n xs.length = 0;\n for (const e of active) {\n if (e.top <= y) xs.push(e.ax + ((y - e.ay) / (e.by - e.ay)) * (e.bx - e.ax));\n }\n xs.sort((a, b) => a - b);\n for (let k = 0; k + 1 < xs.length; k += 2) {\n const x0 = Math.max(0, xs[k]);\n const x1 = Math.min(width, xs[k + 1]);\n if (x1 <= x0) continue;\n const c0 = Math.floor(x0);\n const c1 = Math.min(width, Math.ceil(x1));\n for (let cx = c0; cx < c1; cx++) {\n let cov: number;\n if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);\n else {\n const c = cx + 0.5;\n cov = c >= x0 && c < x1 ? 1 : 0;\n }\n if (cov > 0) m[row + cx] += cov / sub;\n }\n }\n }\n }\n for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;\n return m;\n}\n\nfunction toMask(width: number, height: number, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** Lasso / polygonal lasso: an even-odd polygon in natural image pixels. */\nexport function polygonSelection(\n width: number,\n height: number,\n points: readonly ImagePoint[],\n options: ShapeSelectionOptions = {}\n): SelectionMask {\n return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));\n}\n\n/** Normalises a rectangle with a negative width or height. */\nexport function normalizeRect(rect: CropBox): CropBox {\n const x = rect.width < 0 ? rect.x + rect.width : rect.x;\n const y = rect.height < 0 ? rect.y + rect.height : rect.y;\n return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };\n}\n\n/** Rectangular marquee. Fractional edges get partial coverage when anti-aliased. */\nexport function rectSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n const r = normalizeRect(rect);\n const pts = [\n { x: r.x, y: r.y },\n { x: r.x + r.width, y: r.y },\n { x: r.x + r.width, y: r.y + r.height },\n { x: r.x, y: r.y + r.height },\n ];\n return polygonSelection(width, height, pts, options);\n}\n\n/** The ellipse inscribed in `rect` as a polygon fine enough that its chords stay within 0.05 px. */\nexport function ellipsePoints(rect: CropBox): ImagePoint[] {\n const r = normalizeRect(rect);\n const rx = r.width / 2;\n const ry = r.height / 2;\n const cx = r.x + rx;\n const cy = r.y + ry;\n const rmax = Math.max(rx, ry, 0.5);\n // Sagitta of a chord with angle step θ is r(1 − cos(θ/2)) ≈ rθ²/8 ≤ 0.05.\n const step = Math.sqrt(0.4 / rmax);\n const n = Math.min(4096, Math.max(16, Math.ceil((2 * Math.PI) / step)));\n const pts: ImagePoint[] = [];\n for (let i = 0; i < n; i++) {\n const t = (i / n) * 2 * Math.PI;\n pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });\n }\n return pts;\n}\n\n/** Elliptical marquee inscribed in `rect`. */\nexport function ellipseSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n return polygonSelection(width, height, ellipsePoints(rect), options);\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\n\n/** How the magic wand measures colour difference. */\nexport type ColorDistance = 'rgb' | 'perceptual';\n\nexport interface MagicWandOptions {\n /**\n * 0..255. `rgb`: the largest per-channel difference in levels (alpha included), as\n * Photoshop's wand. `perceptual`: CIE76 ΔE*ab, with 255 ≙ ΔE 100. Default 32.\n */\n tolerance?: number;\n /** Only the 4-connected region around the seed (default true); otherwise every similar pixel. */\n contiguous?: boolean;\n /** Soft one-pixel fringe on the edge (default true). */\n antiAlias?: boolean;\n /** Default `rgb`. */\n distance?: ColorDistance;\n}\n\nfunction checkPixels(p: PixelBuffer): number {\n const n = p.width * p.height;\n if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {\n throw new Error('Invalid pixel buffer size');\n }\n if (p.data.length < n * 4) throw new Error('Pixel data is shorter than width × height × 4');\n return n;\n}\n\nconst SRGB_TO_LINEAR = (() => {\n const t = new Float32Array(256);\n for (let i = 0; i < 256; i++) {\n const v = i / 255;\n t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n }\n return t;\n})();\n\nconst labF = (t: number) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116);\n\n/** CIE Lab (D65) of an 8-bit sRGB colour. */\nexport function srgb8ToLab(r: number, g: number, b: number): [number, number, number] {\n const rl = SRGB_TO_LINEAR[r & 255];\n const gl = SRGB_TO_LINEAR[g & 255];\n const bl = SRGB_TO_LINEAR[b & 255];\n const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;\n const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;\n const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;\n const fx = labF(x);\n const fy = labF(y);\n const fz = labF(z);\n return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];\n}\n\n/** `similar(i)` for the wand: does pixel `i` match the seed colour within the tolerance? */\nfunction similarity(pixels: PixelBuffer, seed: number, tolerance: number, distance: ColorDistance): (i: number) => boolean {\n const d = pixels.data;\n const tr = d[seed * 4];\n const tg = d[seed * 4 + 1];\n const tb = d[seed * 4 + 2];\n const ta = d[seed * 4 + 3];\n const tol = Math.max(0, Math.floor(tolerance + 1e-3));\n if (distance === 'perceptual') {\n const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);\n const de = (Math.max(0, tolerance) * 100) / 255;\n const de2 = de * de + 1e-6;\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n // Copying a selection can leave RGB under zero alpha; hidden colours must not split a\n // transparent region.\n if (ta === 0 && a === 0) return true;\n if (Math.abs(a - ta) > tol) return false;\n const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);\n return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;\n };\n }\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n if (ta === 0 && a === 0) return true;\n return (\n Math.abs(d[p] - tr) <= tol &&\n Math.abs(d[p + 1] - tg) <= tol &&\n Math.abs(d[p + 2] - tb) <= tol &&\n Math.abs(a - ta) <= tol\n );\n };\n}\n\n/**\n * Softens hard edges: an edge pixel (one whose 3×3 neighbourhood differs from it) becomes\n * `½ + ½·avg` if selected, `½·avg` if not (neighbours outside the image count as the pixel).\n * Only the box `x0..x1 × y0..y1` (exclusive ends) is visited.\n */\nfunction antialiasMarks(marks: Uint8Array, w: number, h: number, x0: number, y0: number, x1: number, y1: number): Uint8Array {\n const out = marks.slice();\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const i = y * w + x;\n const v = marks[i];\n let sum = 0;\n let edge = false;\n for (let dy = -1; dy <= 1; dy++) {\n const ny = y + dy;\n for (let dx = -1; dx <= 1; dx++) {\n const nx = x + dx;\n const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];\n if (n !== v) edge = true;\n sum += n;\n }\n }\n if (edge) {\n const avg = sum / (9 * 255);\n out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);\n }\n }\n }\n return out;\n}\n\n/**\n * Magic wand: pixels similar to the seed pixel. Contiguous mode is a scanline span fill with an\n * explicit stack (fill a run, then scan the rows above and below it for new runs), so it is\n * linear in the pixels visited and never recurses. An empty selection when the seed is outside.\n */\nexport function magicWand(pixels: PixelBuffer, seed: ImagePoint, options: MagicWandOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h } = pixels;\n const sx = Math.floor(seed.x);\n const sy = Math.floor(seed.y);\n if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);\n const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? 'rgb');\n const marks = new Uint8Array(n);\n let bx0 = w;\n let by0 = h;\n let bx1 = -1;\n let by1 = -1;\n if (options.contiguous ?? true) {\n // Span fill. Each stack entry is a seed pixel index.\n const stack: number[] = [sy * w + sx];\n while (stack.length) {\n const idx = stack.pop()!;\n if (marks[idx]) continue;\n const y = (idx / w) | 0;\n const row = y * w;\n let l = idx - row;\n let r = l;\n while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;\n while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;\n marks.fill(255, row + l, row + r + 1);\n if (l < bx0) bx0 = l;\n if (r > bx1) bx1 = r;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n for (const ny of [y - 1, y + 1]) {\n if (ny < 0 || ny >= h) continue;\n const nrow = ny * w;\n let inRun = false;\n for (let nx = l; nx <= r; nx++) {\n const ok = !marks[nrow + nx] && similar(nrow + nx);\n if (ok && !inRun) stack.push(nrow + nx);\n inRun = ok;\n }\n }\n }\n } else {\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) {\n if (similar(row + x)) {\n marks[row + x] = 255;\n if (x < bx0) bx0 = x;\n if (x > bx1) bx1 = x;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n }\n }\n }\n }\n if (bx1 < 0) return createSelectionMask(w, h);\n if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);\n return createSelectionMask(\n w,\n h,\n antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))\n );\n}\n\n/** One Color Range sample: a colour (0..255 levels or `#rrggbb`) and, optionally, where it was picked. */\nexport interface ColorRangeSample {\n color: readonly [number, number, number] | string;\n at?: ImagePoint;\n}\n\nexport interface ColorRangeOptions {\n /** 0..200 levels (default 40): coverage falls off linearly to 0 at `fuzziness + 1` levels. */\n fuzziness?: number;\n /**\n * Localized Color Clusters: a sample with a position also fades with distance from it,\n * reaching 0 at this many pixels.\n */\n localized?: number;\n}\n\nfunction sampleColor(c: ColorRangeSample['color']): [number, number, number] {\n if (typeof c === 'string') {\n const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());\n if (!m) throw new Error(`Invalid colour \"${c}\"`);\n const v = parseInt(m[1], 16);\n return [(v >> 16) & 255, (v >> 8) & 255, v & 255];\n }\n return [c[0], c[1], c[2]];\n}\n\n/**\n * Select › Color Range › Sampled Colors: each pixel takes its best match over the samples; a\n * match falls off linearly with the largest per-channel difference, `1 − d / (fuzziness + 1)`.\n * Transparent pixels are never selected.\n */\nexport function colorRange(pixels: PixelBuffer, samples: readonly ColorRangeSample[], options: ColorRangeOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h, data } = pixels;\n const f = Math.max(0, options.fuzziness ?? 40) + 1;\n const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));\n const radius = options.localized !== undefined ? Math.max(1, options.localized) : undefined;\n const out = new Uint8Array(n);\n if (!parsed.length) return createSelectionMask(w, h, out);\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (data[p + 3] <= 0) continue;\n const x = i % w;\n const y = (i - x) / w;\n let best = 0;\n for (const s of parsed) {\n const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));\n let k = Math.min(1, Math.max(0, 1 - d / f));\n if (k > 0 && radius !== undefined && s.at) {\n const dist = Math.hypot(x - s.at.x, y - s.at.y);\n k *= Math.min(1, Math.max(0, 1 - dist / radius));\n }\n if (k > best) best = k;\n }\n out[i] = coverageToByte(best);\n }\n return createSelectionMask(w, h, out);\n}\n\nexport type ToneRangePreset = 'shadows' | 'midtones' | 'highlights';\n\n/** Luma bounds (0..255, Rec. 601) of the tonal presets. */\nexport const TONE_RANGE_PRESETS: Record<ToneRangePreset, [number, number]> = {\n shadows: [0, 65],\n midtones: [105, 150],\n highlights: [190, 255],\n};\n\n/**\n * Select › Color Range › Shadows / Midtones / Highlights: pixels whose luma lies in `lo..hi` are\n * selected; outside, selection falls off linearly to 0 over `falloff` levels.\n */\nexport function toneRange(\n pixels: PixelBuffer,\n range: ToneRangePreset | readonly [number, number],\n falloff = 20\n): SelectionMask {\n const n = checkPixels(pixels);\n const [lo, hi] = typeof range === 'string' ? TONE_RANGE_PRESETS[range] : range;\n const ramp = Math.max(1e-3, falloff);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));\n const outside = l < lo ? lo - l : l > hi ? l - hi : 0;\n out[i] = coverageToByte(1 - outside / ramp);\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n\n/**\n * Select › Color Range › Reds … Magentas (`center` 0, 60, …, 300°): closeness of hue (full at\n * the centre, none 60° away) × chroma × (2 − max), so neutrals are never selected.\n */\nexport function hueRange(pixels: PixelBuffer, center: number): SelectionMask {\n const n = checkPixels(pixels);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const r = d[p] / 255;\n const g = d[p + 1] / 255;\n const b = d[p + 2] / 255;\n const mx = Math.max(r, g, b);\n const mn = Math.min(r, g, b);\n const chroma = mx - mn;\n if (chroma <= 0) continue;\n let hue: number;\n if (mx === r) hue = (((g - b) / chroma) % 6 + 6) % 6;\n else if (mx === g) hue = (b - r) / chroma + 2;\n else hue = (r - g) / chroma + 4;\n hue *= 60;\n let dh = (((hue - center) % 360) + 360) % 360;\n dh = Math.min(dh, 360 - dh);\n out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n","// Portions ported from photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\nimport { distanceTransform, gaussianBlur, greyMorphology, shiftCoverage } from './modify';\n\n/**\n * Refine Edge / Select and Mask parameters (Photoshop's units). Refinement is edge-aware: within\n * `radius` pixels of the mask's edge the coverage is re-estimated from the image with a colour\n * guided filter (He et al.), so a coarse or misaligned mask (e.g. an AI segmentation) follows\n * the real edges of the image.\n */\nexport interface RefineEdgeParams {\n /** Edge-detection band radius in pixels (0 = no edge detection). */\n radius?: number;\n /** Narrow the band where the image edge is sharp (hair stays soft, hard edges stay hard). */\n smartRadius?: boolean;\n /** 0..100: rounds jagged edges. */\n smooth?: number;\n /** Pixels of Gaussian softening. */\n feather?: number;\n /** 0..100: hardens soft edges. */\n contrast?: number;\n /** −100..100: moves the edge out (positive) or in; 100 % is `max(radius, 4)` pixels. */\n shiftEdge?: number;\n}\n\nexport const DEFAULT_REFINE_EDGE_PARAMS: Required<RefineEdgeParams> = {\n radius: 0,\n smartRadius: false,\n smooth: 0,\n feather: 0,\n contrast: 0,\n shiftEdge: 0,\n};\n\n/** Guided-filter regulariser (colour guide, samples in 0..1). */\nexport const GUIDED_FILTER_EPS = 1e-4;\n\nconst fin = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\n/** Clamped, complete parameters. */\nexport function normalizeRefineEdgeParams(p: RefineEdgeParams = {}): Required<RefineEdgeParams> {\n return {\n radius: Math.min(250, Math.max(0, fin(p.radius, 0))),\n smartRadius: p.smartRadius === true,\n smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),\n feather: Math.min(1000, Math.max(0, fin(p.feather, 0))),\n contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),\n shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0))),\n };\n}\n\nconst gfRadius = (p: Required<RefineEdgeParams>) => Math.min(64, Math.max(1, Math.ceil(p.radius)));\nconst smoothSigma = (p: Required<RefineEdgeParams>) => (p.smooth / 100) * 4;\nconst featherSigma = (p: Required<RefineEdgeParams>) => p.feather / 2;\nfunction shiftAmount(p: Required<RefineEdgeParams>): number {\n const px = (p.shiftEdge / 100) * Math.max(p.radius, 4);\n return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;\n}\n\n/** Pixels around the mask's content that refinement can read or write. */\nexport function refineEdgeHalo(params: RefineEdgeParams = {}): number {\n const p = normalizeRefineEdgeParams(params);\n const g = gfRadius(p);\n const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;\n return (\n edge +\n Math.ceil(3 * smoothSigma(p)) +\n Math.ceil(3 * featherSigma(p)) +\n Math.ceil(Math.abs(shiftAmount(p))) +\n 2\n );\n}\n\n/** Box mean of radius `r` (window clipped to the image, divided by its pixel count). O(1) per pixel. */\nexport function boxMean(src: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const tmp = new Float32Array(w * h);\n const pre = new Float64Array(Math.max(w, h) + 1);\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];\n for (let x = 0; x < w; x++) {\n const lo = Math.max(0, x - r);\n const hi = Math.min(w, x + r + 1);\n tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n const out = new Float32Array(w * h);\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];\n for (let y = 0; y < h; y++) {\n const lo = Math.max(0, y - r);\n const hi = Math.min(h, y + r + 1);\n out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n return out;\n}\n\n/**\n * Guided filter with a colour guide (He et al., Eq. 19–21: `a_k = (Σ_k + εU)⁻¹ cov_k(I, p)`):\n * filters `p` so that it is locally a linear function of the guide's colour, which makes the\n * output's edges follow the guide's edges. `guide` is three planes of 0..1 values.\n */\nexport function guidedFilterColor(\n guide: readonly [Float32Array, Float32Array, Float32Array],\n p: Float32Array,\n w: number,\n h: number,\n r: number,\n eps = GUIDED_FILTER_EPS\n): Float32Array {\n const n = w * h;\n const [c0, c1, c2] = guide;\n const prod = (a: ArrayLike<number>, b: ArrayLike<number>) => {\n const o = new Float32Array(n);\n for (let i = 0; i < n; i++) o[i] = a[i] * b[i];\n return o;\n };\n const m0 = boxMean(c0, w, h, r);\n const m1 = boxMean(c1, w, h, r);\n const m2 = boxMean(c2, w, h, r);\n const mp = boxMean(p, w, h, r);\n const mip0 = boxMean(prod(c0, p), w, h, r);\n const mip1 = boxMean(prod(c1, p), w, h, r);\n const mip2 = boxMean(prod(c2, p), w, h, r);\n const s00 = boxMean(prod(c0, c0), w, h, r);\n const s01 = boxMean(prod(c0, c1), w, h, r);\n const s02 = boxMean(prod(c0, c2), w, h, r);\n const s11 = boxMean(prod(c1, c1), w, h, r);\n const s12 = boxMean(prod(c1, c2), w, h, r);\n const s22 = boxMean(prod(c2, c2), w, h, r);\n const a0 = new Float32Array(n);\n const a1 = new Float32Array(n);\n const a2 = new Float32Array(n);\n const b = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];\n const rr = s00[i] - u0 * u0 + eps;\n const rg = s01[i] - u0 * u1;\n const rb = s02[i] - u0 * u2;\n const gg = s11[i] - u1 * u1 + eps;\n const gb = s12[i] - u1 * u2;\n const bb = s22[i] - u2 * u2 + eps;\n const v0 = mip0[i] - u0 * up;\n const v1 = mip1[i] - u1 * up;\n const v2 = mip2[i] - u2 * up;\n // Inverse of the symmetric 3×3 matrix.\n const i00 = gg * bb - gb * gb;\n const i01 = gb * rb - rg * bb;\n const i02 = rg * gb - gg * rb;\n const i11 = rr * bb - rb * rb;\n const i12 = rb * rg - rr * gb;\n const i22 = rr * gg - rg * rg;\n const det = rr * i00 + rg * i01 + rb * i02;\n let x0 = 0, x1 = 0, x2 = 0;\n if (Math.abs(det) >= 1e-30) {\n x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;\n x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;\n x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;\n }\n a0[i] = x0;\n a1[i] = x1;\n a2[i] = x2;\n b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;\n }\n const ma0 = boxMean(a0, w, h, r);\n const ma1 = boxMean(a1, w, h, r);\n const ma2 = boxMean(a2, w, h, r);\n const mb = boxMean(b, w, h, r);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];\n return out;\n}\n\n/**\n * Per-pixel transition width (pixels) of the guide's luminance: local range over local maximum\n * gradient in a window of radius `r` (a sharp step gives about 2, a ramp of width L about L).\n */\nexport function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array {\n const n = w * h;\n const y = new Float32Array(n);\n for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];\n const g = new Float32Array(n);\n for (let yy = 0; yy < h; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;\n const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;\n g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);\n }\n }\n const gm = greyMorphology(g, w, h, r, true);\n const hi = greyMorphology(y, w, h, r, true);\n const lo = greyMorphology(y, w, h, r, false);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);\n return out;\n}\n\n/**\n * Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when\n * `radius > 0`): edge-aware re-estimation in a band around the edge, then smooth, feather,\n * contrast and shift edge, in that order.\n */\nexport function refineCoverage(\n guide: readonly [Float32Array, Float32Array, Float32Array] | null,\n m: Float32Array,\n w: number,\n h: number,\n params: RefineEdgeParams = {}\n): Float32Array {\n const p = normalizeRefineEdgeParams(params);\n let a: Float32Array = Float32Array.from(m);\n if (p.radius > 0 && guide) {\n const n = w * h;\n const boundary = new Uint8Array(n);\n let any = false;\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = y * w + x;\n const v = m[i] >= 0.5;\n const diff =\n (x > 0 && m[i - 1] >= 0.5 !== v) ||\n (x + 1 < w && m[i + 1] >= 0.5 !== v) ||\n (y > 0 && m[i - w] >= 0.5 !== v) ||\n (y + 1 < h && m[i + w] >= 0.5 !== v);\n if (diff) {\n boundary[i] = 1;\n any = true;\n }\n }\n }\n if (any) {\n const dist = distanceTransform(boundary, w, h);\n const r = gfRadius(p);\n const q = guidedFilterColor(guide, m, w, h, r);\n const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;\n for (let i = 0; i < n; i++) {\n const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;\n const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));\n if (wgt > 0) {\n const qi = Math.min(1, Math.max(0, q[i]));\n a[i] = m[i] + (qi - m[i]) * wgt;\n }\n }\n }\n }\n const ss = smoothSigma(p);\n if (ss >= 0.3) {\n a = gaussianBlur(a, w, h, ss, 'clamp');\n const gain = 1 + ss;\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n a = gaussianBlur(a, w, h, featherSigma(p), 'clamp');\n if (p.contrast > 0) {\n const gain = 1 / (1 - (0.99 * p.contrast) / 100);\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n const amount = shiftAmount(p);\n if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);\n return a;\n}\n\n/** Three 0..1 planes of a window of an RGBA buffer. */\nexport function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array] {\n const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };\n const n = box.width * box.height;\n const r = new Float32Array(n);\n const g = new Float32Array(n);\n const b = new Float32Array(n);\n const d = pixels.data;\n for (let y = 0; y < box.height; y++) {\n for (let x = 0; x < box.width; x++) {\n const i = y * box.width + x;\n const p = ((box.y + y) * pixels.width + box.x + x) * 4;\n r[i] = d[p] / 255;\n g[i] = d[p + 1] / 255;\n b[i] = d[p + 2] / 255;\n }\n }\n return [r, g, b];\n}\n\n/**\n * Refine Edge on a selection (or a coarse AI mask loaded as one) against `pixels` (the image,\n * same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small\n * selection on a large image stays cheap.\n */\nexport function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params: RefineEdgeParams = {}): SelectionMask {\n const p = normalizeRefineEdgeParams(params);\n if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {\n throw new Error('Refine Edge needs pixels the size of the selection');\n }\n if (!mask.bounds) return mask;\n const halo = refineEdgeHalo(p);\n // The guided filter reads 2r beyond the band; the window must hold that too.\n const x0 = Math.max(0, mask.bounds.x - halo);\n const y0 = Math.max(0, mask.bounds.y - halo);\n const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);\n const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);\n const ww = x1 - x0;\n const wh = y1 - y0;\n const m = new Float32Array(ww * wh);\n for (let y = 0; y < wh; y++) {\n const src = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;\n }\n const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;\n const refined = refineCoverage(guide, m, ww, wh, p);\n const out = new Uint8Array(mask.width * mask.height);\n for (let y = 0; y < wh; y++) {\n const dst = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);\n }\n return createSelectionMask(mask.width, mask.height, out);\n}\n","/**\n * Free Transform geometry for the image editor, as pure functions both the UI bindings and the\n * agent tools use: a transform is the frame being transformed (`box`, document pixels), where its\n * four corners land (`corners`, TL, TR, BR, BL) and an optional warp mesh over the box. Scale,\n * rotate, skew and flips keep the corners a parallelogram (an affine map); Distort and Perspective\n * move them freely (a projective map); Warp bends the box through a Bézier mesh before the corner\n * map.\n *\n * The maths (homographies, meshes, presets) is shared with the vector editor in `src/geom`.\n */\n\nimport {\n applyMatrix,\n invertMatrix,\n matrixAbout,\n multiplyMatrices,\n rotateMatrix,\n translateMatrix,\n type AffineMatrix,\n} from '../geom/affine';\nimport {\n affineFromHomography,\n applyHomography,\n boxQuad,\n homographyFromAffine,\n homographyKeepsQuad,\n rectToQuad,\n type Homography,\n} from '../geom/homography';\nimport { evalWarpMesh, identityWarpMesh, isValidWarpMesh, WARP_STYLES, warpPresetMesh, type WarpPreset } from '../geom/warp';\nimport type { Quad, Vec2, WarpMesh, WarpStyle } from '../geom/types';\nimport type { CropBox } from './types';\n\nexport type { Quad, Vec2, WarpMesh, WarpStyle, WarpPreset, AffineMatrix, Homography };\nexport { WARP_STYLES };\n\n/** Free Transform modes (Edit › Transform). Scale, rotate and skew stay affine. */\nexport type FreeTransformMode = 'scale' | 'rotate' | 'skew' | 'distort' | 'perspective' | 'warp';\n\nexport const FREE_TRANSFORM_MODES: readonly FreeTransformMode[] = ['scale', 'rotate', 'skew', 'distort', 'perspective', 'warp'];\n\n/** The nine reference-point positions of the options bar's locator, over the transformed box. */\nexport type TransformAnchor = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';\n\nexport const TRANSFORM_ANCHORS: readonly TransformAnchor[] = ['top-left', 'top', 'top-right', 'left', 'center', 'right', 'bottom-left', 'bottom', 'bottom-right'];\n\nconst ANCHOR_FRACTION: Record<TransformAnchor, [number, number]> = {\n 'top-left': [0, 0],\n top: [0.5, 0],\n 'top-right': [1, 0],\n left: [0, 0.5],\n center: [0.5, 0.5],\n right: [1, 0.5],\n 'bottom-left': [0, 1],\n bottom: [0.5, 1],\n 'bottom-right': [1, 1],\n};\n\nexport function isTransformAnchor(value: unknown): value is TransformAnchor {\n return typeof value === 'string' && value in ANCHOR_FRACTION;\n}\n\n/** A transform in progress: the frame, where its corners go and an optional warp over the frame. */\nexport interface FreeTransformGeometry {\n /** The frame being transformed, in document pixels (the content bounds when it began). */\n box: CropBox;\n /** Where the frame's corners land: top-left, top-right, bottom-right, bottom-left (document pixels). */\n corners: Quad;\n /**\n * A warp applied in the frame's own space before the corner map: its control points are in\n * document pixels of the untransformed frame (`identityWarpMesh(box)` is no warp).\n */\n warp?: WarpMesh | null;\n}\n\n/** The untransformed geometry of a box. */\nexport function identityTransform(box: CropBox): FreeTransformGeometry {\n return { box: { ...box }, corners: boxQuad(box) };\n}\n\n/** Where `box`'s corners go under the affine `m`. */\nexport function cornersFromMatrix(box: CropBox, m: AffineMatrix): Quad {\n const q = boxQuad(box);\n return [applyMatrix(m, q[0]), applyMatrix(m, q[1]), applyMatrix(m, q[2]), applyMatrix(m, q[3])];\n}\n\n/** The affine matrix taking `box` onto `corners` when they form a parallelogram, else `null`. */\nexport function matrixFromCorners(box: CropBox, corners: Quad, eps = 1e-6): AffineMatrix | null {\n const [tl, tr, br, bl] = corners;\n const size = 1 + Math.max(box.width, box.height);\n if (Math.abs(tl.x + br.x - tr.x - bl.x) > eps * size || Math.abs(tl.y + br.y - tr.y - bl.y) > eps * size) return null;\n const h = rectToQuad(box, corners);\n return h ? affineFromHomography(h, 1e-9) : null;\n}\n\n/** The projective map of the corner move (box → corners), or `null` when the corners are degenerate. */\nexport function transformHomography(geometry: FreeTransformGeometry): Homography | null {\n const h = rectToQuad(geometry.box, geometry.corners);\n if (!h) return null;\n // A quad folded through the horizon would wrap content round infinity.\n return homographyKeepsQuad(h, boxQuad(geometry.box)) ? h : null;\n}\n\n/** Whether a warp mesh changes anything (not the identity over the box). */\nexport function hasWarp(geometry: FreeTransformGeometry): boolean {\n const w = geometry.warp;\n if (!w) return false;\n const id = identityWarpMesh(geometry.box, w.us.length - 1, w.vs.length - 1);\n if (id.points.length !== w.points.length || id.us.some((u, i) => Math.abs(u - w.us[i]) > 1e-12) || id.vs.some((v, i) => Math.abs(v - w.vs[i]) > 1e-12)) return true;\n return w.points.some((p, i) => Math.abs(p.x - id.points[i].x) > 1e-6 || Math.abs(p.y - id.points[i].y) > 1e-6);\n}\n\n/** Whether the geometry changes nothing. */\nexport function isIdentityTransform(geometry: FreeTransformGeometry): boolean {\n if (hasWarp(geometry)) return false;\n const q = boxQuad(geometry.box);\n return geometry.corners.every((c, i) => Math.abs(c.x - q[i].x) < 1e-6 && Math.abs(c.y - q[i].y) < 1e-6);\n}\n\n/**\n * The forward map of the whole transform (document pixels of the source → document pixels of the\n * result): the warp over the box, then the corner map. `null` where it is undefined.\n */\nexport function transformMap(geometry: FreeTransformGeometry): ((p: Vec2) => Vec2 | null) | null {\n const h = transformHomography(geometry);\n if (!h) return null;\n const { box } = geometry;\n const warp = hasWarp(geometry) ? geometry.warp! : null;\n if (!warp) return (p) => applyHomography(h, p);\n return (p) => applyHomography(h, evalWarpMesh(warp, (p.x - box.x) / box.width, (p.y - box.y) / box.height));\n}\n\n/** A point of the transformed box at box fractions (`fx`, `fy`), warp included. */\nexport function transformedPoint(geometry: FreeTransformGeometry, fx: number, fy: number): Vec2 | null {\n const f = transformMap(geometry);\n const { box } = geometry;\n return f ? f({ x: box.x + fx * box.width, y: box.y + fy * box.height }) : null;\n}\n\n/** Where an anchor of the box is after the corner map (the warp ignored). */\nexport function anchorPoint(geometry: FreeTransformGeometry, anchor: TransformAnchor): Vec2 {\n const [fx, fy] = ANCHOR_FRACTION[anchor];\n const h = rectToQuad(geometry.box, geometry.corners);\n const p = { x: geometry.box.x + fx * geometry.box.width, y: geometry.box.y + fy * geometry.box.height };\n return (h && applyHomography(h, p)) ?? p;\n}\n\n/** Bounds of the transformed result's outline (document pixels, not rounded). */\nexport function transformedBox(geometry: FreeTransformGeometry): CropBox | null {\n const f = transformMap(geometry);\n if (!f) return null;\n const { box } = geometry;\n const pts: Vec2[] = [];\n const n = hasWarp(geometry) ? 16 : 1;\n for (let i = 0; i <= n; i++) {\n const t = i / n;\n for (const [fx, fy] of [[t, 0], [1, t], [1 - t, 1], [0, 1 - t]]) {\n const p = f({ x: box.x + fx * box.width, y: box.y + fy * box.height });\n if (!p) return null;\n pts.push(p);\n }\n }\n const xs = pts.map((p) => p.x);\n const ys = pts.map((p) => p.y);\n const x = Math.min(...xs);\n const y = Math.min(...ys);\n return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };\n}\n\n// ---------------------------------------------------------------------------\n// Numeric transforms (the options bar, and agents)\n// ---------------------------------------------------------------------------\n\n/** Free Transform as numbers, as Photoshop's options bar shows it. */\nexport interface TransformParams {\n /** Width in percent of the frame (default 100; negative flips). */\n scaleX?: number;\n /** Height in percent of the frame (default 100; negative flips). */\n scaleY?: number;\n /** Rotation in degrees, clockwise (default 0). */\n rotate?: number;\n /** Horizontal skew in degrees: vertical edges lean right for positive angles (default 0). */\n skewX?: number;\n /** Vertical skew in degrees: horizontal edges lean down for positive angles (default 0). */\n skewY?: number;\n /** Move after scaling and rotating, in document pixels (default 0). */\n dx?: number;\n dy?: number;\n /** The fixed point of scale, skew and rotation: an anchor of the frame or a document point (default the centre). */\n reference?: TransformAnchor | Vec2;\n}\n\n/** The reference point of params over a box (default its centre). */\nexport function referencePoint(box: CropBox, reference: TransformParams['reference']): Vec2 {\n if (reference && typeof reference === 'object') return { x: reference.x, y: reference.y };\n const [fx, fy] = ANCHOR_FRACTION[reference ?? 'center'];\n return { x: box.x + fx * box.width, y: box.y + fy * box.height };\n}\n\n/**\n * The affine matrix of `params` over `box`: about the reference point, skew, then scale, then\n * rotate; then the move.\n */\nexport function matrixFromParams(box: CropBox, params: TransformParams): AffineMatrix {\n const sx = (params.scaleX ?? 100) / 100;\n const sy = (params.scaleY ?? 100) / 100;\n const tx = Math.tan(((params.skewX ?? 0) * Math.PI) / 180);\n const ty = Math.tan(((params.skewY ?? 0) * Math.PI) / 180);\n const ref = referencePoint(box, params.reference);\n const linear = multiplyMatrices(multiplyMatrices([1, ty, tx, 1, 0, 0], [sx, 0, 0, sy, 0, 0]), rotateMatrix(params.rotate ?? 0));\n return multiplyMatrices(matrixAbout(linear, ref), translateMatrix(params.dx ?? 0, params.dy ?? 0));\n}\n\n/** What the options bar shows for a transform. */\nexport interface TransformReadout {\n /** False for Distort, Perspective and Warp: only `bounds` and `corners` then mean anything. */\n affine: boolean;\n /** Width and height in percent of the frame. */\n scaleX: number;\n scaleY: number;\n /** Rotation in degrees, clockwise, in (−180, 180]. */\n rotate: number;\n /** Horizontal skew in degrees (vertical skew is folded into rotation and scale). */\n skewX: number;\n /** Where the reference point is now (document pixels). */\n x: number;\n y: number;\n /** Bounds of the transformed outline. */\n bounds: CropBox | null;\n corners: Quad;\n}\n\n/**\n * Decomposes a transform for the options bar: the linear part as rotate · scale · skew (QR) with\n * the given reference point. Non-affine geometry reports its bounds and corners only.\n */\nexport function describeTransform(geometry: FreeTransformGeometry, reference: TransformParams['reference'] = 'center'): TransformReadout {\n const m = hasWarp(geometry) ? null : matrixFromCorners(geometry.box, geometry.corners);\n const ref = referencePoint(geometry.box, reference);\n const bounds = transformedBox(geometry);\n if (!m) {\n const h = rectToQuad(geometry.box, geometry.corners);\n const at = (h && applyHomography(h, ref)) ?? ref;\n return { affine: false, scaleX: 100, scaleY: 100, rotate: 0, skewX: 0, x: at.x, y: at.y, bounds, corners: geometry.corners };\n }\n const [a, b, c, d] = m;\n const sxAbs = Math.hypot(a, b);\n const rot = (Math.atan2(b, a) * 180) / Math.PI;\n const shear = sxAbs ? (a * c + b * d) / sxAbs : 0;\n const sy = sxAbs ? (a * d - b * c) / sxAbs : 0;\n // Skew then scale: [[sx, sx·tan(skew)], [0, sy]], so tan(skew) = shear / sx.\n const skew = sxAbs ? (Math.atan(shear / sxAbs) * 180) / Math.PI : 0;\n const at = applyMatrix(m, ref);\n const clean = (v: number) => (Math.abs(v) < 1e-9 ? 0 : v);\n return {\n affine: true,\n scaleX: clean(sxAbs * 100),\n scaleY: clean(sy * 100),\n rotate: clean(rot === -180 ? 180 : rot),\n skewX: clean(skew),\n x: at.x,\n y: at.y,\n bounds,\n corners: geometry.corners,\n };\n}\n\n/**\n * Corners for a transform given as numbers relative to the current corners: `params` are applied\n * after the current affine part (when the corners are a parallelogram) about the reference point\n * of the current geometry, so typing \"rotate 15\" rotates the box as it is now.\n */\nexport function applyParams(geometry: FreeTransformGeometry, params: TransformParams): Quad {\n const ref = params.reference && typeof params.reference === 'object' ? params.reference : anchorPoint(geometry, (params.reference as TransformAnchor | undefined) ?? 'center');\n const m = matrixFromParams({ x: ref.x, y: ref.y, width: 0, height: 0 }, { ...params, reference: ref });\n const c = geometry.corners;\n return [applyMatrix(m, c[0]), applyMatrix(m, c[1]), applyMatrix(m, c[2]), applyMatrix(m, c[3])];\n}\n\n/** Flip of the corners about the box's centre line (horizontal: left ↔ right). */\nexport function flipCorners(geometry: FreeTransformGeometry, axis: 'horizontal' | 'vertical'): Quad {\n const c = anchorPoint(geometry, 'center');\n const m: AffineMatrix = axis === 'horizontal' ? [-1, 0, 0, 1, 2 * c.x, 0] : [1, 0, 0, -1, 0, 2 * c.y];\n const q = geometry.corners;\n return [applyMatrix(m, q[0]), applyMatrix(m, q[1]), applyMatrix(m, q[2]), applyMatrix(m, q[3])];\n}\n\n/** Inverse of `matrixFromCorners` (box space → document) for mapping a document point back into the box. */\nexport function boxPointOf(geometry: FreeTransformGeometry, p: Vec2): Vec2 | null {\n const m = matrixFromCorners(geometry.box, geometry.corners);\n if (m) return applyMatrix(invertMatrix(m), p);\n return null;\n}\n\n/** A warp mesh from a preset over the box (null when it changes nothing). */\nexport function presetWarp(box: CropBox, preset: WarpPreset): WarpMesh | null {\n return warpPresetMesh(preset, box);\n}\n\n/** Validates a warp mesh for a box: well formed, finite, the right number of points. */\nexport function isValidWarp(mesh: unknown): mesh is WarpMesh {\n return isValidWarpMesh(mesh);\n}\n\n/** The geometry's affine map when it is one (no warp, a parallelogram), as a homography; else the projective map. */\nexport function geometryHomography(geometry: FreeTransformGeometry): Homography | null {\n const m = matrixFromCorners(geometry.box, geometry.corners);\n return m ? homographyFromAffine(m) : transformHomography(geometry);\n}\n","/**\n * Layer tree operations as pure functions: each validates its arguments against the stack and\n * throws a `LayerOperationError` that says what is wrong before changing anything, and returns a\n * new stack that shares every untouched layer. The controller wraps them with undo history.\n */\n\nimport type { BlendMode } from '../blend';\nimport { isBlendMode } from '../blend';\nimport type { CropBox } from '../types';\nimport { translateSurface } from '../../raster/surface';\nimport {\n defaultLayerEffect,\n isLayerEffectKind,\n LAYER_EFFECT_KINDS,\n LayerEffectError,\n mergeLayerEffect,\n newEffectId,\n validateLayerEffects,\n type LayerEffect,\n type LayerEffectKind,\n type LayerEffects,\n} from '../../raster/effects/model';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isLayerLocked,\n mapLayer,\n newLayerId,\n type GroupLayer,\n type ImageLayer,\n type LayerBlendMode,\n type LayerFilter,\n type LayerLocks,\n type PixelLayer,\n type RasterMask,\n} from './model';\n\n/** A refused layer operation; the message says why and how to fix it. */\nexport class LayerOperationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LayerOperationError';\n }\n}\n\nconst fail = (message: string): never => {\n throw new LayerOperationError(message);\n};\n\n/** Where a layer goes: above or below a layer (in its group), or at a position in a group. */\nexport interface LayerPlacement {\n /** Directly above this layer, in the same group. */\n aboveId?: string;\n /** Directly below this layer, in the same group. */\n belowId?: string;\n /** Inside this group (at `index`, default the top). */\n parentId?: string;\n /** Index in the parent's children (or the top level), 0 = bottom. */\n index?: number;\n}\n\n/** The layer with `id`, or a `LayerOperationError` listing the ids that exist. */\nexport function requireLayer(layers: readonly ImageLayer[], id: string) {\n const loc = findLayer(layers, id);\n if (!loc) {\n const ids = flattenLayers(layers).map((l) => l.id);\n fail(`Unknown layer \"${id}\". ${ids.length ? `Layers: ${ids.join(', ')}.` : 'The document has no layers.'} Call list_layers for ids.`);\n }\n return loc!;\n}\n\n/** Refuses when the layer is locked for `what` (and always for `all`). */\nexport function requireUnlocked(layer: ImageLayer, what: keyof LayerLocks, action: string): void {\n if (isLayerLocked(layer, 'all')) fail(`Layer \"${layer.name || layer.id}\" is locked (all); unlock it before you ${action}.`);\n if (what !== 'all' && isLayerLocked(layer, what)) fail(`Layer \"${layer.name || layer.id}\" has its ${what} locked; unlock it before you ${action}.`);\n}\n\n/** Inserts `layer` at `placement` (default: the top level at `defaultIndex`, or the top). */\nexport function placeLayer(layers: readonly ImageLayer[], layer: ImageLayer, placement: LayerPlacement = {}, defaultIndex?: number): ImageLayer[] {\n const given = [placement.aboveId, placement.belowId, placement.parentId].filter((v) => v !== undefined).length;\n if (given > 1) fail('Give only one of aboveId, belowId and parentId.');\n if (placement.aboveId || placement.belowId) {\n const ref = requireLayer(layers, (placement.aboveId ?? placement.belowId)!);\n return insertLayer(layers, layer, ref.parent?.id ?? null, ref.index + (placement.aboveId ? 1 : 0));\n }\n if (placement.parentId) {\n const ref = requireLayer(layers, placement.parentId);\n if (ref.layer.type !== 'group') fail(`Layer \"${placement.parentId}\" is not a group; parentId must be a group id.`);\n return insertLayer(layers, layer, ref.layer.id, placement.index);\n }\n return insertLayer(layers, layer, null, placement.index ?? defaultIndex);\n}\n\n/** Removes a layer (refused when it is locked). */\nexport function removeLayer(layers: readonly ImageLayer[], id: string): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'delete it');\n return mapLayer(layers, id, () => null);\n}\n\n/** Moves a layer to `placement`. A group cannot go inside itself. Returns the new stack. */\nexport function moveLayer(layers: readonly ImageLayer[], id: string, placement: LayerPlacement): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'move it');\n const target = placement.aboveId ?? placement.belowId ?? placement.parentId;\n if (target === id) fail('A layer cannot be placed relative to itself.');\n if (target && loc.layer.type === 'group' && flattenLayers(loc.layer.children).some((l) => l.id === target)) {\n fail(`Group \"${id}\" cannot be moved into its own child \"${target}\".`);\n }\n if (target) requireLayer(layers, target);\n if (!target && placement.index === undefined) fail('Give aboveId, belowId, parentId or index.');\n const without = mapLayer(layers, id, () => null);\n return placeLayer(without, loc.layer, placement);\n}\n\n/** Every id in a subtree gets a fresh one (for duplicates). */\nexport function withNewIds<T extends ImageLayer>(layer: T): T {\n const next = { ...layer, id: newLayerId(layer.type === 'adjustment' ? 'adj' : layer.type) } as T;\n if (next.type === 'group') (next as GroupLayer).children = (next as GroupLayer).children.map(withNewIds);\n if (next.type === 'annotation') next.annotation = { ...next.annotation, id: next.id };\n if (next.type === 'smart' && next.psd?.placed) {\n // A copy is another instance of the same contents: Photoshop needs it to have its own instance id.\n next.psd = { ...next.psd };\n delete next.psd.placed;\n }\n return next;\n}\n\n/** Duplicates a layer (and its children) directly above it. Returns the stack and the copy's id. */\nexport function duplicateLayer(layers: readonly ImageLayer[], id: string, name?: string): { layers: ImageLayer[]; id: string } {\n const loc = requireLayer(layers, id);\n const copy = withNewIds(loc.layer);\n copy.name = name ?? `${loc.layer.name || 'Layer'} copy`;\n delete copy.locks;\n return { layers: insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1), id: copy.id };\n}\n\n/** Properties `setLayerProperties` changes. */\nexport interface LayerProperties {\n name?: string;\n visible?: boolean;\n /** 0..1 */\n opacity?: number;\n /** 0..1, pixel layers only. */\n fillOpacity?: number;\n blend?: LayerBlendMode;\n clipped?: boolean;\n locks?: LayerLocks;\n /** Groups only (UI state). */\n collapsed?: boolean;\n}\n\n/** Validates and applies property changes to one layer. */\nexport function setLayerProperties(layers: readonly ImageLayer[], id: string, props: LayerProperties): ImageLayer[] {\n const loc = requireLayer(layers, id);\n const layer = loc.layer;\n const onlyUnlocking = Object.keys(props).every((k) => k === 'locks' || k === 'visible' || k === 'collapsed');\n if (!onlyUnlocking) requireUnlocked(layer, 'all', 'change its properties');\n const next = { ...layer } as ImageLayer;\n if (props.name !== undefined) {\n if (typeof props.name !== 'string' || !props.name.trim()) fail('name must be a non-empty string.');\n next.name = props.name.trim();\n }\n if (props.visible !== undefined) next.visible = Boolean(props.visible);\n if (props.opacity !== undefined) {\n if (!(typeof props.opacity === 'number' && props.opacity >= 0 && props.opacity <= 1)) fail('opacity must be a number from 0 to 1.');\n next.opacity = props.opacity;\n }\n if (props.fillOpacity !== undefined) {\n if (layer.type !== 'pixel' && layer.type !== 'smart') fail(`fillOpacity applies to pixel layers and smart objects; \"${id}\" is a ${layer.type} layer.`);\n if (!(typeof props.fillOpacity === 'number' && props.fillOpacity >= 0 && props.fillOpacity <= 1)) fail('fillOpacity must be a number from 0 to 1.');\n const p = next as { fillOpacity?: number };\n if (props.fillOpacity === 1) delete p.fillOpacity;\n else p.fillOpacity = props.fillOpacity;\n }\n if (props.blend !== undefined) {\n if (props.blend === 'pass-through') {\n if (layer.type !== 'group') fail('pass-through is only for groups.');\n } else if (!isBlendMode(props.blend)) fail(`Unknown blend mode \"${props.blend}\".`);\n (next as { blend: LayerBlendMode }).blend = props.blend;\n }\n if (props.clipped !== undefined) {\n if (props.clipped && loc.index === 0) fail('The bottom layer of a group or the stack has nothing below to clip to.');\n if (props.clipped) next.clipped = true;\n else delete next.clipped;\n }\n if (props.locks !== undefined) {\n const locks: LayerLocks = {};\n for (const k of ['pixels', 'transparency', 'position', 'all'] as const) if (props.locks[k]) locks[k] = true;\n if (Object.keys(locks).length) next.locks = locks;\n else delete next.locks;\n }\n if (props.collapsed !== undefined) {\n if (layer.type !== 'group') fail('collapsed applies to groups.');\n const g = next as GroupLayer;\n if (props.collapsed) g.collapsed = true;\n else delete g.collapsed;\n }\n return mapLayer(layers, id, () => next);\n}\n\n/**\n * Puts layers (siblings in one group or the top level) into a new group at the position of the\n * topmost one, keeping their order. Returns the stack and the group id.\n */\nexport function groupLayers(layers: readonly ImageLayer[], ids: string[], name = 'Group'): { layers: ImageLayer[]; id: string } {\n if (!ids.length) fail('Give at least one layer id to group.');\n const locs = ids.map((id) => requireLayer(layers, id));\n const parent = locs[0].parent?.id ?? null;\n if (locs.some((l) => (l.parent?.id ?? null) !== parent)) fail('Layers to group must share one parent; move them next to each other first.');\n for (const l of locs) requireUnlocked(l.layer, 'all', 'group it');\n const ordered = [...new Set(locs)].sort((a, b) => a.index - b.index);\n const top = ordered[ordered.length - 1].index;\n const below = ordered.filter((l) => l.index < top).length;\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name,\n visible: true,\n opacity: 1,\n blend: 'pass-through',\n children: ordered.map((l) => {\n const c = { ...l.layer } as ImageLayer;\n return c;\n }),\n };\n // The bottom child cannot be clipped to anything inside the new group.\n if (group.children[0]?.clipped) {\n const c = { ...group.children[0] };\n delete c.clipped;\n group.children[0] = c;\n }\n let out = layers as ImageLayer[];\n for (const l of ordered) out = mapLayer(out, l.layer.id, () => null);\n return { layers: insertLayer(out, group, parent, top - below), id: group.id };\n}\n\n/** Replaces a group by its children (the group's own opacity, blend and mask are dropped). */\nexport function ungroupLayer(layers: readonly ImageLayer[], id: string): { layers: ImageLayer[]; ids: string[] } {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'group') fail(`Layer \"${id}\" is not a group.`);\n requireUnlocked(loc.layer, 'all', 'ungroup it');\n const group = loc.layer as GroupLayer;\n let out = mapLayer(layers, id, () => null);\n group.children.forEach((c, i) => {\n out = insertLayer(out, c, loc.parent?.id ?? null, loc.index + i);\n });\n return { layers: out, ids: group.children.map((c) => c.id) };\n}\n\n/** Sets a pixel layer's offset (refused when its position is locked). An unlinked mask stays in place. */\nexport function offsetLayer(layers: readonly ImageLayer[], id: string, offset: { x: number; y: number }): ImageLayer[] {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'pixel') fail(`Only pixel layers move; \"${id}\" is a ${loc.layer.type} layer.`);\n requireUnlocked(loc.layer, 'position', 'move it');\n if (!Number.isFinite(offset.x) || !Number.isFinite(offset.y)) fail('x and y must be numbers.');\n const layer = loc.layer as PixelLayer;\n const next: PixelLayer = { ...layer, offset: { x: Math.round(offset.x), y: Math.round(offset.y) } };\n const mask = layer.mask;\n if (mask && mask.linked === false && !mask.source) {\n // An unlinked mask stays where it is in the document, so it moves the other way in layer space.\n next.mask = { ...mask, surface: translateSurface(mask.surface, layer.offset.x - next.offset.x, layer.offset.y - next.offset.y) };\n }\n return mapLayer(layers, id, () => next);\n}\n\n/** Sets or removes (`null`) a layer's mask. */\nexport function setLayerMask(layers: readonly ImageLayer[], id: string, mask: RasterMask | null): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'change its mask');\n return mapLayer(layers, id, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Filters\n// ---------------------------------------------------------------------------\n\nconst DEVELOP_FIELDS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/** A clean filter from loose input (for tools and the UI). */\nexport function normalizeFilter(raw: Partial<LayerFilter> & { kind: LayerFilter['kind'] }, id = newLayerId('flt')): LayerFilter {\n const enabled = raw.enabled !== false;\n if (raw.kind === 'blur') {\n const r = (raw as { radius?: unknown }).radius;\n if (r !== undefined && !(typeof r === 'number' && r >= 0 && r <= 50)) fail('blur radius must be a number from 0 to 50 px.');\n return { id, kind: 'blur', enabled, radius: typeof r === 'number' ? r : 2 };\n }\n if (raw.kind !== 'develop') fail(`Unknown filter kind \"${(raw as { kind: string }).kind}\". Use develop or blur.`);\n const out = { id, kind: 'develop', enabled, brightness: 0, contrast: 0, saturation: 0, exposure: 0, temperature: 0, tint: 0, process: 2 } as LayerFilter & { kind: 'develop' };\n for (const k of DEVELOP_FIELDS) {\n const v = (raw as Record<string, unknown>)[k];\n if (v === undefined) continue;\n if (!(typeof v === 'number' && v >= -100 && v <= 100)) fail(`${k} must be a number from -100 to 100.`);\n out[k] = v as number;\n }\n const p = (raw as { process?: unknown }).process;\n if (p !== undefined) {\n if (p !== 1 && p !== 2) fail('process must be 1 or 2.');\n out.process = p as 1 | 2;\n }\n return out;\n}\n\nfunction requirePixelLayer(layers: readonly ImageLayer[], id: string): PixelLayer {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'pixel') fail(`Filters belong to pixel layers; \"${id}\" is a ${loc.layer.type} layer.`);\n requireUnlocked(loc.layer, 'all', 'change its filters');\n return loc.layer as PixelLayer;\n}\n\n/** Adds a filter on top of a pixel layer's filters. A layer has at most one filter of each kind. */\nexport function addLayerFilter(layers: readonly ImageLayer[], id: string, filter: LayerFilter): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n if (layer.filters?.some((f) => f.kind === filter.kind)) fail(`Layer \"${id}\" already has a ${filter.kind} filter; change it with update_layer_filter.`);\n return mapLayer(layers, id, () => ({ ...layer, filters: [...(layer.filters ?? []), filter] }));\n}\n\n/** Changes one filter of a pixel layer (`changes` merged and validated). */\nexport function updateLayerFilter(layers: readonly ImageLayer[], id: string, filterId: string, changes: Record<string, unknown>): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n const i = (layer.filters ?? []).findIndex((f) => f.id === filterId);\n if (i === -1) fail(`Layer \"${id}\" has no filter \"${filterId}\". Filters: ${(layer.filters ?? []).map((f) => f.id).join(', ') || 'none'}.`);\n const current = layer.filters![i];\n if ('kind' in changes && changes.kind !== current.kind) fail('A filter kind cannot change; remove it and add another.');\n const merged = normalizeFilter({ ...current, ...changes, kind: current.kind } as LayerFilter, current.id);\n const filters = [...layer.filters!];\n filters[i] = merged;\n return mapLayer(layers, id, () => ({ ...layer, filters }));\n}\n\nexport function removeLayerFilter(layers: readonly ImageLayer[], id: string, filterId: string): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n if (!(layer.filters ?? []).some((f) => f.id === filterId)) fail(`Layer \"${id}\" has no filter \"${filterId}\".`);\n const filters = layer.filters!.filter((f) => f.id !== filterId);\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return mapLayer(layers, id, () => next);\n}\n\n// ---------------------------------------------------------------------------\n// Layer effects\n// ---------------------------------------------------------------------------\n\n/** A layer that can carry effects (pixel, annotation and group layers), checked for locks. */\nfunction requireEffectLayer(layers: readonly ImageLayer[], id: string, action: string): ImageLayer {\n const layer = requireLayer(layers, id).layer;\n if (layer.type === 'adjustment') fail(`Layer \"${layer.name || id}\" is an adjustment layer; effects go on pixel, annotation and group layers.`);\n requireUnlocked(layer, 'all', action);\n return layer;\n}\n\n/** A layer's effects, or an empty, enabled list when it has none. */\nexport function layerEffectsOf(layer: ImageLayer): LayerEffects {\n return layer.effects ?? { enabled: true, items: [] };\n}\n\n/** Wraps an effect validation error as a layer operation error (same message). */\nfunction guard<T>(fn: () => T): T {\n try {\n return fn();\n } catch (e) {\n if (e instanceof LayerEffectError) fail(e.message);\n throw e;\n }\n}\n\n/** Replaces a layer's effects (validated); `null` or an empty list without a PSD block removes them. */\nexport function setLayerEffects(layers: readonly ImageLayer[], id: string, effects: LayerEffects | null): ImageLayer[] {\n const layer = requireEffectLayer(layers, id, 'change its effects');\n const next = { ...layer };\n if (effects && (effects.items.length || effects.psd || effects.scale !== undefined || !effects.enabled)) {\n guard(() => validateLayerEffects(effects));\n next.effects = effects;\n } else delete next.effects;\n return mapLayer(layers, id, () => next as ImageLayer);\n}\n\n/**\n * Adds an effect of `kind` (Photoshop's defaults with `params` applied) to a layer's effects, at\n * `index` in the list (default: the end; the first listed of a kind is on top). Returns the stack\n * and the new effect.\n */\nexport function addLayerEffect(\n layers: readonly ImageLayer[],\n id: string,\n kind: LayerEffectKind,\n params: Record<string, unknown> = {},\n index?: number\n): { layers: ImageLayer[]; effect: LayerEffect } {\n if (!isLayerEffectKind(kind)) fail(`Unknown effect kind \"${String(kind)}\". Kinds: ${LAYER_EFFECT_KINDS.join(', ')}.`);\n const layer = requireEffectLayer(layers, id, 'add effects');\n const current = layerEffectsOf(layer);\n const ids = new Set(current.items.map((e) => e.id));\n let effectId = newEffectId();\n while (ids.has(effectId)) effectId = newEffectId();\n const { id: _ignored, kind: _kind, ...rest } = params;\n const effect = guard(() => mergeLayerEffect(defaultLayerEffect(kind, effectId), rest));\n const items = [...current.items];\n const at = index === undefined ? items.length : Math.max(0, Math.min(items.length, Math.floor(index)));\n items.splice(at, 0, effect);\n return { layers: setLayerEffects(layers, id, { ...current, items }), effect };\n}\n\n/** The index of effect `effectId` on a layer, or a `LayerOperationError` listing the effect ids. */\nfunction requireEffect(layer: ImageLayer, effectId: string): number {\n const items = layer.effects?.items ?? [];\n const i = items.findIndex((e) => e.id === effectId);\n 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.`);\n return i;\n}\n\n/** Changes one effect (only the given fields; nested objects replace whole, `null` removes an optional one). */\nexport function updateLayerEffect(layers: readonly ImageLayer[], id: string, effectId: string, changes: Record<string, unknown>): ImageLayer[] {\n const layer = requireEffectLayer(layers, id, 'change its effects');\n const i = requireEffect(layer, effectId);\n const current = layer.effects!;\n const items = [...current.items];\n items[i] = guard(() => mergeLayerEffect(items[i], changes));\n return setLayerEffects(layers, id, { ...current, items });\n}\n\nexport function removeLayerEffect(layers: readonly ImageLayer[], id: string, effectId: string): ImageLayer[] {\n const layer = requireEffectLayer(layers, id, 'change its effects');\n requireEffect(layer, effectId);\n const current = layer.effects!;\n return setLayerEffects(layers, id, { ...current, items: current.items.filter((e) => e.id !== effectId) });\n}\n\n/** Sets the master switch and, when given, Scale Effects (0.01..10, 1 = 100 %). */\nexport function setLayerEffectsEnabled(layers: readonly ImageLayer[], id: string, enabled: boolean, scale?: number): ImageLayer[] {\n const layer = requireEffectLayer(layers, id, 'change its effects');\n if (scale !== undefined && !(Number.isFinite(scale) && scale >= 0.01 && scale <= 10)) fail(`scale must be from 0.01 to 10 (1 = 100 %), got ${scale}.`);\n const current = layerEffectsOf(layer);\n const next: LayerEffects = { ...current, enabled };\n if (scale !== undefined) {\n if (scale === 1) delete next.scale;\n else next.scale = scale;\n }\n return setLayerEffects(layers, id, next);\n}\n\n/** A blend mode check for tools. */\nexport function parseBlend(value: unknown, allowPassThrough = false): BlendMode | 'pass-through' | undefined {\n if (value === undefined) return undefined;\n if (value === 'pass-through' && allowPassThrough) return value;\n if (!isBlendMode(value)) fail(`Unknown blend mode \"${String(value)}\".`);\n return value as BlendMode;\n}\n\n/** The union of two rectangles (either may be null). */\nexport function unionRect(a: CropBox | null, b: CropBox | null): CropBox | null {\n if (!a) return b;\n if (!b) return a;\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n","// Portions ported from photocraft/crates/doc/src/lib.rs (SmartObject, SmartSource, smart filters) (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Smart objects as pure functions: their contents (raster pixels or a nested layer document,\n * shared by linked instances), the non-destructive placement (a warp in the contents' pixels and a\n * projective corner map, the Free Transform geometry), the smart filter stack with its one filter\n * mask, and the rendering that turns them into the layer's pixels. Every change re-renders from\n * the contents, so transforms never degrade.\n *\n * The operations validate against the stack and throw a `LayerOperationError` that says what is\n * wrong before changing anything, like the other layer operations.\n */\n\nimport { blendTile } from '../../raster/compositor';\nimport { gaussianBlur } from '../../raster/blur';\nimport { MAX_EFFECT_REACH, effectsMargin } from '../../raster/effects/model';\nimport {\n contentBounds,\n emptySurface,\n readRegion,\n surfaceFromPixels,\n translateSurface,\n type PixelSurface,\n type RasterRect,\n} from '../../raster/surface';\nimport { isInterpolation, reduceSurface, transformSurface, TransformTooLargeError, warpSurface, type Interpolation } from '../../raster/transform';\nimport { isBlendMode, type BlendMode } from '../../raster/blend';\nimport { boxQuad, invertHomography, multiplyHomographies, type Homography } from '../../geom/homography';\nimport { isValidWarpMesh, mapWarpMeshPoints } from '../../geom/warp';\nimport type { Quad, WarpMesh } from '../../geom/types';\nimport { hasWarp, transformHomography, transformMap, transformedBox, type FreeTransformGeometry } from '../transform';\nimport type { CropBox } from '../types';\nimport { LayerCompositor, runLayerFilters } from './compositor';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isActiveFilter,\n mapLayer,\n newLayerId,\n type ImageLayer,\n type LayerFilter,\n type PixelLayer,\n type RasterMask,\n type SmartContents,\n type SmartDocument,\n type SmartFilter,\n type SmartFilterStack,\n type SmartObjectLayer,\n type SmartPlacement,\n} from './model';\nimport { LayerOperationError, normalizeFilter, requireLayer, requireUnlocked } from './ops';\n\nconst fail = (message: string): never => {\n throw new LayerOperationError(message);\n};\n\n/**\n * How far beyond the canvas a smart object is rendered (document pixels): enough for any layer\n * effect to see the parts just outside the canvas. Further out a smart object is not drawn.\n */\nexport const SMART_RENDER_MARGIN = MAX_EFFECT_REACH;\n\n/** The document canvas a smart object renders for. */\nexport interface SmartCanvas {\n width: number;\n height: number;\n}\n\n/** A fresh contents id (independent contents). */\nexport function newContentsId(): string {\n return newLayerId('so');\n}\n\n/** The contents' rectangle in its own pixels. */\nexport function contentsBox(contents: SmartContents): CropBox {\n return { x: 0, y: 0, width: contents.width, height: contents.height };\n}\n\n/** A smart object's placement as Free Transform geometry (the frame is the contents' rectangle). */\nexport function placementGeometry(layer: Pick<SmartObjectLayer, 'contents' | 'placement'>): FreeTransformGeometry {\n return { box: contentsBox(layer.contents), corners: layer.placement.corners, warp: layer.placement.warp ?? null };\n}\n\n/** The placement that draws the contents 1:1 with their top-left corner at (`x`, `y`). */\nexport function placementAt(contents: Pick<SmartContents, 'width' | 'height'>, x: number, y: number): SmartPlacement {\n return { corners: boxQuad({ x, y, width: contents.width, height: contents.height }) };\n}\n\n/** Whether the contents can be rendered here (decoded pixels or a layer document). */\nexport function isContentsDecoded(contents: SmartContents): boolean {\n return contents.kind === 'raster' ? Boolean(contents.raster) : Boolean(contents.document);\n}\n\nfunction requireDecoded(layer: SmartObjectLayer, action: string): void {\n if (isContentsDecoded(layer.contents)) return;\n const what = layer.contents.file?.type ? `a ${layer.contents.file.type.toUpperCase()} file` : 'a file';\n fail(\n `The contents of smart object \"${layer.name || layer.id}\" are ${what} that could not be decoded here, so it cannot ${action}. ` +\n 'Run prepareSmartObjects() in a browser first, or rasterize it.'\n );\n}\n\n/** The layer with `id`, which must be a smart object. */\nexport function requireSmartObject(layers: readonly ImageLayer[], id: string): SmartObjectLayer {\n const layer = requireLayer(layers, id).layer;\n if (layer.type !== 'smart') fail(`Layer \"${layer.name || id}\" is a ${layer.type} layer, not a smart object; convert it with convert_to_smart_object.`);\n return layer as SmartObjectLayer;\n}\n\n// ---------------------------------------------------------------------------\n// Contents\n// ---------------------------------------------------------------------------\n\nconst contentsPixels = new WeakMap<SmartContents, PixelSurface>();\n\n/**\n * The contents as pixels at (0, 0) in their own space: the raster, or the nested document\n * composited at its size (annotation layers inside need a canvas and are left out). Memoised.\n */\nexport function contentsSurface(contents: SmartContents): PixelSurface {\n let s = contentsPixels.get(contents);\n if (s) return s;\n if (contents.kind === 'raster') {\n s = contents.raster ?? emptySurface(4);\n } else {\n const doc = contents.document;\n if (!doc) return emptySurface(4);\n const W = contents.width;\n const H = contents.height;\n const data = new LayerCompositor().composite(doc.layers, { x: 0, y: 0, width: W, height: H }, {\n width: W,\n height: H,\n globalLight: doc.globalLight,\n pattern: (id) => doc.patterns?.find((p) => p.id === id) ?? null,\n });\n s = surfaceFromPixels({ width: W, height: H, data }, 4, 0, 0);\n }\n contentsPixels.set(contents, s);\n return s;\n}\n\n/** Raster contents from RGBA pixels. */\nexport function rasterContents(pixels: { width: number; height: number; data: ArrayLike<number> }, name: string, id = newContentsId()): SmartContents {\n if (!(pixels.width > 0 && pixels.height > 0) || pixels.data.length < pixels.width * pixels.height * 4) fail('Contents pixels must be RGBA with width × height × 4 bytes.');\n return { id, name, width: pixels.width, height: pixels.height, kind: 'raster', raster: surfaceFromPixels(pixels, 4, 0, 0) };\n}\n\n/** Document contents from layers on a `width × height` canvas. */\nexport function documentContents(document: SmartDocument, width: number, height: number, name: string, id = newContentsId()): SmartContents {\n if (!(Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0)) fail('Contents width and height must be positive whole numbers.');\n return { id, name, width, height, kind: 'document', document };\n}\n\n// ---------------------------------------------------------------------------\n// Rendering\n// ---------------------------------------------------------------------------\n\n/** How a render runs: full quality (default), or a fast preview. */\nexport interface SmartRenderOptions {\n /** Preview block size (a power of two): sample a reduced copy of the contents once per block. */\n step?: number;\n /** Only this part of the document (the visible area). */\n clip?: RasterRect;\n /** Leave the smart filters out (previews while transforming). */\n skipFilters?: boolean;\n /** Reduced copies of the contents by factor, kept by the caller between previews. */\n reduced?: Map<number, PixelSurface>;\n}\n\nconst placedCache = new WeakMap<SmartContents, Map<string, PixelSurface>>();\nconst filteredCache = new WeakMap<PixelSurface, { key: string; base: Uint8ClampedArray; filtered: Uint8ClampedArray }>();\n\nfunction placementKey(p: SmartPlacement, canvas: SmartCanvas): string {\n return JSON.stringify([p.corners, p.warp ?? null, p.interpolation ?? 'bicubic', canvas.width, canvas.height]);\n}\n\n/** The integer move of a placement that is a whole-pixel translation without a warp, or `null`. */\nfunction integerMove(layer: Pick<SmartObjectLayer, 'contents' | 'placement'>): { x: number; y: number } | null {\n const g = placementGeometry(layer);\n if (hasWarp(g)) return null;\n const q = boxQuad(g.box);\n const dx = g.corners[0].x - q[0].x;\n const dy = g.corners[0].y - q[0].y;\n if (Math.abs(dx - Math.round(dx)) > 1e-9 || Math.abs(dy - Math.round(dy)) > 1e-9) return null;\n const same = g.corners.every((c, i) => Math.abs(c.x - q[i].x - dx) < 1e-9 && Math.abs(c.y - q[i].y - dy) < 1e-9);\n return same ? { x: Math.round(dx), y: Math.round(dy) } : null;\n}\n\n/** The contents placed in the document (no filters), clipped to the canvas grown by the render margin. */\nexport function placeContents(layer: Pick<SmartObjectLayer, 'contents' | 'placement'>, canvas: SmartCanvas, options: SmartRenderOptions = {}): PixelSurface {\n const { contents, placement } = layer;\n const preview = Boolean(options.step && options.step > 1) || Boolean(options.clip);\n const key = placementKey(placement, canvas);\n if (!preview) {\n const hit = placedCache.get(contents)?.get(key);\n if (hit) return hit;\n }\n const src = contentsSurface(contents);\n const m = SMART_RENDER_MARGIN;\n const area = { x: -m, y: -m, width: canvas.width + 2 * m, height: canvas.height + 2 * m };\n let clip = area;\n if (options.clip) {\n const c = options.clip;\n const x0 = Math.max(area.x, Math.floor(c.x));\n const y0 = Math.max(area.y, Math.floor(c.y));\n const x1 = Math.min(area.x + area.width, Math.ceil(c.x + c.width));\n const y1 = Math.min(area.y + area.height, Math.ceil(c.y + c.height));\n clip = { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };\n }\n let out: PixelSurface;\n const move = integerMove(layer);\n if (move && !options.clip) {\n // Exact and cheap: a whole-pixel move shares tiles (Convert to Smart Object renders this way).\n out = translateSurface(src, move.x, move.y);\n } else {\n const g = placementGeometry(layer);\n const interpolation: Interpolation = preview ? (placement.interpolation === 'nearest' ? 'nearest' : 'bilinear') : (placement.interpolation ?? 'bicubic');\n const step = options.step && options.step > 1 ? options.step : 1;\n let source = src;\n let k = 1;\n if (step > 1) {\n k = step;\n let r = options.reduced?.get(k);\n if (!r) {\n r = reduceSurface(src, k);\n options.reduced?.set(k, r);\n }\n source = r;\n }\n const box = { x: 0, y: 0, width: Math.ceil(contents.width / k), height: Math.ceil(contents.height / k) };\n try {\n if (hasWarp(g)) {\n const f = transformMap(g);\n if (!f) fail('The placement folds the contents over itself or through the horizon; keep the corners a convex quad.');\n out = warpSurface(source, (p) => f!({ x: p.x * k, y: p.y * k }), { interpolation, source: box, clip, step: step > 1 ? step : undefined });\n } else {\n const h = transformHomography(g);\n if (!h) fail('The placement folds the contents over itself or through the horizon; keep the corners a convex quad.');\n const hk = multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], h!);\n out = transformSurface(source, hk, { interpolation, source: box, clip, step: step > 1 ? step : undefined });\n }\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n if (!preview) {\n let byKey = placedCache.get(contents);\n if (!byKey) {\n byKey = new Map();\n placedCache.set(contents, byKey);\n }\n byKey.set(key, out);\n while (byKey.size > 4) byKey.delete(byKey.keys().next().value as string);\n }\n return out;\n}\n\n/** The filters of a stack that change pixels (the stack enabled, each filter active, a modelled kind). */\nexport function activeSmartFilters(stack: SmartFilterStack | undefined): Array<SmartFilter & LayerFilter> {\n if (!stack?.enabled) return [];\n return stack.filters.filter((f): f is SmartFilter & LayerFilter => f.kind !== 'psd' && isActiveFilter(f as LayerFilter) && f.opacity > 0);\n}\n\n/** A filter mask's coverage over the canvas (0..1), after density and feather; `null` reveals all. */\nexport function filterMaskCoverage(mask: RasterMask | undefined, canvas: SmartCanvas): Float32Array | null {\n if (!mask || mask.enabled === false || mask.source) return null;\n const W = canvas.width;\n const H = canvas.height;\n const bytes = readRegion(mask.surface, { x: 0, y: 0, width: W, height: H });\n let v: Float32Array = new Float32Array(W * H);\n let all = true;\n for (let i = 0; i < v.length; i++) {\n v[i] = bytes[i] / 255;\n if (bytes[i] !== 255) all = false;\n }\n const feather = mask.feather ?? 0;\n if (all && !(feather > 0)) return null;\n if (feather > 0) v = gaussianBlur(v, W, H, feather, 'clamp') as Float32Array;\n const density = mask.density ?? 1;\n if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);\n return v;\n}\n\n/**\n * Runs a smart filter stack over the placed contents, over the canvas (spatial filters clamp at\n * its edges, as a pixel layer's filters do): each filter filters the result below it, and its own\n * blend mode and opacity mix its result with that input. The filter mask then mixes the filtered\n * result with the unfiltered contents. Returns `placed` itself when no filter is active.\n */\nexport function applySmartFilters(placed: PixelSurface, stack: SmartFilterStack | undefined, canvas: SmartCanvas): PixelSurface {\n const active = activeSmartFilters(stack);\n if (!active.length || !(canvas.width > 0 && canvas.height > 0)) return placed;\n const W = canvas.width;\n const H = canvas.height;\n const key = JSON.stringify([W, H, active.map((f) => ({ ...f, id: undefined }))]);\n let entry = filteredCache.get(placed);\n if (!entry || entry.key !== key) {\n const base = readRegion(placed, { x: 0, y: 0, width: W, height: H });\n let acc = new Uint8ClampedArray(base);\n for (const f of active) {\n const out = new Uint8ClampedArray(acc);\n runLayerFilters(out, W, H, [f]);\n if (f.blend === 'normal') {\n if (f.opacity >= 1) acc = out;\n else lerpInto(acc, out, f.opacity, null);\n } else {\n blendTile(acc, out, f.blend, f.opacity, null);\n }\n }\n entry = { key, base, filtered: acc };\n filteredCache.set(placed, entry);\n }\n const cov = filterMaskCoverage(stack!.mask, canvas);\n let result = entry.filtered;\n if (cov) {\n result = new Uint8ClampedArray(entry.base);\n lerpInto(result, entry.filtered, 1, cov);\n }\n return surfaceFromPixels({ width: W, height: H, data: result }, 4, 0, 0);\n}\n\n/** `dst = lerp(dst, src, k · coverage)` per channel. */\nfunction lerpInto(dst: Uint8ClampedArray, src: Uint8ClampedArray, k: number, coverage: Float32Array | null): void {\n for (let i = 0, p = 0; i < dst.length; i += 4, p++) {\n const t = coverage ? k * coverage[p] : k;\n if (t <= 0) continue;\n if (t >= 1) {\n dst[i] = src[i];\n dst[i + 1] = src[i + 1];\n dst[i + 2] = src[i + 2];\n dst[i + 3] = src[i + 3];\n continue;\n }\n for (let c = 0; c < 4; c++) dst[i + c] = Math.round(dst[i + c] + (src[i + c] - dst[i + c]) * t);\n }\n}\n\n/** A smart object's pixels: the contents through the placement, then the smart filters. */\nexport function renderSmartObject(layer: Pick<SmartObjectLayer, 'contents' | 'placement' | 'smartFilters'>, canvas: SmartCanvas, options: SmartRenderOptions = {}): PixelSurface {\n const placed = placeContents(layer, canvas, options);\n return options.skipFilters ? placed : applySmartFilters(placed, layer.smartFilters, canvas);\n}\n\n/** The layer with its pixels rendered again (refused when the contents are not decoded). */\nexport function rerender(layer: SmartObjectLayer, canvas: SmartCanvas, action = 'change'): SmartObjectLayer {\n requireDecoded(layer, action);\n return { ...layer, surface: renderSmartObject(layer, canvas) };\n}\n\n// ---------------------------------------------------------------------------\n// Placement\n// ---------------------------------------------------------------------------\n\n/** Validates a placement for contents of `width × height`; returns a clean copy. */\nexport function normalizePlacement(raw: SmartPlacement, contents: Pick<SmartContents, 'width' | 'height'>): SmartPlacement {\n const c = raw?.corners as unknown;\n if (!Array.isArray(c) || c.length !== 4 || !c.every((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y))) {\n fail('corners must be four finite points [top-left, top-right, bottom-right, bottom-left] in document pixels.');\n }\n const out: SmartPlacement = { corners: (c as Quad).map((p) => ({ x: p.x, y: p.y })) as unknown as Quad };\n if (raw.warp) {\n if (!isValidWarpMesh(raw.warp)) fail('warp must be a mesh with us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points in contents pixels.');\n out.warp = { us: [...raw.warp.us], vs: [...raw.warp.vs], points: raw.warp.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n if (raw.interpolation !== undefined) {\n if (!isInterpolation(raw.interpolation)) fail(`Unknown interpolation \"${String(raw.interpolation)}\"; use nearest, bilinear or bicubic.`);\n if (raw.interpolation !== 'bicubic') out.interpolation = raw.interpolation;\n }\n const g: FreeTransformGeometry = { box: { x: 0, y: 0, width: contents.width, height: contents.height }, corners: out.corners, warp: out.warp ?? null };\n if (!transformHomography(g)) fail('These corners fold the contents over themselves or through the horizon; keep the quad convex.');\n if (out.warp && !hasWarp(g)) delete out.warp;\n return out;\n}\n\n/** The projective map of a warp-free placement (contents → document), or `null`. */\nfunction placementHomography(layer: Pick<SmartObjectLayer, 'contents' | 'placement'>): Homography | null {\n const g = placementGeometry(layer);\n return hasWarp(g) ? null : transformHomography(g);\n}\n\n/**\n * Moves a linked mask with a placement change when both placements are warp-free (by the change's\n * projective map); otherwise, or for an unlinked or parametric mask, it stays.\n */\nfunction followMask(mask: RasterMask | undefined, before: SmartObjectLayer, after: SmartObjectLayer): RasterMask | undefined {\n if (!mask || mask.linked === false || mask.source) return mask;\n const h0 = placementHomography(before);\n const h1 = placementHomography(after);\n if (!h0 || !h1) return mask;\n const inv = invertHomography(h0);\n if (!inv) return mask;\n const delta = multiplyHomographies(inv, h1);\n const surface = transformSurface(mask.surface, delta, { interpolation: 'bilinear' });\n return { ...mask, surface };\n}\n\n/**\n * Sets a smart object's placement (corners, warp, interpolation) and renders it again from its\n * contents. A linked layer mask and filter mask follow a warp-free change. Returns the stack and\n * whether the masks moved.\n */\nexport function setSmartPlacement(\n layers: readonly ImageLayer[],\n id: string,\n placement: SmartPlacement,\n canvas: SmartCanvas\n): { layers: ImageLayer[]; layer: SmartObjectLayer; masksMoved: boolean } {\n const layer = requireSmartObject(layers, id);\n requireUnlocked(layer, 'position', 'transform it');\n requireDecoded(layer, 'be transformed');\n const clean = normalizePlacement(placement, layer.contents);\n let next: SmartObjectLayer = { ...layer, placement: clean };\n let masksMoved = false;\n if (layer.mask) {\n const m = followMask(layer.mask, layer, next);\n masksMoved ||= m !== layer.mask;\n next.mask = m;\n }\n if (next.smartFilters?.mask) {\n const m = followMask(next.smartFilters.mask, layer, next);\n masksMoved ||= m !== next.smartFilters.mask;\n next.smartFilters = { ...next.smartFilters, mask: m };\n }\n next = rerender(next, canvas, 'be transformed');\n return { layers: mapLayer(layers, id, () => next), layer: next, masksMoved };\n}\n\n/**\n * Moves a smart object by whole pixels (the move tool): its corners, a linked mask and filter mask\n * move with it. Without spatial smart filters the rendered pixels are shifted, not resampled.\n */\nexport function offsetSmartObject(layers: readonly ImageLayer[], id: string, dx: number, dy: number, canvas: SmartCanvas): ImageLayer[] {\n const layer = requireSmartObject(layers, id);\n requireUnlocked(layer, 'position', 'move it');\n const mx = Math.round(dx);\n const my = Math.round(dy);\n if (!mx && !my) return layers as ImageLayer[];\n const corners = layer.placement.corners.map((p) => ({ x: p.x + mx, y: p.y + my })) as unknown as Quad;\n let next: SmartObjectLayer = { ...layer, placement: { ...layer.placement, corners } };\n const moveMask = (m: RasterMask | undefined) => (m && m.linked !== false && !m.source ? { ...m, surface: translateSurface(m.surface, mx, my) } : m);\n if (layer.mask) next.mask = moveMask(layer.mask);\n if (layer.smartFilters?.mask) next.smartFilters = { ...layer.smartFilters, mask: moveMask(layer.smartFilters.mask) };\n // Shifting is exact unless a filter reads across the canvas edge, or the render was cut at its margin.\n const spatial = activeSmartFilters(layer.smartFilters).some((f) => f.kind === 'blur');\n const b = contentBounds(layer.surface);\n const m = SMART_RENDER_MARGIN;\n const cut = b && (b.x <= -m || b.y <= -m || b.x + b.width >= canvas.width + m || b.y + b.height >= canvas.height + m);\n if (!spatial && !cut && !(next.smartFilters?.mask && activeSmartFilters(next.smartFilters).length && next.smartFilters.mask.linked === false)) {\n next = { ...next, surface: translateSurface(layer.surface, mx, my) };\n } else {\n next = rerender(next, canvas, 'move');\n }\n return mapLayer(layers, id, () => next);\n}\n\n// ---------------------------------------------------------------------------\n// Convert, rasterize, copy\n// ---------------------------------------------------------------------------\n\n/** Moves a layer (and everything in it) by whole pixels, for nesting it in contents. */\nfunction translateLayer(layer: ImageLayer, dx: number, dy: number): ImageLayer {\n if (!dx && !dy) return layer;\n const moveMask = (m: RasterMask | undefined): RasterMask | undefined => {\n if (!m) return m;\n if (m.source) fail(`Layer \"${layer.name || layer.id}\" has a depth mask, which cannot move into a smart object; bake or remove it first.`);\n return { ...m, surface: translateSurface(m.surface, dx, dy) };\n };\n switch (layer.type) {\n case 'pixel':\n // Pixels and the mask are in layer space; moving the offset moves both.\n return { ...layer, offset: { x: layer.offset.x + dx, y: layer.offset.y + dy } };\n case 'smart': {\n const next: SmartObjectLayer = {\n ...layer,\n placement: { ...layer.placement, corners: layer.placement.corners.map((p) => ({ x: p.x + dx, y: p.y + dy })) as unknown as Quad },\n surface: translateSurface(layer.surface, dx, dy),\n mask: moveMask(layer.mask),\n };\n if (!next.mask) delete next.mask;\n if (layer.smartFilters?.mask) next.smartFilters = { ...layer.smartFilters, mask: moveMask(layer.smartFilters.mask) };\n return next;\n }\n case 'group': {\n const next = { ...layer, children: layer.children.map((c) => translateLayer(c, dx, dy)), mask: moveMask(layer.mask) };\n if (!next.mask) delete next.mask;\n return next;\n }\n case 'adjustment': {\n const next = { ...layer, mask: moveMask(layer.mask) } as ImageLayer;\n if (!next.mask) delete next.mask;\n return next;\n }\n case 'annotation':\n return fail(`Annotation layer \"${layer.name || layer.id}\" cannot go into a smart object; rasterize it first.`);\n }\n}\n\n/** Integer document bounds a layer draws into (effects included), or `null` for none; the canvas for layers without bounds. */\nfunction layerExtent(layer: ImageLayer, surfaceOf: (l: PixelLayer) => PixelSurface, canvas: SmartCanvas): CropBox | null {\n const full = { x: 0, y: 0, width: canvas.width, height: canvas.height };\n let b: CropBox | null = null;\n if (layer.type === 'pixel' || layer.type === 'smart') {\n const s = layer.type === 'pixel' ? surfaceOf(layer) : layer.surface;\n if (s.defaultValue !== 0) b = full;\n else {\n const c = contentBounds(s);\n b = c ? { x: c.x + layer.offset.x, y: c.y + layer.offset.y, width: c.width, height: c.height } : null;\n }\n } else if (layer.type === 'group') {\n for (const c of layer.children) b = union(b, layerExtent(c, surfaceOf, canvas));\n } else {\n b = full;\n }\n const m = effectsMargin(layer.effects);\n if (b && m > 0) b = { x: b.x - Math.ceil(m), y: b.y - Math.ceil(m), width: b.width + 2 * Math.ceil(m), height: b.height + 2 * Math.ceil(m) };\n return b;\n}\n\nfunction union(a: CropBox | null, b: CropBox | null): CropBox | null {\n if (!a) return b;\n if (!b) return a;\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n\n/** Plain smart filters from a pixel layer's filters (normal, 100 %). */\nfunction asSmartFilters(filters: readonly LayerFilter[] | undefined): SmartFilterStack | undefined {\n if (!filters?.length) return undefined;\n return { enabled: true, filters: filters.map((f) => ({ ...f, blend: 'normal' as BlendMode, opacity: 1 })) };\n}\n\nexport interface ConvertOptions {\n /** The pixels of a pixel layer (resolves the Background's document source). */\n surfaceOf(layer: PixelLayer): PixelSurface;\n /** Name of the new layer (default: the top layer's name). */\n name?: string;\n}\n\n/**\n * Layer › Smart Objects › Convert to Smart Object: sibling layers become one smart object at the\n * position of the topmost one.\n *\n * - One pixel layer becomes raster contents (its pixels, cropped to their bounds) and keeps its\n * id, name, visibility, opacity, fill, blend, clipping, mask and effects; its filters become\n * smart filters. It renders exactly as before.\n * - Anything else (several layers, a group, an adjustment, a smart object) becomes a nested\n * document holding the layers as they were, sized to their bounds (effects included), and the\n * new layer is a plain Normal layer named after the top one, rendering the isolated composite.\n *\n * Returns the stack, the new layer and its contents.\n */\nexport function convertToSmartObject(\n layers: readonly ImageLayer[],\n ids: readonly string[],\n canvas: SmartCanvas,\n options: ConvertOptions\n): { layers: ImageLayer[]; layer: SmartObjectLayer } {\n if (!ids.length) fail('Give at least one layer id to convert.');\n const locs = [...new Set(ids)].map((id) => requireLayer(layers, id));\n const parent = locs[0].parent?.id ?? null;\n if (locs.some((l) => (l.parent?.id ?? null) !== parent)) fail('Layers to convert must share one parent; move them next to each other first.');\n for (const l of locs) requireUnlocked(l.layer, 'all', 'convert it');\n const ordered = locs.sort((a, b) => a.index - b.index);\n const top = ordered[ordered.length - 1];\n\n let smart: SmartObjectLayer;\n const single = ordered.length === 1 ? ordered[0].layer : null;\n if (single && single.type === 'pixel') {\n const surface = options.surfaceOf(single);\n if (surface.defaultValue !== 0) fail(`Layer \"${single.name || single.id}\" has no transparent area to crop to; it cannot be converted.`);\n const b = contentBounds(surface);\n if (!b) fail(`Layer \"${single.name || single.id}\" has no pixels to convert; paint something first.`);\n const contents: SmartContents = {\n id: newContentsId(),\n name: `${single.name || 'Layer'}.png`,\n width: b!.width,\n height: b!.height,\n kind: 'raster',\n raster: translateSurface(surface, -b!.x, -b!.y),\n };\n smart = {\n type: 'smart',\n id: single.id,\n name: single.name,\n visible: single.visible,\n opacity: single.opacity,\n blend: single.blend,\n contents,\n placement: placementAt(contents, b!.x + single.offset.x, b!.y + single.offset.y),\n surface: emptySurface(4),\n offset: { x: 0, y: 0 },\n };\n if (single.clipped) smart.clipped = true;\n if (single.fillOpacity !== undefined) smart.fillOpacity = single.fillOpacity;\n if (single.effects) smart.effects = single.effects;\n if (single.mask) {\n // Masks of pixel layers are in layer space; a smart object's are in document space.\n smart.mask = single.mask.source ? single.mask : { ...single.mask, surface: translateSurface(single.mask.surface, single.offset.x, single.offset.y) };\n }\n const filters = asSmartFilters(single.filters);\n if (filters) smart.smartFilters = filters;\n } else {\n let b: CropBox | null = null;\n for (const l of ordered) b = union(b, layerExtent(l.layer, options.surfaceOf, canvas));\n if (!b || !(b.width > 0 && b.height > 0)) fail('The layers have no pixels to convert; paint something first.');\n const nested = ordered.map((l) => {\n let layer = l.layer;\n if (layer.type === 'pixel' && layer.source === 'document') {\n const resolved = { ...layer, surface: options.surfaceOf(layer) };\n delete resolved.source;\n layer = resolved;\n }\n if (flattenLayers([layer]).some((x) => x.type === 'pixel' && x.source === 'document')) fail(`Layer \"${layer.name || layer.id}\" holds the document image inside a group; convert the Background on its own.`);\n return translateLayer(layer, -b!.x, -b!.y);\n });\n // The bottom layer has nothing below it to clip to inside the contents.\n if (nested[0].clipped) {\n const first = { ...nested[0] };\n delete first.clipped;\n nested[0] = first;\n }\n const name = options.name?.trim() || top.layer.name || 'Smart Object';\n const contents = documentContents({ layers: nested }, b!.width, b!.height, `${name}.psb`);\n smart = {\n type: 'smart',\n id: ordered.length === 1 ? ordered[0].layer.id : newLayerId('smart'),\n name,\n visible: true,\n opacity: 1,\n blend: 'normal',\n contents,\n placement: placementAt(contents, b!.x, b!.y),\n surface: emptySurface(4),\n offset: { x: 0, y: 0 },\n };\n }\n if (options.name?.trim()) smart.name = options.name.trim();\n smart = { ...smart, surface: renderSmartObject(smart, canvas) };\n const below = ordered.filter((l) => l.index < top.index).length;\n let out = layers as ImageLayer[];\n for (const l of ordered) out = mapLayer(out, l.layer.id, () => null);\n return { layers: insertLayer(out, smart, parent, top.index - below), layer: smart };\n}\n\n/**\n * Layer › Smart Objects › Rasterize: the smart object becomes a pixel layer holding what it\n * renders (filters baked), with its mask, effects, blend, opacity and fill. Same id.\n */\nexport function rasterizeSmartObject(layers: readonly ImageLayer[], id: string): { layers: ImageLayer[]; layer: PixelLayer } {\n const layer = requireSmartObject(layers, id);\n requireUnlocked(layer, 'all', 'rasterize it');\n const out: PixelLayer = {\n type: 'pixel',\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend: layer.blend,\n surface: layer.surface,\n offset: { x: 0, y: 0 },\n };\n if (layer.clipped) out.clipped = true;\n if (layer.mask) out.mask = layer.mask;\n if (layer.locks) out.locks = layer.locks;\n if (layer.effects) out.effects = layer.effects;\n if (layer.fillOpacity !== undefined) out.fillOpacity = layer.fillOpacity;\n return { layers: mapLayer(layers, id, () => out), layer: out };\n}\n\n/**\n * Layer › Smart Objects › New Smart Object via Copy (`linked: false`, fresh contents that edit\n * independently) or a linked duplicate (`linked: true`, shared contents), directly above.\n */\nexport function copySmartObject(layers: readonly ImageLayer[], id: string, linked: boolean, name?: string): { layers: ImageLayer[]; layer: SmartObjectLayer } {\n const loc = requireLayer(layers, id);\n const layer = requireSmartObject(layers, id);\n const copy: SmartObjectLayer = { ...layer, id: newLayerId('smart'), name: name?.trim() || `${layer.name || 'Smart Object'} copy` };\n delete copy.locks;\n delete copy.psd;\n if (!linked) copy.contents = { ...layer.contents, id: newContentsId() };\n if (copy.smartFilters) copy.smartFilters = { ...copy.smartFilters, filters: copy.smartFilters.filters.map((f) => ({ ...f, id: newLayerId('flt') })) };\n return { layers: insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1), layer: copy };\n}\n\n/** Ids of the smart objects in the stack placed from contents `contentsId` (linked instances), bottom first. */\nexport function instancesOf(layers: readonly ImageLayer[], contentsId: string): string[] {\n return flattenLayers(layers)\n .filter((l): l is SmartObjectLayer => l.type === 'smart' && l.contents.id === contentsId)\n .map((l) => l.id);\n}\n\n/**\n * Puts new contents into every instance of `contentsId` and renders them again. With\n * `keepPlacement` false (Replace Contents of a different size), each instance keeps its scale and\n * rotation about the contents' centre, and a warp is stretched to the new size.\n */\nexport function replaceContents(\n layers: readonly ImageLayer[],\n contentsId: string,\n contents: SmartContents,\n canvas: SmartCanvas\n): { layers: ImageLayer[]; ids: string[] } {\n const ids = instancesOf(layers, contentsId);\n if (!ids.length) fail(`No smart object uses contents \"${contentsId}\".`);\n for (const id of ids) requireUnlocked(findLayer(layers, id)!.layer, 'all', 'replace its contents');\n const next = { ...contents, id: contentsId };\n let out = layers as ImageLayer[];\n for (const id of ids) {\n out = mapLayer(out, id, (l) => {\n const layer = l as SmartObjectLayer;\n return rerender({ ...layer, contents: next, placement: refitPlacement(layer, next) }, canvas, 'take the new contents');\n });\n }\n return { layers: out, ids };\n}\n\n/** A placement for new contents of another size: the same map about the contents' centre (warp stretched). */\nfunction refitPlacement(layer: SmartObjectLayer, contents: Pick<SmartContents, 'width' | 'height'>): SmartPlacement {\n const W0 = layer.contents.width;\n const H0 = layer.contents.height;\n const W1 = contents.width;\n const H1 = contents.height;\n if (W0 === W1 && H0 === H1) return layer.placement;\n const g = placementGeometry(layer);\n const h = transformHomography(g);\n if (!h) return placementAt(contents, 0, 0);\n // The new rectangle centred on the old one, through the old corner map.\n const shift: Homography = [1, 0, (W0 - W1) / 2, 0, 1, (H0 - H1) / 2, 0, 0, 1];\n const hn = multiplyHomographies(shift, h);\n const pts = boxQuad({ x: 0, y: 0, width: W1, height: H1 }).map((p) => {\n const w = hn[6] * p.x + hn[7] * p.y + hn[8];\n return { x: (hn[0] * p.x + hn[1] * p.y + hn[2]) / w, y: (hn[3] * p.x + hn[4] * p.y + hn[5]) / w };\n });\n const out: SmartPlacement = { corners: pts as unknown as Quad };\n if (layer.placement.interpolation) out.interpolation = layer.placement.interpolation;\n if (layer.placement.warp) out.warp = mapWarpMeshPoints(layer.placement.warp, (p) => ({ x: (p.x * W1) / W0, y: (p.y * H1) / H0 }));\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Smart filters\n// ---------------------------------------------------------------------------\n\n/** A clean smart filter from loose input: a `develop` or `blur` filter plus blend and opacity. */\nexport function normalizeSmartFilter(raw: Record<string, unknown> & { kind: string }, id = newLayerId('flt')): SmartFilter {\n if (raw.kind === 'psd') fail('Photoshop filters can only come from a PSD; add develop or blur.');\n const base = normalizeFilter(raw as Partial<LayerFilter> & { kind: LayerFilter['kind'] }, id);\n const blend = raw.blend ?? 'normal';\n if (!isBlendMode(blend) || blend === 'pigment') fail(`Unknown blend mode \"${String(blend)}\" for a smart filter.`);\n const opacity = raw.opacity ?? 1;\n if (!(typeof opacity === 'number' && opacity >= 0 && opacity <= 1)) fail('opacity must be a number from 0 to 1.');\n return { ...base, blend: blend as BlendMode, opacity: opacity as number };\n}\n\nfunction withStack(layer: SmartObjectLayer, stack: SmartFilterStack | undefined, canvas: SmartCanvas): SmartObjectLayer {\n const next = { ...layer };\n if (stack && (stack.filters.length || stack.mask || !stack.enabled)) next.smartFilters = stack;\n else delete next.smartFilters;\n return rerender(next, canvas, 'change its smart filters');\n}\n\nfunction stackOf(layer: SmartObjectLayer): SmartFilterStack {\n return layer.smartFilters ?? { enabled: true, filters: [] };\n}\n\nfunction requireFilter(layer: SmartObjectLayer, filterId: string): number {\n const list = layer.smartFilters?.filters ?? [];\n const i = list.findIndex((f) => f.id === filterId);\n if (i === -1) fail(`Smart object \"${layer.name || layer.id}\" has no smart filter \"${filterId}\". Filters: ${list.map((f) => `${f.id} (${f.kind})`).join(', ') || 'none'}; call get_smart_object.`);\n return i;\n}\n\nfunction editableSmart(layers: readonly ImageLayer[], id: string): SmartObjectLayer {\n const layer = requireSmartObject(layers, id);\n requireUnlocked(layer, 'all', 'change its smart filters');\n return layer;\n}\n\n/** Adds a smart filter at `index` (default the top). Returns the stack and the filter. */\nexport function addSmartFilter(\n layers: readonly ImageLayer[],\n id: string,\n filter: SmartFilter,\n canvas: SmartCanvas,\n index?: number\n): { layers: ImageLayer[]; layer: SmartObjectLayer; filter: SmartFilter } {\n const layer = editableSmart(layers, id);\n const stack = stackOf(layer);\n const filters = [...stack.filters];\n const at = index === undefined ? filters.length : Math.max(0, Math.min(filters.length, Math.floor(index)));\n filters.splice(at, 0, filter);\n const next = withStack(layer, { ...stack, filters }, canvas);\n return { layers: mapLayer(layers, id, () => next), layer: next, filter };\n}\n\n/** Changes one smart filter (settings, enabled, blend, opacity; validated). */\nexport function updateSmartFilter(\n layers: readonly ImageLayer[],\n id: string,\n filterId: string,\n changes: Record<string, unknown>,\n canvas: SmartCanvas\n): { layers: ImageLayer[]; layer: SmartObjectLayer; filter: SmartFilter } {\n const layer = editableSmart(layers, id);\n const i = requireFilter(layer, filterId);\n const current = layer.smartFilters!.filters[i];\n if ('kind' in changes && changes.kind !== current.kind) fail('A smart filter kind cannot change; remove it and add another.');\n let merged: SmartFilter;\n if (current.kind === 'psd') {\n const extra = Object.keys(changes).filter((k) => !['enabled', 'blend', 'opacity', 'kind'].includes(k));\n if (extra.length) fail(`\"${current.name}\" is a Photoshop filter we keep but do not model; only enabled, blend and opacity change. Unknown: ${extra.join(', ')}.`);\n const blend = changes.blend ?? current.blend;\n if (!isBlendMode(blend) || blend === 'pigment') fail(`Unknown blend mode \"${String(blend)}\" for a smart filter.`);\n const opacity = changes.opacity ?? current.opacity;\n if (!(typeof opacity === 'number' && opacity >= 0 && opacity <= 1)) fail('opacity must be a number from 0 to 1.');\n merged = { ...current, enabled: changes.enabled === undefined ? current.enabled : Boolean(changes.enabled), blend: blend as BlendMode, opacity: opacity as number };\n } else {\n merged = normalizeSmartFilter({ ...current, ...changes, kind: current.kind } as Record<string, unknown> & { kind: string }, current.id);\n }\n const filters = [...layer.smartFilters!.filters];\n filters[i] = merged;\n const next = withStack(layer, { ...layer.smartFilters!, filters }, canvas);\n return { layers: mapLayer(layers, id, () => next), layer: next, filter: merged };\n}\n\n/** Removes one smart filter. */\nexport function removeSmartFilter(layers: readonly ImageLayer[], id: string, filterId: string, canvas: SmartCanvas): { layers: ImageLayer[]; layer: SmartObjectLayer } {\n const layer = editableSmart(layers, id);\n requireFilter(layer, filterId);\n const stack = layer.smartFilters!;\n const filters = stack.filters.filter((f) => f.id !== filterId);\n // The filter mask goes with the last filter, as in Photoshop.\n const next = withStack(layer, filters.length ? { ...stack, filters } : stack.enabled ? undefined : { enabled: false, filters }, canvas);\n return { layers: mapLayer(layers, id, () => next), layer: next };\n}\n\n/** Moves one smart filter to `index` (0 = bottom, applied first). */\nexport function moveSmartFilter(layers: readonly ImageLayer[], id: string, filterId: string, index: number, canvas: SmartCanvas): { layers: ImageLayer[]; layer: SmartObjectLayer } {\n const layer = editableSmart(layers, id);\n const i = requireFilter(layer, filterId);\n const list = layer.smartFilters!.filters;\n if (!Number.isInteger(index) || index < 0 || index >= list.length) fail(`index must be a whole number from 0 to ${list.length - 1}.`);\n const filters = [...list];\n const [f] = filters.splice(i, 1);\n filters.splice(index, 0, f);\n const next = withStack(layer, { ...layer.smartFilters!, filters }, canvas);\n return { layers: mapLayer(layers, id, () => next), layer: next };\n}\n\n/** Turns the whole smart filter stack on or off (each filter keeps its own switch). */\nexport function setSmartFiltersEnabled(layers: readonly ImageLayer[], id: string, enabled: boolean, canvas: SmartCanvas): { layers: ImageLayer[]; layer: SmartObjectLayer } {\n const layer = editableSmart(layers, id);\n const next = withStack(layer, { ...stackOf(layer), enabled: Boolean(enabled) }, canvas);\n return { layers: mapLayer(layers, id, () => next), layer: next };\n}\n\n/** Sets or removes (`null`) the smart filter mask. */\nexport function setSmartFilterMask(layers: readonly ImageLayer[], id: string, mask: RasterMask | null, canvas: SmartCanvas): { layers: ImageLayer[]; layer: SmartObjectLayer } {\n const layer = editableSmart(layers, id);\n if (mask?.source) fail('A smart filter mask is painted, not generated; use a raster mask.');\n const stack = { ...stackOf(layer) };\n if (mask) stack.mask = mask;\n else delete stack.mask;\n const next = withStack(layer, stack, canvas);\n return { layers: mapLayer(layers, id, () => next), layer: next };\n}\n\n/** A smart object's filter mask surface replaced (painting it), rendered again. */\nexport function withFilterMaskSurface(layer: SmartObjectLayer, surface: PixelSurface, canvas: SmartCanvas): SmartObjectLayer {\n const stack = stackOf(layer);\n if (!stack.mask) fail(`Smart object \"${layer.name || layer.id}\" has no filter mask; add one with set_smart_filter_mask.`);\n return withStack(layer, { ...stack, mask: { ...stack.mask!, surface } }, canvas);\n}\n\n/** The transformed outline of a smart object's contents (document bounds, warp included). */\nexport function placementBounds(layer: Pick<SmartObjectLayer, 'contents' | 'placement'>): CropBox | null {\n return transformedBox(placementGeometry(layer));\n}\n","/**\n * Schema version 3 of the image editor document: the layer tree, with pixel layers and painted\n * masks stored as PNG assets (cropped to their content and placed by position), plus the\n * document-level crop, transform, output opacity, migrated blur and selection.\n *\n * Encoding is synchronous and exact (see `png.ts`). Each surface's encoding is memoised, so\n * saving an unchanged layer again costs nothing.\n */\n\nimport { normalizeAdjustmentLayer } from '../adjustment-layers';\nimport { isBlendMode, normalizeBlendMode } from '../blend';\nimport { normalizeProcessVersion } from '../color';\nimport type { Annotation, CropBox, DepthMaskOptions, ImageTransform } from '../types';\nimport type { SerializedSelection } from '../selection/mask';\nimport { DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport {\n newLayerId,\n type PsdSmartFilter,\n type SmartContents,\n type SmartFilter,\n type SmartFilterStack,\n type SmartObjectLayer,\n type SmartPlacement,\n type AdjustmentLayerNode,\n type GroupLayer,\n type ImageLayer,\n type LayerBase,\n type LayerFilter,\n type LayerLocks,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { base64ToBytes, bytesToBase64, decodePng, parseDataUrl, pngDataUrl } from '../../raster/png';\nimport { normalizeLayerEffects, type GlobalLight, type LayerEffect, type LayerEffects } from '../../raster/effects/model';\nimport type { EffectPattern } from '../../raster/effects/patterns';\nimport { contentBounds, emptySurface, readRegion, surfaceFromPixels, type TiledSurface } from '../../raster/surface';\nimport { isInterpolation } from '../../raster/transform';\nimport { isValidWarpMesh } from '../../geom/warp';\nimport type { Quad, WarpMesh } from '../../geom/types';\nimport { normalizePlacement, renderSmartObject } from './smart';\n\nexport const LAYERED_SCHEMA_VERSION = 3 as const;\n\n/** A stored raster mask. Pixels: `asset` (grey PNG) placed at `x`, `y`; elsewhere `defaultValue`. */\nexport interface SerializedRasterMask {\n defaultValue: 0 | 255;\n asset?: string;\n x?: number;\n y?: number;\n density?: number;\n feather?: number;\n enabled?: boolean;\n /** `false`: unlinked from the layer's pixels. */\n linked?: boolean;\n source?: DepthMaskOptions;\n}\n\ninterface SerializedLayerBase {\n id: string;\n name: string;\n visible: boolean;\n opacity: number;\n blend: string;\n clipped?: boolean;\n mask?: SerializedRasterMask;\n locks?: LayerLocks;\n effects?: SerializedLayerEffects;\n}\n\n/** A layer's effects: the effect list as JSON plus the PSD block they came from (base64). */\nexport interface SerializedLayerEffects {\n enabled: boolean;\n scale?: number;\n items: LayerEffect[];\n psd?: { key: string; data: string };\n}\n\nexport interface SerializedPixelLayer extends SerializedLayerBase {\n type: 'pixel';\n /** Asset id of the pixels (RGBA PNG) placed at `x`, `y` in layer space; missing when empty. */\n asset?: string;\n x?: number;\n y?: number;\n offset: { x: number; y: number };\n fillOpacity?: number;\n filters?: LayerFilter[];\n /** `'document'`: the pixels are the document's source image (no asset is stored). */\n source?: 'document';\n}\n\nexport interface SerializedAdjustmentLayer extends SerializedLayerBase {\n type: 'adjustment';\n kind: string;\n params: unknown;\n}\n\nexport interface SerializedGroupLayer extends SerializedLayerBase {\n type: 'group';\n children: SerializedLayer[];\n collapsed?: boolean;\n}\n\nexport interface SerializedAnnotationLayer extends SerializedLayerBase {\n type: 'annotation';\n annotation: Annotation;\n}\n\n/** A smart filter as stored (a Photoshop filter keeps its descriptor as base64). */\nexport type SerializedSmartFilter = (LayerFilter | (Omit<PsdSmartFilter, 'data'> & { data: string })) & { blend: string; opacity: number };\n\nexport interface SerializedSmartObjectLayer extends SerializedLayerBase {\n type: 'smart';\n /** Id of its entry in the document's `smartContents` (shared by linked instances). */\n contents: string;\n placement: { corners: Array<{ x: number; y: number }>; warp?: WarpMesh; interpolation?: string };\n smartFilters?: { enabled: boolean; filters: SerializedSmartFilter[]; mask?: SerializedRasterMask };\n /** The rendered pixels (RGBA PNG) at `x`, `y` in document space, so a load does not re-render. */\n asset?: string;\n x?: number;\n y?: number;\n fillOpacity?: number;\n /** The PSD placed-layer block kept for round trips (base64). */\n psd?: { key: string; data: string; placed?: string };\n}\n\n/** Smart object contents as stored once per document, referenced by id from every instance. */\nexport interface SerializedSmartContents {\n id: string;\n name: string;\n width: number;\n height: number;\n kind: 'raster' | 'document';\n /** Raster contents: RGBA PNG asset at (0, 0). */\n asset?: string;\n /** Document contents: the nested layers (their assets are in the document's `assets`). */\n layers?: SerializedLayer[];\n globalLight?: GlobalLight;\n patterns?: SerializedPattern[];\n /** The file the contents came from (base64), written back while unedited. */\n file?: { type: string; data: string };\n linked?: { path: string };\n}\n\nexport type SerializedLayer = SerializedPixelLayer | SerializedAdjustmentLayer | SerializedGroupLayer | SerializedAnnotationLayer | SerializedSmartObjectLayer;\n\n/** A version 3 document (layers). */\nexport interface SerializedLayeredImageEditorState {\n version: 3;\n /** Canvas size in pixels. */\n width: number;\n height: number;\n /** Bottom to top. */\n layers: SerializedLayer[];\n /** Asset id → PNG data URL (or an external reference, see `ImageEditorAssetStore`). */\n assets: Record<string, string>;\n activeLayerId: string | null;\n crop: CropBox | null;\n transform: ImageTransform;\n /** Export alpha, 0..1. */\n opacity: number;\n /** Blur migrated from a v1 / v2 document with adjustment layers (runs after the layers). */\n legacyOutputBlur?: number;\n selection?: SerializedSelection;\n /** Global light for layer effects (missing = 120°, 30°). */\n globalLight?: GlobalLight;\n /** The document's patterns for pattern effects, each an RGBA PNG data URL. */\n patterns?: SerializedPattern[];\n /** Contents of the smart objects (nested ones included), each stored once. */\n smartContents?: SerializedSmartContents[];\n}\n\nexport interface SerializedPattern {\n id: string;\n name: string;\n png: string;\n}\n\nexport function serializePatterns(patterns: readonly EffectPattern[]): SerializedPattern[] {\n return patterns.map((p) => ({ id: p.id, name: p.name, png: pngDataUrl(p.width, p.height, p.data) }));\n}\n\n/** Reads stored patterns, skipping (and reporting in `problems`) any that cannot be decoded. */\nexport function deserializePatterns(raw: unknown, problems: string[] = []): EffectPattern[] {\n if (!Array.isArray(raw)) return [];\n const out: EffectPattern[] = [];\n for (const p of raw) {\n if (!p || typeof p !== 'object' || typeof p.id !== 'string' || typeof p.png !== 'string') continue;\n try {\n const png = decodePngDataUrl(p.png, 4);\n out.push({ id: p.id, name: typeof p.name === 'string' ? p.name : p.id, width: png.width, height: png.height, data: png.data });\n } catch (e) {\n problems.push(`Pattern \"${p.name ?? p.id}\" could not be decoded (${e instanceof Error ? e.message : String(e)}).`);\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Writing\n// ---------------------------------------------------------------------------\n\ninterface EncodedSurface {\n url: string;\n x: number;\n y: number;\n}\n\nconst encodedCache = new WeakMap<TiledSurface, EncodedSurface | null>();\n\n/** The surface's content (non-default pixels) as a PNG data URL and its position, or `null`. */\nexport function encodeSurface(surface: TiledSurface): EncodedSurface | null {\n if (encodedCache.has(surface)) return encodedCache.get(surface)!;\n const b = contentBounds(surface);\n let out: EncodedSurface | null = null;\n if (b) {\n const data = readRegion(surface, b);\n out = { url: pngDataUrl(b.width, b.height, data, surface.channels), x: b.x, y: b.y };\n }\n encodedCache.set(surface, out);\n return out;\n}\n\nexport interface SerializeContext {\n /** Adds an asset and returns its id. */\n addAsset(url: string, hint: string): string;\n /** Stores a smart object's contents (once per id). Without it, smart objects cannot be written. */\n addContents?(contents: SmartContents): void;\n}\n\nfunction baseFields(layer: ImageLayer, ctx: SerializeContext): SerializedLayerBase {\n const out: SerializedLayerBase = {\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend: layer.blend,\n };\n if (layer.clipped) out.clipped = true;\n if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);\n if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };\n if (layer.effects) out.effects = serializeEffects(layer.effects);\n return out;\n}\n\nexport function serializeEffects(effects: LayerEffects): SerializedLayerEffects {\n const out: SerializedLayerEffects = { enabled: effects.enabled, items: JSON.parse(JSON.stringify(effects.items)) };\n if (effects.scale !== undefined && effects.scale !== 1) out.scale = effects.scale;\n if (effects.psd) out.psd = { key: effects.psd.key, data: bytesToBase64(effects.psd.data) };\n return out;\n}\n\n/** Reads stored effects (leniently: broken effects are dropped), or `undefined`. */\nexport function deserializeEffects(raw: unknown): LayerEffects | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const r = raw as Record<string, unknown>;\n let psd: { key: string; data: Uint8Array } | undefined;\n const p = r.psd as Record<string, unknown> | undefined;\n if (p && typeof p.key === 'string' && typeof p.data === 'string') {\n try {\n psd = { key: p.key, data: base64ToBytes(p.data) };\n } catch {\n psd = undefined;\n }\n }\n return normalizeLayerEffects({ ...r, psd });\n}\n\nfunction serializeMask(mask: RasterMask, hint: string, ctx: SerializeContext): SerializedRasterMask {\n const out: SerializedRasterMask = { defaultValue: mask.defaultValue };\n if (!mask.source) {\n const enc = encodeSurface(mask.surface);\n if (enc) {\n out.asset = ctx.addAsset(enc.url, hint);\n out.x = enc.x;\n out.y = enc.y;\n }\n } else {\n out.source = { ...mask.source, depthRange: [...mask.source.depthRange] as [number, number] };\n }\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n if (mask.linked === false) out.linked = false;\n return out;\n}\n\nexport function serializeLayer(layer: ImageLayer, ctx: SerializeContext): SerializedLayer {\n const base = baseFields(layer, ctx);\n switch (layer.type) {\n case 'pixel': {\n const out: SerializedPixelLayer = { ...base, type: 'pixel', offset: { ...layer.offset } };\n if (layer.source === 'document') out.source = 'document';\n else {\n const enc = encodeSurface(layer.surface);\n if (enc) {\n out.asset = ctx.addAsset(enc.url, layer.id);\n out.x = enc.x;\n out.y = enc.y;\n }\n }\n if (layer.fillOpacity !== undefined && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;\n if (layer.filters?.length) out.filters = JSON.parse(JSON.stringify(layer.filters));\n return out;\n }\n case 'adjustment':\n return { ...base, type: 'adjustment', kind: layer.kind, params: JSON.parse(JSON.stringify(layer.params)) };\n case 'group': {\n const out: SerializedGroupLayer = { ...base, type: 'group', children: layer.children.map((c) => serializeLayer(c, ctx)) };\n if (layer.collapsed) out.collapsed = true;\n return out;\n }\n case 'annotation':\n return { ...base, type: 'annotation', annotation: JSON.parse(JSON.stringify(layer.annotation)) };\n case 'smart':\n return serializeSmartObject(layer, base, ctx);\n }\n}\n\nfunction serializeSmartFilters(stack: SmartFilterStack, layerId: string, ctx: SerializeContext): NonNullable<SerializedSmartObjectLayer['smartFilters']> {\n const out: NonNullable<SerializedSmartObjectLayer['smartFilters']> = {\n enabled: stack.enabled,\n filters: stack.filters.map((f) => (f.kind === 'psd' ? { ...f, data: bytesToBase64(f.data) } : JSON.parse(JSON.stringify(f)))),\n };\n if (stack.mask) out.mask = serializeMask(stack.mask, `${layerId}-filter-mask`, ctx);\n return out;\n}\n\nfunction serializeSmartObject(layer: SmartObjectLayer, base: SerializedLayerBase, ctx: SerializeContext): SerializedSmartObjectLayer {\n if (!ctx.addContents) throw new Error('Smart objects need a contents table to be written.');\n ctx.addContents(layer.contents);\n const p = layer.placement;\n const out: SerializedSmartObjectLayer = {\n ...base,\n type: 'smart',\n contents: layer.contents.id,\n placement: { corners: p.corners.map((c) => ({ x: c.x, y: c.y })) },\n };\n if (p.warp) out.placement.warp = JSON.parse(JSON.stringify(p.warp));\n if (p.interpolation && p.interpolation !== 'bicubic') out.placement.interpolation = p.interpolation;\n if (layer.smartFilters) out.smartFilters = serializeSmartFilters(layer.smartFilters, layer.id, ctx);\n const enc = encodeSurface(layer.surface);\n if (enc) {\n out.asset = ctx.addAsset(enc.url, layer.id);\n out.x = enc.x;\n out.y = enc.y;\n }\n if (layer.fillOpacity !== undefined && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;\n if (layer.psd) {\n out.psd = { key: layer.psd.key, data: bytesToBase64(layer.psd.data) };\n if (layer.psd.placed) out.psd.placed = layer.psd.placed;\n }\n return out;\n}\n\n/** Writes one contents entry (nested layers add their own assets and contents to `ctx`). */\nexport function serializeContents(c: SmartContents, ctx: SerializeContext): SerializedSmartContents {\n const out: SerializedSmartContents = { id: c.id, name: c.name, width: c.width, height: c.height, kind: c.kind };\n if (c.kind === 'raster' && c.raster) {\n const enc = encodeSurface(c.raster);\n if (enc) {\n if (enc.x !== 0 || enc.y !== 0) {\n // Contents always start at (0, 0): store the full rectangle.\n const data = readRegion(c.raster, { x: 0, y: 0, width: c.width, height: c.height });\n out.asset = ctx.addAsset(pngDataUrl(c.width, c.height, data, 4), `contents-${c.id}`);\n } else out.asset = ctx.addAsset(enc.url, `contents-${c.id}`);\n }\n }\n if (c.kind === 'document' && c.document) {\n out.layers = c.document.layers.map((l) => serializeLayer(l, ctx));\n if (c.document.globalLight) out.globalLight = { ...c.document.globalLight };\n if (c.document.patterns?.length) out.patterns = serializePatterns(c.document.patterns);\n }\n if (c.file) out.file = { type: c.file.type, data: bytesToBase64(c.file.bytes) };\n if (c.linked) out.linked = { path: c.linked.path };\n return out;\n}\n\n/**\n * Serializes a layer tree with inline assets (asset ids are derived from layer ids). Smart object\n * contents go to `smartContents`, once each (present only when there are smart objects).\n */\nexport function serializeLayers(layers: readonly ImageLayer[]): { layers: SerializedLayer[]; assets: Record<string, string>; smartContents?: SerializedSmartContents[] } {\n const assets: Record<string, string> = {};\n const contents = new Map<string, SerializedSmartContents | null>();\n const ctx: SerializeContext = {\n addAsset(url, hint) {\n let id = hint;\n for (let n = 2; id in assets && assets[id] !== url; n++) id = `${hint}-${n}`;\n assets[id] = url;\n return id;\n },\n addContents(c) {\n if (contents.has(c.id)) return;\n // Reserve the id first: contents can hold instances of themselves only through a cycle, which is cut.\n contents.set(c.id, null);\n contents.set(c.id, serializeContents(c, ctx));\n },\n };\n const out = layers.map((l) => serializeLayer(l, ctx));\n const list = [...contents.values()].filter(Boolean) as SerializedSmartContents[];\n return list.length ? { layers: out, assets, smartContents: list } : { layers: out, assets };\n}\n\n// ---------------------------------------------------------------------------\n// External assets\n// ---------------------------------------------------------------------------\n\n/**\n * Where a host keeps layer pixels outside the JSON (`toJSON({ assets: 'external', store })`):\n * `put` stores a PNG and returns a reference (a URL, a key) that `get` resolves later.\n */\nexport interface ImageEditorAssetStore {\n put(blob: Blob, info: { id: string; width: number; height: number }): Promise<string>;\n get(ref: string): Promise<Blob>;\n}\n\nconst externalRefs = new WeakMap<ImageEditorAssetStore, Map<string, string>>();\n\n/**\n * Serializes a layer tree with its assets in `store`: each PNG is uploaded once per store (by\n * content), and `assets` maps asset ids to the store's references.\n */\nexport async function serializeLayersExternal(\n layers: readonly ImageLayer[],\n store: ImageEditorAssetStore\n): Promise<{ layers: SerializedLayer[]; assets: Record<string, string>; smartContents?: SerializedSmartContents[] }> {\n const inline = serializeLayers(layers);\n let refs = externalRefs.get(store);\n if (!refs) {\n refs = new Map();\n externalRefs.set(store, refs);\n }\n const assets: Record<string, string> = {};\n for (const [id, url] of Object.entries(inline.assets)) {\n let ref = refs.get(url);\n if (!ref) {\n const parsed = parseDataUrl(url)!;\n const png = decodePngHeader(parsed.bytes);\n ref = await store.put(new Blob([parsed.bytes as BlobPart], { type: 'image/png' }), { id, width: png.width, height: png.height });\n if (typeof ref !== 'string' || !ref) throw new Error(`The asset store returned no reference for \"${id}\".`);\n refs.set(url, ref);\n }\n assets[id] = ref;\n }\n return inline.smartContents ? { layers: inline.layers, assets, smartContents: inline.smartContents } : { layers: inline.layers, assets };\n}\n\n/** Width and height from a PNG's header. */\nfunction decodePngHeader(bytes: Uint8Array): { width: number; height: number } {\n const u32 = (o: number) => ((bytes[o] << 24) | (bytes[o + 1] << 16) | (bytes[o + 2] << 8) | bytes[o + 3]) >>> 0;\n return { width: u32(16), height: u32(20) };\n}\n\n/**\n * Turns external asset references back into data URLs through `store.get` (data URLs pass through),\n * so the document loads like an inline one. References that fail are reported and left out.\n */\nexport async function resolveExternalAssets(\n assets: Record<string, string>,\n store: ImageEditorAssetStore | undefined,\n problems: string[]\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const [id, ref] of Object.entries(assets)) {\n if (ref.startsWith('data:')) {\n out[id] = ref;\n continue;\n }\n if (!store) {\n problems.push(`Asset \"${id}\" is external (${ref}) and no asset store was given.`);\n continue;\n }\n try {\n const blob = await store.get(ref);\n const bytes = new Uint8Array(await blob.arrayBuffer());\n out[id] = `data:image/png;base64,${bytesToBase64(bytes)}`;\n } catch (err) {\n problems.push(`Asset \"${id}\" (${ref}) could not be fetched: ${(err as Error).message}`);\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\nexport interface DeserializeContext {\n assets: Record<string, string>;\n /** The stored `smartContents` table (smart objects reference it by id). */\n smartContents?: unknown[];\n /** Canvas size, for re-rendering a smart object stored without its pixels (default 1 × 1). */\n canvas?: { width: number; height: number };\n /** @internal Contents read so far by id (`null` while being read). */\n contentsMemo?: Map<string, SmartContents | null>;\n /** Decodes an asset (PNG data URL by default). Throw for an unusable asset. */\n decodeAsset?(ref: string, channels: 1 | 4): { width: number; height: number; data: ArrayLike<number> };\n /** Problems found while reading (unknown kinds, broken assets); the rest still loads. */\n problems: string[];\n}\n\n/** Decodes a PNG data URL into pixels of `channels` bytes each. */\nexport function decodePngDataUrl(url: string, channels: 1 | 4): { width: number; height: number; data: Uint8ClampedArray } {\n const parsed = parseDataUrl(url);\n if (!parsed) throw new Error('Asset is not a data URL.');\n const png = decodePng(parsed.bytes, { keepGray: channels === 1 });\n if (png.channels === channels) return png;\n if (channels === 1) {\n // An RGBA PNG used as a mask: take its luminance (alpha-weighted).\n const out = new Uint8ClampedArray(png.width * png.height);\n for (let i = 0, p = 0; p < out.length; i += 4, p++) {\n out[p] = Math.round(((0.299 * png.data[i] + 0.587 * png.data[i + 1] + 0.114 * png.data[i + 2]) * png.data[i + 3]) / 255);\n }\n return { width: png.width, height: png.height, data: out };\n }\n const out = new Uint8ClampedArray(png.width * png.height * 4);\n for (let p = 0; p < png.width * png.height; p++) {\n out[p * 4] = out[p * 4 + 1] = out[p * 4 + 2] = png.data[p];\n out[p * 4 + 3] = 255;\n }\n return { width: png.width, height: png.height, data: out };\n}\n\nconst num = (v: unknown, lo: number, hi: number, fallback: number) =>\n typeof v === 'number' && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : fallback;\n\nfunction readSurface(ref: unknown, x: unknown, y: unknown, channels: 1 | 4, defaultValue: number, ctx: DeserializeContext, what: string): TiledSurface {\n if (typeof ref !== 'string' || !ref) return emptySurface(channels, defaultValue);\n const url = ctx.assets[ref];\n if (typeof url !== 'string') {\n ctx.problems.push(`${what}: asset \"${ref}\" is missing`);\n return emptySurface(channels, defaultValue);\n }\n try {\n const px = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);\n return surfaceFromPixels(px, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);\n } catch (err) {\n ctx.problems.push(`${what}: asset \"${ref}\" could not be decoded (${(err as Error).message})`);\n return emptySurface(channels, defaultValue);\n }\n}\n\nfunction readMask(raw: unknown, ctx: DeserializeContext, what: string): RasterMask | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const r = raw as Record<string, unknown>;\n const defaultValue: 0 | 255 = r.defaultValue === 255 ? 255 : 0;\n const mask: RasterMask = {\n surface: r.source ? emptySurface(1, 255) : readSurface(r.asset, r.x, r.y, 1, defaultValue, ctx, `${what} mask`),\n defaultValue: r.source ? 255 : defaultValue,\n };\n if (typeof r.density === 'number' && r.density !== 1) mask.density = num(r.density, 0, 1, 1);\n if (typeof r.feather === 'number' && r.feather > 0) mask.feather = num(r.feather, 0, 1000, 0);\n if (r.enabled === false) mask.enabled = false;\n if (r.linked === false) mask.linked = false;\n if (r.source && typeof r.source === 'object') {\n const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...(r.source as Partial<DepthMaskOptions>) };\n mask.source = { ...s, depthRange: [...s.depthRange] as [number, number] };\n }\n return mask;\n}\n\nfunction readFilters(raw: unknown): LayerFilter[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: LayerFilter[] = [];\n for (const f of raw) {\n if (!f || typeof f !== 'object') continue;\n const r = f as Record<string, unknown>;\n const id = typeof r.id === 'string' && r.id ? r.id : newLayerId('flt');\n const enabled = r.enabled !== false;\n if (r.kind === 'blur') out.push({ id, kind: 'blur', enabled, radius: num(r.radius, 0, 50, 0) });\n else if (r.kind === 'develop') {\n out.push({\n id,\n kind: 'develop',\n enabled,\n brightness: num(r.brightness, -100, 100, 0),\n contrast: num(r.contrast, -100, 100, 0),\n saturation: num(r.saturation, -100, 100, 0),\n exposure: num(r.exposure, -100, 100, 0),\n temperature: num(r.temperature, -100, 100, 0),\n tint: num(r.tint, -100, 100, 0),\n process: normalizeProcessVersion(r.process),\n });\n }\n }\n return out.length ? out : undefined;\n}\n\nexport function deserializeLayer(raw: unknown, ctx: DeserializeContext): ImageLayer | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, any>;\n const id = typeof r.id === 'string' && r.id ? r.id : newLayerId();\n const base: LayerBase = {\n id,\n name: typeof r.name === 'string' ? r.name : '',\n visible: r.visible !== false,\n opacity: num(r.opacity, 0, 1, 1),\n blend: normalizeBlendMode(r.blend),\n };\n if (r.clipped === true) base.clipped = true;\n const mask = readMask(r.mask, ctx, `Layer \"${base.name || id}\"`);\n if (mask) base.mask = mask;\n if (r.locks && typeof r.locks === 'object') {\n const locks: LayerLocks = {};\n for (const k of ['pixels', 'transparency', 'position', 'all'] as const) if (r.locks[k] === true) locks[k] = true;\n if (Object.keys(locks).length) base.locks = locks;\n }\n const effects = deserializeEffects(r.effects);\n if (effects && r.type !== 'adjustment') base.effects = effects;\n switch (r.type) {\n case 'pixel': {\n const layer: PixelLayer = {\n ...base,\n type: 'pixel',\n surface: r.source === 'document' ? emptySurface(4) : readSurface(r.asset, r.x, r.y, 4, 0, ctx, `Layer \"${base.name || id}\"`),\n offset: { x: Math.round(num(r.offset?.x, -1e7, 1e7, 0)), y: Math.round(num(r.offset?.y, -1e7, 1e7, 0)) },\n };\n if (r.source === 'document') layer.source = 'document';\n if (typeof r.fillOpacity === 'number' && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);\n const filters = readFilters(r.filters);\n if (filters) layer.filters = filters;\n return layer;\n }\n case 'adjustment': {\n const norm = normalizeAdjustmentLayer({ id, kind: r.kind, params: r.params });\n if (!norm) {\n ctx.problems.push(`Layer \"${base.name || id}\": unknown adjustment kind \"${r.kind}\"`);\n return null;\n }\n return { ...base, type: 'adjustment', kind: norm.kind, params: norm.params } as AdjustmentLayerNode;\n }\n case 'group': {\n const children = Array.isArray(r.children) ? r.children.map((c: unknown) => deserializeLayer(c, ctx)).filter(Boolean) : [];\n const blend = r.blend === 'pass-through' ? 'pass-through' : isBlendMode(r.blend) ? r.blend : 'pass-through';\n const group: GroupLayer = { ...base, type: 'group', blend, children: children as ImageLayer[] };\n if (r.collapsed === true) group.collapsed = true;\n return group;\n }\n case 'annotation': {\n const a = r.annotation;\n if (!a || typeof a !== 'object' || typeof a.type !== 'string') {\n ctx.problems.push(`Layer \"${base.name || id}\": annotation is missing`);\n return null;\n }\n return { ...base, type: 'annotation', annotation: JSON.parse(JSON.stringify({ ...a, id: typeof a.id === 'string' ? a.id : id })) };\n }\n case 'smart':\n return readSmartObject(r, base, ctx);\n default:\n ctx.problems.push(`Layer \"${base.name || id}\": unknown layer type \"${r.type}\"`);\n return null;\n }\n}\n\n/** Reads contents `id` from the table (memoised; a contents that contains itself is refused). */\nfunction readContents(id: string, ctx: DeserializeContext): SmartContents | null {\n const memo = (ctx.contentsMemo ??= new Map());\n if (memo.has(id)) {\n const c = memo.get(id);\n if (c === null) ctx.problems.push(`Smart object contents \"${id}\" contain themselves; the inner instance was dropped`);\n return c ?? null;\n }\n const raw = (ctx.smartContents ?? []).find((c) => c && typeof c === 'object' && (c as { id?: unknown }).id === id) as Record<string, any> | undefined;\n if (!raw) {\n ctx.problems.push(`Smart object contents \"${id}\" are missing`);\n return null;\n }\n const width = Math.round(num(raw.width, 0, 300000, 0));\n const height = Math.round(num(raw.height, 0, 300000, 0));\n if (!(width > 0 && height > 0)) {\n ctx.problems.push(`Smart object contents \"${id}\" have no size`);\n return null;\n }\n memo.set(id, null);\n const c: SmartContents = { id, name: typeof raw.name === 'string' ? raw.name : 'Contents', width, height, kind: raw.kind === 'document' ? 'document' : 'raster' };\n if (raw.file && typeof raw.file.type === 'string' && typeof raw.file.data === 'string') {\n try {\n c.file = { type: raw.file.type, bytes: base64ToBytes(raw.file.data) };\n } catch {\n ctx.problems.push(`Smart object contents \"${c.name}\": the embedded file could not be read`);\n }\n }\n if (raw.linked && typeof raw.linked.path === 'string') c.linked = { path: raw.linked.path };\n if (c.kind === 'raster') {\n if (typeof raw.asset === 'string') c.raster = readSurface(raw.asset, 0, 0, 4, 0, ctx, `Smart object contents \"${c.name}\"`);\n else if (!c.file) c.raster = emptySurface(4);\n } else {\n const outer = ctx.canvas;\n ctx.canvas = { width, height };\n const layers = deserializeLayers(raw.layers, ctx);\n ctx.canvas = outer;\n c.document = { layers };\n if (raw.globalLight && typeof raw.globalLight === 'object') c.document.globalLight = { angle: num(raw.globalLight.angle, -360, 360, 120), altitude: num(raw.globalLight.altitude, 0, 90, 30) };\n const patterns = deserializePatterns(raw.patterns, ctx.problems);\n if (patterns.length) c.document.patterns = patterns;\n }\n memo.set(id, c);\n return c;\n}\n\nfunction readSmartFilters(raw: unknown, ctx: DeserializeContext, what: string): SmartFilterStack | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const r = raw as Record<string, any>;\n const filters: SmartFilter[] = [];\n const list = Array.isArray(r.filters) ? r.filters : [];\n const plain = readFilters(list.filter((f: any) => f && f.kind !== 'psd')) ?? [];\n let k = 0;\n for (const f of list) {\n if (!f || typeof f !== 'object') continue;\n const blend = isBlendMode(f.blend) && f.blend !== 'pigment' ? f.blend : 'normal';\n const opacity = num(f.opacity, 0, 1, 1);\n if (f.kind === 'psd') {\n try {\n filters.push({ id: typeof f.id === 'string' ? f.id : newLayerId('flt'), kind: 'psd', enabled: f.enabled !== false, name: typeof f.name === 'string' ? f.name : 'Filter', data: base64ToBytes(String(f.data ?? '')), blend, opacity });\n } catch {\n ctx.problems.push(`${what}: a Photoshop smart filter could not be read`);\n }\n } else if (f.kind === 'develop' || f.kind === 'blur') {\n const base = plain[k++];\n if (base) filters.push({ ...base, blend, opacity });\n }\n }\n const stack: SmartFilterStack = { enabled: r.enabled !== false, filters };\n const mask = readMask(r.mask, ctx, `${what} filter`);\n if (mask) stack.mask = mask;\n return stack;\n}\n\nfunction readSmartObject(r: Record<string, any>, base: LayerBase, ctx: DeserializeContext): SmartObjectLayer | null {\n const what = `Layer \"${base.name || base.id}\"`;\n const contents = typeof r.contents === 'string' ? readContents(r.contents, ctx) : null;\n if (!contents) {\n ctx.problems.push(`${what}: its smart object contents are missing; the layer was dropped`);\n return null;\n }\n const p = r.placement ?? {};\n let placement: SmartPlacement;\n try {\n const raw: SmartPlacement = { corners: p.corners as Quad };\n if (p.warp && isValidWarpMesh(p.warp)) raw.warp = p.warp;\n if (isInterpolation(p.interpolation)) raw.interpolation = p.interpolation;\n placement = normalizePlacement(raw, contents);\n } catch (e) {\n ctx.problems.push(`${what}: its placement is invalid (${e instanceof Error ? e.message : String(e)}); it was placed at the top left`);\n placement = { corners: [{ x: 0, y: 0 }, { x: contents.width, y: 0 }, { x: contents.width, y: contents.height }, { x: 0, y: contents.height }] };\n }\n const layer: SmartObjectLayer = { ...base, type: 'smart', contents, placement, surface: emptySurface(4), offset: { x: 0, y: 0 } };\n const filters = readSmartFilters(r.smartFilters, ctx, what);\n if (filters) layer.smartFilters = filters;\n if (typeof r.fillOpacity === 'number' && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);\n if (r.psd && typeof r.psd.key === 'string' && typeof r.psd.data === 'string') {\n try {\n layer.psd = { key: r.psd.key, data: base64ToBytes(r.psd.data) };\n if (typeof r.psd.placed === 'string') layer.psd.placed = r.psd.placed;\n } catch {\n // A damaged PSD block is regenerated on export.\n }\n }\n if (typeof r.asset === 'string') layer.surface = readSurface(r.asset, r.x, r.y, 4, 0, ctx, what);\n else if (contents.raster || contents.document) layer.surface = renderSmartObject(layer, ctx.canvas ?? { width: 1, height: 1 });\n return layer;\n}\n\n/** Reads a stored layer tree (layers that cannot be read are dropped and reported). */\nexport function deserializeLayers(raw: unknown, ctx: DeserializeContext): ImageLayer[] {\n if (!Array.isArray(raw)) return [];\n const seen = new Set<string>();\n const dedupe = (list: ImageLayer[]): ImageLayer[] =>\n list.map((l) => {\n let next = l;\n if (seen.has(l.id)) next = { ...l, id: newLayerId() } as ImageLayer;\n seen.add(next.id);\n if (next.type === 'group') next = { ...next, children: dedupe(next.children) };\n return next;\n });\n return dedupe(raw.map((l) => deserializeLayer(l, ctx)).filter(Boolean) as ImageLayer[]);\n}\n","// Portions ported from photocraft/crates/psd/src/io.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** A malformed or unsupported PSD; the message says what was wrong and where. */\nexport class PsdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PsdError';\n }\n}\n\n/** Big-endian reader over a byte range that never reads past its end (it throws `PsdError`). */\nexport class PsdReader {\n pos: number;\n\n constructor(\n readonly data: Uint8Array,\n private readonly start = 0,\n readonly end = data.length\n ) {\n this.pos = start;\n }\n\n get remaining(): number {\n return this.end - this.pos;\n }\n\n get offset(): number {\n return this.pos - this.start;\n }\n\n private need(n: number, what: string): void {\n if (n < 0 || this.pos + n > this.end) throw new PsdError(`Unexpected end of data reading ${what} at byte ${this.pos}.`);\n }\n\n u8(): number {\n this.need(1, 'a byte');\n return this.data[this.pos++];\n }\n\n i8(): number {\n const v = this.u8();\n return v > 127 ? v - 256 : v;\n }\n\n u16(): number {\n this.need(2, 'a 16-bit value');\n const v = (this.data[this.pos] << 8) | this.data[this.pos + 1];\n this.pos += 2;\n return v;\n }\n\n i16(): number {\n const v = this.u16();\n return v > 32767 ? v - 65536 : v;\n }\n\n u32(): number {\n this.need(4, 'a 32-bit value');\n const d = this.data;\n const p = this.pos;\n this.pos += 4;\n return ((d[p] << 24) | (d[p + 1] << 16) | (d[p + 2] << 8) | d[p + 3]) >>> 0;\n }\n\n i32(): number {\n return this.u32() | 0;\n }\n\n u64(): number {\n const hi = this.u32();\n const lo = this.u32();\n const v = hi * 2 ** 32 + lo;\n if (!Number.isSafeInteger(v)) throw new PsdError('A 64-bit length is too large.');\n return v;\n }\n\n i64(): number {\n const hi = this.i32();\n const lo = this.u32();\n return hi * 2 ** 32 + lo;\n }\n\n f32(): number {\n this.need(4, 'a float');\n const v = new DataView(this.data.buffer, this.data.byteOffset + this.pos, 4).getFloat32(0, false);\n this.pos += 4;\n return v;\n }\n\n f64(): number {\n this.need(8, 'a double');\n const v = new DataView(this.data.buffer, this.data.byteOffset + this.pos, 8).getFloat64(0, false);\n this.pos += 8;\n return v;\n }\n\n /** A length field: 8 bytes in PSB where the format says so, else 4. */\n len(long: boolean): number {\n return long ? this.u64() : this.u32();\n }\n\n bytes(n: number): Uint8Array {\n this.need(n, `${n} bytes`);\n const out = this.data.subarray(this.pos, this.pos + n);\n this.pos += n;\n return out;\n }\n\n ascii(n: number): string {\n return String.fromCharCode(...this.bytes(n));\n }\n\n skip(n: number): void {\n this.need(n, `${n} bytes`);\n this.pos += n;\n }\n\n /** A reader over the next `n` bytes (and skips them here). */\n sub(n: number): PsdReader {\n this.need(n, `a ${n}-byte section`);\n const r = new PsdReader(this.data, this.pos, this.pos + n);\n this.pos += n;\n return r;\n }\n\n peek(n: number): Uint8Array {\n return this.data.subarray(this.pos, Math.min(this.end, this.pos + n));\n }\n\n rest(): Uint8Array {\n const out = this.data.subarray(this.pos, this.end);\n this.pos = this.end;\n return out;\n }\n\n /** Refuses counts that could not fit in what is left (each item at least `size` bytes). */\n checkCount(n: number, size: number, what: string): void {\n if (n * size > this.remaining) throw new PsdError(`${what}: count ${n} does not fit in the remaining ${this.remaining} bytes.`);\n }\n\n /** Pascal string padded to a multiple of `align` (length byte included). */\n pascal(align: number): Uint8Array {\n const n = this.u8();\n const s = this.bytes(n);\n const pad = (align - ((1 + n) % align)) % align;\n this.skip(Math.min(pad, this.remaining));\n return s;\n }\n\n /** Photoshop Unicode string: u32 code-unit count, UTF-16BE units (trailing NULs dropped). */\n unicode(): string {\n const n = this.u32();\n this.checkCount(n, 2, 'Unicode string');\n let s = '';\n for (let i = 0; i < n; i++) s += String.fromCharCode(this.u16());\n return s.replace(/\\u0000+$/, '');\n }\n}\n\n/** A growable big-endian writer. */\nexport class PsdWriter {\n private buf = new Uint8Array(1 << 16);\n length = 0;\n\n private ensure(n: number): void {\n if (this.length + n <= this.buf.length) return;\n let size = this.buf.length * 2;\n while (size < this.length + n) size *= 2;\n const next = new Uint8Array(size);\n next.set(this.buf.subarray(0, this.length));\n this.buf = next;\n }\n\n u8(v: number): void {\n this.ensure(1);\n this.buf[this.length++] = v & 255;\n }\n\n u16(v: number): void {\n this.ensure(2);\n this.buf[this.length++] = (v >>> 8) & 255;\n this.buf[this.length++] = v & 255;\n }\n\n i16(v: number): void {\n this.u16(v < 0 ? v + 65536 : v);\n }\n\n u32(v: number): void {\n this.ensure(4);\n this.buf[this.length++] = (v >>> 24) & 255;\n this.buf[this.length++] = (v >>> 16) & 255;\n this.buf[this.length++] = (v >>> 8) & 255;\n this.buf[this.length++] = v & 255;\n }\n\n i32(v: number): void {\n this.u32(v >>> 0);\n }\n\n u64(v: number): void {\n this.u32(Math.floor(v / 2 ** 32));\n this.u32(v >>> 0);\n }\n\n f64(v: number): void {\n this.ensure(8);\n new DataView(this.buf.buffer, this.length, 8).setFloat64(0, v, false);\n this.length += 8;\n }\n\n bytes(b: ArrayLike<number>): void {\n this.ensure(b.length);\n this.buf.set(b as Uint8Array, this.length);\n this.length += b.length;\n }\n\n ascii(s: string): void {\n for (let i = 0; i < s.length; i++) this.u8(s.charCodeAt(i));\n }\n\n zeros(n: number): void {\n this.ensure(n);\n this.buf.fill(0, this.length, this.length + n);\n this.length += n;\n }\n\n /** Reserves a length field; `endLen` fills it with the bytes written since. */\n beginLen(long: boolean): { at: number; long: boolean } {\n const at = this.length;\n if (long) this.u64(0);\n else this.u32(0);\n return { at, long };\n }\n\n endLen(mark: { at: number; long: boolean }): number {\n const n = this.length - mark.at - (mark.long ? 8 : 4);\n const save = this.length;\n this.length = mark.at;\n if (mark.long) this.u64(n);\n else this.u32(n);\n this.length = save;\n return n;\n }\n\n /** Pascal string padded to a multiple of `align`. */\n pascal(s: Uint8Array, align: number): void {\n const n = Math.min(255, s.length);\n this.u8(n);\n this.bytes(s.subarray(0, n));\n this.zeros((align - ((1 + n) % align)) % align);\n }\n\n unicode(s: string, nul = false): void {\n const n = s.length + (nul ? 1 : 0);\n this.u32(n);\n for (let i = 0; i < s.length; i++) this.u16(s.charCodeAt(i));\n if (nul) this.u16(0);\n }\n\n result(): Uint8Array {\n return this.buf.slice(0, this.length);\n }\n}\n\n/** A legacy (Pascal) name: UTF-8 when valid, else Latin-1. */\nexport function decodeLegacyName(b: Uint8Array): string {\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(b);\n } catch {\n return String.fromCharCode(...b);\n }\n}\n\n/** Encodes a name for a legacy Pascal field: non-ASCII characters become `?`. */\nexport function encodeLegacyName(s: string): Uint8Array {\n const out: number[] = [];\n for (const ch of s) {\n const c = ch.codePointAt(0)!;\n out.push(c >= 32 && c < 127 ? c : 63);\n if (out.length >= 255) break;\n }\n return Uint8Array.from(out);\n}\n","// Portions ported from photocraft/crates/psd/src/descriptor.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Photoshop ActionDescriptors (the structured values inside `vibA`, `blwh` and other blocks):\n * read and write. Keys and class ids are 4-character codes, or longer strings.\n */\n\nimport { PsdError, PsdReader, PsdWriter } from './binary';\n\nconst MAX_DEPTH = 64;\n/** Ids that are 4 characters long but stored as strings, never codes. */\nconst STRING_IDS = new Set(['warp', 'list', 'time']);\n\nexport interface DescClass {\n name: string;\n classId: string;\n}\n\nexport type ReferenceItem =\n | { type: 'prop'; class: DescClass; key: string }\n | { type: 'Clss'; class: DescClass }\n | { type: 'Enmr'; class: DescClass; typeId: string; value: string }\n | { type: 'rele'; class: DescClass; offset: number }\n | { type: 'Idnt'; class: DescClass; id: number }\n | { type: 'indx'; class: DescClass; index: number }\n | { type: 'name'; class: DescClass; name: string };\n\nexport type DescValue =\n | { type: 'obj '; items: ReferenceItem[] }\n | { type: 'Objc' | 'GlbO'; value: Descriptor }\n | { type: 'VlLs'; items: DescValue[] }\n | { type: 'doub'; value: number }\n | { type: 'UntF'; unit: string; value: number }\n | { type: 'UnFl'; unit: string; values: number[] }\n | { type: 'TEXT'; value: string }\n | { type: 'enum'; typeId: string; value: string }\n | { type: 'long'; value: number }\n | { type: 'comp'; value: number }\n | { type: 'bool'; value: boolean }\n | { type: 'type' | 'GlbC'; class: DescClass }\n | { type: 'alis' | 'Pth ' | 'tdta'; data: Uint8Array }\n | { type: 'ObAr'; prefix: number; body: Descriptor };\n\nexport interface Descriptor {\n name: string;\n classId: string;\n items: Array<[string, DescValue]>;\n}\n\nexport function descriptor(classId: string, items: Array<[string, DescValue]> = []): Descriptor {\n return { name: '', classId, items };\n}\n\nexport function descGet(d: Descriptor, key: string): DescValue | undefined {\n return d.items.find(([k]) => k === key)?.[1];\n}\n\n/** A number value (long, double or unit float), or `undefined`. */\nexport function descNumber(d: Descriptor, key: string): number | undefined {\n const v = descGet(d, key);\n if (!v) return undefined;\n if (v.type === 'long' || v.type === 'doub' || v.type === 'UntF' || v.type === 'comp') return v.value;\n return undefined;\n}\n\nexport function descBool(d: Descriptor, key: string): boolean | undefined {\n const v = descGet(d, key);\n return v?.type === 'bool' ? v.value : undefined;\n}\n\nexport function descObject(d: Descriptor, key: string): Descriptor | undefined {\n const v = descGet(d, key);\n return v && (v.type === 'Objc' || v.type === 'GlbO') ? v.value : undefined;\n}\n\nfunction readId(r: PsdReader): string {\n const n = r.u32();\n return n === 0 ? r.ascii(4) : r.ascii(n);\n}\n\nfunction writeId(w: PsdWriter, id: string): void {\n if (id.length === 4 && !STRING_IDS.has(id)) {\n w.u32(0);\n w.ascii(id);\n } else {\n w.u32(id.length);\n w.ascii(id);\n }\n}\n\nfunction readClass(r: PsdReader): DescClass {\n return { name: r.unicode(), classId: readId(r) };\n}\n\nfunction writeClass(w: PsdWriter, c: DescClass): void {\n w.unicode(c.name);\n writeId(w, c.classId);\n}\n\nfunction readReferenceItem(r: PsdReader): ReferenceItem {\n const type = r.ascii(4);\n switch (type) {\n case 'prop':\n return { type, class: readClass(r), key: readId(r) };\n case 'Clss':\n return { type, class: readClass(r) };\n case 'Enmr':\n return { type, class: readClass(r), typeId: readId(r), value: readId(r) };\n case 'rele':\n return { type, class: readClass(r), offset: r.i32() };\n case 'Idnt':\n return { type, class: readClass(r), id: r.u32() };\n case 'indx':\n return { type, class: readClass(r), index: r.u32() };\n case 'name':\n return { type, class: readClass(r), name: r.unicode() };\n default:\n throw new PsdError(`Unsupported descriptor reference item \"${type}\".`);\n }\n}\n\nfunction writeReferenceItem(w: PsdWriter, it: ReferenceItem): void {\n w.ascii(it.type);\n writeClass(w, it.class);\n switch (it.type) {\n case 'prop':\n writeId(w, it.key);\n break;\n case 'Enmr':\n writeId(w, it.typeId);\n writeId(w, it.value);\n break;\n case 'rele':\n w.i32(it.offset);\n break;\n case 'Idnt':\n w.u32(it.id);\n break;\n case 'indx':\n w.u32(it.index);\n break;\n case 'name':\n w.unicode(it.name);\n break;\n }\n}\n\nfunction readValue(r: PsdReader, depth: number): DescValue {\n if (depth > MAX_DEPTH) throw new PsdError('Descriptor nesting is too deep.');\n const type = r.ascii(4);\n switch (type) {\n case 'obj ': {\n const n = r.u32();\n r.checkCount(n, 8, 'Descriptor reference');\n const items: ReferenceItem[] = [];\n for (let i = 0; i < n; i++) items.push(readReferenceItem(r));\n return { type, items };\n }\n case 'Objc':\n case 'GlbO':\n return { type, value: readDescriptorBody(r, depth + 1) };\n case 'VlLs': {\n const n = r.u32();\n r.checkCount(n, 4, 'Descriptor list');\n const items: DescValue[] = [];\n for (let i = 0; i < n; i++) items.push(readValue(r, depth + 1));\n return { type, items };\n }\n case 'doub':\n return { type, value: r.f64() };\n case 'UntF':\n return { type, unit: r.ascii(4), value: r.f64() };\n case 'UnFl': {\n const unit = r.ascii(4);\n const n = r.u32();\n r.checkCount(n, 8, 'Unit float list');\n const values: number[] = [];\n for (let i = 0; i < n; i++) values.push(r.f64());\n return { type, unit, values };\n }\n case 'TEXT':\n return { type, value: r.unicode() };\n case 'enum':\n return { type, typeId: readId(r), value: readId(r) };\n case 'long':\n return { type, value: r.i32() };\n case 'comp':\n return { type, value: r.i64() };\n case 'bool':\n return { type, value: r.u8() !== 0 };\n case 'type':\n case 'GlbC':\n return { type, class: readClass(r) };\n case 'alis':\n case 'Pth ':\n case 'tdta':\n return { type, data: r.bytes(r.u32()) };\n case 'ObAr':\n return { type, prefix: r.u32(), body: readDescriptorBody(r, depth + 1) };\n default:\n throw new PsdError(`Unsupported descriptor value type \"${type}\".`);\n }\n}\n\nfunction writeValue(w: PsdWriter, v: DescValue): void {\n w.ascii(v.type);\n switch (v.type) {\n case 'obj ':\n w.u32(v.items.length);\n for (const it of v.items) writeReferenceItem(w, it);\n break;\n case 'Objc':\n case 'GlbO':\n writeDescriptorBody(w, v.value);\n break;\n case 'VlLs':\n w.u32(v.items.length);\n for (const it of v.items) writeValue(w, it);\n break;\n case 'doub':\n w.f64(v.value);\n break;\n case 'UntF':\n w.ascii(v.unit);\n w.f64(v.value);\n break;\n case 'UnFl':\n w.ascii(v.unit);\n w.u32(v.values.length);\n for (const x of v.values) w.f64(x);\n break;\n case 'TEXT':\n w.unicode(v.value, true);\n break;\n case 'enum':\n writeId(w, v.typeId);\n writeId(w, v.value);\n break;\n case 'long':\n w.i32(v.value);\n break;\n case 'comp':\n w.u32(Math.floor(v.value / 2 ** 32) >>> 0);\n w.u32(v.value >>> 0);\n break;\n case 'bool':\n w.u8(v.value ? 1 : 0);\n break;\n case 'type':\n case 'GlbC':\n writeClass(w, v.class);\n break;\n case 'alis':\n case 'Pth ':\n case 'tdta':\n w.u32(v.data.length);\n w.bytes(v.data);\n break;\n case 'ObAr':\n w.u32(v.prefix);\n writeDescriptorBody(w, v.body);\n break;\n }\n}\n\nfunction readDescriptorBody(r: PsdReader, depth: number): Descriptor {\n if (depth > MAX_DEPTH) throw new PsdError('Descriptor nesting is too deep.');\n const name = r.unicode();\n const classId = readId(r);\n const n = r.u32();\n r.checkCount(n, 10, 'Descriptor items');\n const items: Array<[string, DescValue]> = [];\n for (let i = 0; i < n; i++) {\n const key = readId(r);\n items.push([key, readValue(r, depth + 1)]);\n }\n return { name, classId, items };\n}\n\nfunction writeDescriptorBody(w: PsdWriter, d: Descriptor): void {\n w.unicode(d.name, true);\n writeId(w, d.classId);\n w.u32(d.items.length);\n for (const [k, v] of d.items) {\n writeId(w, k);\n writeValue(w, v);\n }\n}\n\n/** A versioned descriptor (u32 version 16, then the descriptor) at the reader's position. */\nexport function readVersionedDescriptor(r: PsdReader): Descriptor {\n const version = r.u32();\n if (version !== 16) throw new PsdError(`Descriptor version ${version}, expected 16.`);\n return readDescriptorBody(r, 0);\n}\n\nexport function writeVersionedDescriptor(w: PsdWriter, d: Descriptor): void {\n w.u32(16);\n writeDescriptorBody(w, d);\n}\n\n/** A versioned descriptor from the start of `bytes` (trailing bytes are ignored). */\nexport function parseVersionedDescriptor(bytes: Uint8Array): Descriptor {\n return readVersionedDescriptor(new PsdReader(bytes));\n}\n\nexport function versionedDescriptorBytes(d: Descriptor): Uint8Array {\n const w = new PsdWriter();\n writeVersionedDescriptor(w, d);\n return w.result();\n}\n","// Portions ported from photocraft/crates/io/src/adjust_map.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * PSD adjustment-layer blocks ↔ our ten adjustment kinds. Parameters use our units (levels\n * 0..255, degrees, percent), which are also Photoshop's, so most fields copy straight across.\n */\n\nimport { D50, XYZ_D50_TO_SRGB, defaultAdjustmentParams, hexToRgb, neutralHueRange } from '../adjustment-layers';\nimport { linearToSrgb } from '../color';\nimport type {\n AdjustmentKind,\n AdjustmentParamsMap,\n BlackWhiteParams,\n CurvePoint,\n CurvesParams,\n HueRange,\n LevelsChannel,\n} from '../types';\nimport { PsdReader, PsdWriter } from './binary';\nimport {\n type Descriptor,\n type DescValue,\n descBool,\n descNumber,\n descObject,\n descriptor,\n parseVersionedDescriptor,\n versionedDescriptorBytes,\n} from './descriptor';\n\n/** Every adjustment block key Photoshop writes. */\nexport const ADJUSTMENT_KEYS = ['levl', 'curv', 'hue2', 'brit', 'nvrt', 'thrs', 'post', 'expA', 'vibA', 'blnc', 'mixr', 'grdm', 'phfl', 'selc', 'blwh', 'clrL'];\n\n/** Keys we read and write. */\nexport const SUPPORTED_ADJUSTMENT_KEYS: Record<string, AdjustmentKind> = {\n levl: 'levels',\n curv: 'curves',\n hue2: 'hueSaturation',\n vibA: 'vibrance',\n blnc: 'colorBalance',\n blwh: 'blackWhite',\n nvrt: 'invert',\n post: 'posterize',\n thrs: 'threshold',\n phfl: 'photoFilter',\n};\n\nconst LABELS: Record<string, string> = {\n levl: 'Levels',\n curv: 'Curves',\n hue2: 'Hue/Saturation',\n brit: 'Brightness/Contrast',\n nvrt: 'Invert',\n thrs: 'Threshold',\n post: 'Posterize',\n expA: 'Exposure',\n vibA: 'Vibrance',\n blnc: 'Color Balance',\n mixr: 'Channel Mixer',\n grdm: 'Gradient Map',\n phfl: 'Photo Filter',\n selc: 'Selective Color',\n blwh: 'Black & White',\n clrL: 'Color Lookup',\n};\n\nexport function adjustmentLabel(key: string): string {\n return LABELS[key] ?? 'adjustment';\n}\n\nexport type ParsedAdjustment = {\n [K in AdjustmentKind]: { kind: K; params: AdjustmentParamsMap[K]; approximated?: string };\n}[AdjustmentKind];\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst toHex = (rgb: readonly number[]) =>\n '#' + rgb.map((v) => Math.round(clamp(v, 0, 1) * 255).toString(16).padStart(2, '0')).join('');\n\n/** The canonical form of four range sliders: increasing, within one turn, centre in -30..330. */\nexport function canonicalBounds(b: readonly number[]): [number, number, number, number] {\n if (b.some((v) => !Number.isFinite(v))) return [-45, -15, 15, 45];\n const rem = (v: number) => ((v % 360) + 360) % 360;\n const out = [b[0], b[0], b[0], b[0]];\n for (let k = 1; k < 4; k++) out[k] = Math.min(out[k - 1] + rem(b[k] - out[k - 1]), out[0] + 359);\n const centre = (out[1] + out[2]) / 2;\n const shift = rem(centre + 30) - 30 - centre;\n return out.map((v) => v + shift) as [number, number, number, number];\n}\n\nfunction levelsRecord(r: PsdReader): LevelsChannel {\n const inBlack = r.u16();\n const inWhite = r.u16();\n const outBlack = r.u16();\n const outWhite = r.u16();\n const gamma = r.u16() / 100;\n return {\n inBlack: clamp(inBlack, 0, 255),\n inWhite: clamp(inWhite, 0, 255),\n gamma: clamp(gamma || 1, 0.1, 9.99),\n outBlack: clamp(outBlack, 0, 255),\n outWhite: clamp(outWhite, 0, 255),\n };\n}\n\n/** A pencil (256-entry map) curve as points every 15 levels. */\nfunction curveFromMap(map: Uint8Array): CurvePoint[] {\n const pts: CurvePoint[] = [];\n for (let i = 0; i <= 255; i += 15) pts.push({ input: i, output: map[i] });\n return pts;\n}\n\nfunction parseCurves(d: Uint8Array): ParsedAdjustment | null {\n const r = new PsdReader(d);\n const pencil = r.u8() !== 0;\n const version = r.u16();\n if (version !== 1 && version !== 4) return null;\n const bits = r.u32();\n const params = defaultAdjustmentParams('curves');\n const order: Array<keyof CurvesParams> = ['master', 'red', 'green', 'blue'];\n for (let bit = 0; bit < 32; bit++) {\n if (!(bits & (1 << bit))) continue;\n let pts: CurvePoint[];\n if (pencil) {\n pts = curveFromMap(r.bytes(256));\n } else {\n const n = r.u16();\n r.checkCount(n, 4, 'Curve points');\n pts = [];\n for (let i = 0; i < n; i++) {\n const output = r.u16();\n const input = r.u16();\n pts.push({ input: clamp(input, 0, 255), output: clamp(output, 0, 255) });\n }\n pts.sort((a, b) => a.input - b.input);\n }\n if (bit < 4 && pts.length >= 2) params[order[bit]] = pts;\n }\n return { kind: 'curves', params, approximated: pencil ? 'its pencil curve is approximated by points' : undefined };\n}\n\nfunction parseHueSaturation(d: Uint8Array): ParsedAdjustment {\n const r = new PsdReader(d);\n r.u16();\n const colorize = r.u8() !== 0;\n r.u8();\n const triple = () => [r.i16(), r.i16(), r.i16()];\n const col = triple();\n const master = triple();\n const ranges: HueRange[] = [];\n for (let i = 0; i < 6; i++) {\n if (r.remaining < 14) {\n ranges.push(neutralHueRange(i));\n continue;\n }\n const b = [r.u16(), r.u16(), r.u16(), r.u16()];\n const [hue, saturation, lightness] = triple();\n ranges.push({ hue, saturation, lightness, bounds: canonicalBounds(b) });\n }\n const [hue, saturation, lightness] = colorize ? col : master;\n return { kind: 'hueSaturation', params: { hue, saturation, lightness, colorize, ranges } };\n}\n\nconst BW_KEYS: Array<[keyof Omit<BlackWhiteParams, 'tint'>, string]> = [\n ['reds', 'Rd '],\n ['yellows', 'Yllw'],\n ['greens', 'Grn '],\n ['cyans', 'Cyn '],\n ['blues', 'Bl '],\n ['magentas', 'Mgnt'],\n];\nconst BW_TINT = [225 / 255, 211 / 255, 179 / 255];\n\nfunction descRgb(d: Descriptor | undefined): number[] | null {\n if (!d) return null;\n if (descNumber(d, 'redFloat') !== undefined) {\n return [descNumber(d, 'redFloat')!, descNumber(d, 'greenFloat') ?? 0, descNumber(d, 'blueFloat') ?? 0].map((v) => clamp(v, 0, 1));\n }\n const r = descNumber(d, 'Rd ');\n const g = descNumber(d, 'Grn ');\n const b = descNumber(d, 'Bl ');\n if (r === undefined || g === undefined || b === undefined) return null;\n return [r, g, b].map((v) => clamp(v / 255, 0, 1));\n}\n\nfunction parseBlackWhite(d: Uint8Array): ParsedAdjustment {\n const desc = parseVersionedDescriptor(d);\n const params = defaultAdjustmentParams('blackWhite');\n for (const [field, key] of BW_KEYS) {\n const v = descNumber(desc, key);\n if (v !== undefined) params[field] = clamp(v, -200, 300);\n }\n params.tint = descBool(desc, 'useTint') ? toHex(descRgb(descObject(desc, 'tintColor')) ?? BW_TINT) : null;\n return { kind: 'blackWhite', params };\n}\n\nfunction labToRgb(l: number, a: number, b: number): number[] {\n const fy = (l + 16) / 116;\n const fx = fy + a / 500;\n const fz = fy - b / 200;\n const f = (t: number) => (t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29));\n const xyz = [f(fx) * D50[0], f(fy) * D50[1], f(fz) * D50[2]];\n return XYZ_D50_TO_SRGB.map((row) => linearToSrgb(clamp(row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2], 0, 1)));\n}\n\n/** A PSD colour structure (space id + four u16) as 0..1 RGB, or `null` for spaces we cannot read. */\nexport function decodePsdColor(space: number, c: readonly number[]): number[] | null {\n const n = (v: number) => v / 65535;\n switch (space) {\n case 0:\n return [n(c[0]), n(c[1]), n(c[2])];\n case 1: {\n const h = n(c[0]) * 360;\n const s = n(c[1]);\n const v = n(c[2]);\n const k = (m: number) => (m + h / 60) % 6;\n const f = (m: number) => v - v * s * Math.max(0, Math.min(k(m), 4 - k(m), 1));\n return [f(5), f(3), f(1)];\n }\n case 2: {\n // Stored inverted: 65535 is no ink. A naive conversion; CMYK profiles are not read.\n const [cy, m, y, k] = c.map(n);\n return [cy * k, m * k, y * k];\n }\n case 7: {\n const signed = (v: number) => (v << 16) >> 16;\n return labToRgb(Math.min(c[0], 10000) / 100, signed(c[1]) / 100, signed(c[2]) / 100);\n }\n case 8: {\n const v = 1 - Math.min(c[0], 10000) / 10000;\n return [v, v, v];\n }\n default:\n return null;\n }\n}\n\nfunction parsePhotoFilter(d: Uint8Array): ParsedAdjustment | null {\n const r = new PsdReader(d);\n const version = r.u16();\n let rgb: number[] | null;\n if (version === 2) {\n const space = r.u16();\n const c = [r.u16(), r.u16(), r.u16(), r.u16()];\n rgb = [0, 2, 7, 8].includes(space) ? decodePsdColor(space, c) : null;\n } else if (version === 3) {\n const rel = [r.u32(), r.u32(), r.u32()].map((v) => v / 32768);\n if (rel.some((v) => v > 2)) return null;\n const xyz = [rel[0] * D50[0], rel[1], rel[2] * D50[2]];\n rgb = XYZ_D50_TO_SRGB.map((row) => linearToSrgb(clamp(row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2], 0, 1)));\n } else {\n return null;\n }\n if (!rgb) return null;\n const density = clamp(r.u32(), 0, 100);\n const preserveLuminosity = r.u8() !== 0;\n return { kind: 'photoFilter', params: { color: toHex(rgb), density, preserveLuminosity } };\n}\n\n/**\n * Our parameters for an adjustment block, or `null` when the key is not one of our kinds or the\n * block cannot be read.\n */\nexport function parseAdjustmentBlock(key: string, data: Uint8Array): ParsedAdjustment | null {\n try {\n const r = new PsdReader(data);\n switch (key) {\n case 'nvrt':\n return { kind: 'invert', params: {} as Record<string, never> };\n case 'thrs':\n return { kind: 'threshold', params: { level: clamp(r.u16(), 1, 255) } };\n case 'post':\n return { kind: 'posterize', params: { levels: clamp(r.u16(), 2, 255) } };\n case 'levl': {\n r.u16();\n const master = levelsRecord(r);\n const red = levelsRecord(r);\n const green = levelsRecord(r);\n const blue = levelsRecord(r);\n return { kind: 'levels', params: { master, red, green, blue } };\n }\n case 'curv':\n return parseCurves(data);\n case 'hue2':\n return parseHueSaturation(data);\n case 'vibA': {\n const desc = parseVersionedDescriptor(data);\n const get = (k: string) => clamp(descNumber(desc, k) ?? 0, -100, 100);\n return { kind: 'vibrance', params: { vibrance: get('vibrance'), saturation: get('Strt') } };\n }\n case 'blnc': {\n const tone = () => [r.i16(), r.i16(), r.i16()].map((v) => clamp(v, -100, 100)) as [number, number, number];\n const shadows = tone();\n const midtones = tone();\n const highlights = tone();\n return { kind: 'colorBalance', params: { shadows, midtones, highlights, preserveLuminosity: r.u8() !== 0 } };\n }\n case 'blwh':\n return parseBlackWhite(data);\n case 'phfl':\n return parsePhotoFilter(data);\n default:\n return null;\n }\n } catch {\n return null;\n }\n}\n\nconst q = (v: number) => Math.round(clamp(v, 0, 255));\nconst i16 = (v: number) => Math.round(clamp(v, -32768, 32767));\n\nfunction writeLevels(w: PsdWriter, c: LevelsChannel): void {\n w.u16(q(c.inBlack));\n w.u16(q(c.inWhite));\n w.u16(q(c.outBlack));\n w.u16(q(c.outWhite));\n w.u16(Math.round(clamp(c.gamma * 100, 1, 999)));\n}\n\nconst long = (value: number): DescValue => ({ type: 'long', value: i16(value) });\n\n/** The PSD block (key and data) for one of our adjustment layers. */\nexport function writeAdjustmentBlock<K extends AdjustmentKind>(kind: K, params: AdjustmentParamsMap[K]): { key: string; data: Uint8Array } {\n const w = new PsdWriter();\n switch (kind) {\n case 'invert':\n return { key: 'nvrt', data: new Uint8Array(0) };\n case 'threshold': {\n const p = params as AdjustmentParamsMap['threshold'];\n w.u16(Math.max(1, q(p.level)));\n w.u16(0);\n return { key: 'thrs', data: w.result() };\n }\n case 'posterize': {\n const p = params as AdjustmentParamsMap['posterize'];\n w.u16(Math.round(clamp(p.levels, 2, 255)));\n w.u16(0);\n return { key: 'post', data: w.result() };\n }\n case 'levels': {\n const p = params as AdjustmentParamsMap['levels'];\n w.u16(2);\n for (const c of [p.master, p.red, p.green, p.blue]) writeLevels(w, c);\n const ident: LevelsChannel = { inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 };\n for (let i = 4; i < 29; i++) writeLevels(w, ident);\n return { key: 'levl', data: w.result() };\n }\n case 'curves': {\n const p = params as AdjustmentParamsMap['curves'];\n w.u8(0);\n w.u16(1);\n w.u32(0b1111);\n for (const c of [p.master, p.red, p.green, p.blue]) {\n const pts = c.slice(0, 19);\n w.u16(pts.length);\n for (const pt of pts) {\n w.u16(q(pt.output));\n w.u16(q(pt.input));\n }\n }\n return { key: 'curv', data: w.result() };\n }\n case 'hueSaturation': {\n const p = params as AdjustmentParamsMap['hueSaturation'];\n w.u16(2);\n w.u8(p.colorize ? 1 : 0);\n w.u8(0);\n for (let k = 0; k < 2; k++) for (const x of [p.hue, p.saturation, p.lightness]) w.i16(i16(x));\n for (let i = 0; i < 6; i++) {\n const r = p.ranges[i] ?? neutralHueRange(i);\n for (const b of r.bounds) w.u16(Math.round(((b % 360) + 360) % 360) % 360);\n for (const x of [r.hue, r.saturation, r.lightness]) w.i16(i16(x));\n }\n return { key: 'hue2', data: w.result() };\n }\n case 'vibrance': {\n const p = params as AdjustmentParamsMap['vibrance'];\n const d = descriptor('null', [\n ['vibrance', long(clamp(p.vibrance, -100, 100))],\n ['Strt', long(clamp(p.saturation, -100, 100))],\n ]);\n return { key: 'vibA', data: versionedDescriptorBytes(d) };\n }\n case 'colorBalance': {\n const p = params as AdjustmentParamsMap['colorBalance'];\n for (const x of [...p.shadows, ...p.midtones, ...p.highlights]) w.i16(i16(clamp(x, -100, 100)));\n w.u8(p.preserveLuminosity ? 1 : 0);\n w.u8(0);\n return { key: 'blnc', data: w.result() };\n }\n case 'blackWhite': {\n const p = params as AdjustmentParamsMap['blackWhite'];\n const tint = p.tint ? hexToRgb(p.tint) : BW_TINT;\n const dbl = (v: number): DescValue => ({ type: 'doub', value: clamp(v, 0, 1) * 255 });\n const items: Array<[string, DescValue]> = BW_KEYS.map(([field, key]) => [key, long(clamp(p[field], -200, 300))]);\n items.push(\n ['useTint', { type: 'bool', value: p.tint !== null }],\n ['tintColor', { type: 'Objc', value: descriptor('RGBC', [['Rd ', dbl(tint[0])], ['Grn ', dbl(tint[1])], ['Bl ', dbl(tint[2])]]) }],\n ['bwPresetKind', { type: 'long', value: 1 }],\n ['blackAndWhitePresetFileName', { type: 'TEXT', value: '' }]\n );\n return { key: 'blwh', data: versionedDescriptorBytes(descriptor('null', items)) };\n }\n case 'photoFilter': {\n const p = params as AdjustmentParamsMap['photoFilter'];\n const rgb = hexToRgb(p.color);\n w.u16(2);\n w.u16(0);\n for (const x of rgb) w.u16(Math.round(clamp(x, 0, 1) * 65535));\n w.u16(0);\n w.u32(Math.round(clamp(p.density, 0, 100)));\n w.u8(p.preserveLuminosity ? 1 : 0);\n w.zeros(3);\n return { key: 'phfl', data: w.result() };\n }\n }\n throw new Error(`Unknown adjustment kind \"${kind}\".`);\n}\n","// Portions ported from photocraft/crates/psd/src/compression.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Channel and image data compression: raw, RLE (PackBits), ZIP and ZIP with prediction. */\n\nimport { ZlibError, deflate, inflate } from '../../raster/zlib';\nimport { PsdError, PsdReader } from './binary';\n\nexport type PsdCompression = 0 | 1 | 2 | 3;\nexport const COMPRESSION_NAMES = ['raw', 'rle', 'zip', 'zip-prediction'] as const;\n\n/** Largest decoded channel set accepted from one file (bytes). */\nexport const MAX_DECODED_BYTES = 2 ** 31;\n\nexport function rowBytes(width: number, depth: number): number {\n return depth === 1 ? Math.ceil(width / 8) : width * Math.max(1, depth >> 3);\n}\n\nexport interface PlaneLayout {\n planes: number;\n width: number;\n height: number;\n depth: number;\n /** PSB: RLE row counts are 4 bytes. */\n psb: boolean;\n}\n\nfunction decodedLength(l: PlaneLayout): number {\n const total = l.planes * l.height * rowBytes(l.width, l.depth);\n if (total > MAX_DECODED_BYTES) throw new PsdError(`Decoded channel data of ${total} bytes is larger than the ${MAX_DECODED_BYTES}-byte limit.`);\n return total;\n}\n\n/** Worst-case PackBits size of `n` bytes. */\nexport function packbitsBound(n: number): number {\n return n + Math.ceil(n / 128);\n}\n\n/** PackBits: writes `src` encoded into `out` at `at` (room for `packbitsBound`); returns the new offset. */\nexport function packbitsEncode(src: Uint8Array, out: Uint8Array, at: number): number {\n const n = src.length;\n let i = 0;\n while (i < n) {\n let run = 1;\n while (i + run < n && run < 128 && src[i + run] === src[i]) run++;\n if (run >= 2) {\n out[at++] = (257 - run) & 255;\n out[at++] = src[i];\n i += run;\n continue;\n }\n const start = i;\n let len = 0;\n while (i < n && len < 128) {\n if (i + 2 < n && src[i] === src[i + 1] && src[i] === src[i + 2]) break;\n i++;\n len++;\n }\n out[at++] = len - 1;\n out.set(src.subarray(start, start + len), at);\n at += len;\n }\n return at;\n}\n\n/** PackBits: decodes exactly `expected` bytes from `src` into `out` at `at`; returns the new offset. */\nexport function packbitsDecodeInto(src: Uint8Array, expected: number, out: Uint8Array, at: number): number {\n const target = at + expected;\n let i = 0;\n while (at < target) {\n if (i >= src.length) throw new PsdError('PackBits row ended early.');\n const h = src[i++];\n if (h < 128) {\n const len = h + 1;\n if (i + len > src.length) throw new PsdError('PackBits literal is truncated.');\n if (at + len > target) throw new PsdError('PackBits literal overflows its row.');\n out.set(src.subarray(i, i + len), at);\n at += len;\n i += len;\n } else if (h !== 128) {\n const len = 257 - h;\n if (i >= src.length) throw new PsdError('PackBits run is truncated.');\n const b = src[i++];\n if (at + len > target) throw new PsdError('PackBits run overflows its row.');\n out.fill(b, at, at + len);\n at += len;\n }\n }\n return at;\n}\n\nfunction inflateChecked(data: Uint8Array, expected: number): Uint8Array {\n let out: Uint8Array;\n try {\n out = inflate(data, { maxOutput: expected + (1 << 20), sizeHint: expected, verify: false });\n } catch (e) {\n throw new PsdError(`ZIP channel data: ${e instanceof ZlibError || e instanceof Error ? e.message : String(e)}`);\n }\n if (out.length < expected) throw new PsdError(`ZIP stream produced ${out.length} of ${expected} bytes.`);\n return out.length === expected ? out : out.subarray(0, expected);\n}\n\nfunction unpredict(buf: Uint8Array, l: PlaneLayout): void {\n const rb = rowBytes(l.width, l.depth);\n if (rb === 0) return;\n const w = l.width;\n const scratch = l.depth === 32 ? new Uint8Array(rb) : null;\n for (let o = 0; o + rb <= buf.length; o += rb) {\n if (l.depth === 1 || l.depth === 8) {\n for (let i = 1; i < rb; i++) buf[o + i] = (buf[o + i] + buf[o + i - 1]) & 255;\n } else if (l.depth === 16) {\n for (let i = 1; i < w; i++) {\n const a = (buf[o + 2 * i] << 8) | buf[o + 2 * i + 1];\n const b = (buf[o + 2 * i - 2] << 8) | buf[o + 2 * i - 1];\n const v = (a + b) & 0xffff;\n buf[o + 2 * i] = v >> 8;\n buf[o + 2 * i + 1] = v & 255;\n }\n } else if (l.depth === 32) {\n for (let i = 1; i < rb; i++) buf[o + i] = (buf[o + i] + buf[o + i - 1]) & 255;\n scratch!.set(buf.subarray(o, o + rb));\n for (let i = 0; i < w; i++) for (let k = 0; k < 4; k++) buf[o + 4 * i + k] = scratch![k * w + i];\n } else {\n throw new PsdError(`ZIP prediction is not defined for depth ${l.depth}.`);\n }\n }\n}\n\nfunction decodeRle(r: PsdReader, l: PlaneLayout, total: number): Uint8Array {\n const rows = l.planes * l.height;\n const rb = rowBytes(l.width, l.depth);\n r.checkCount(rows, l.psb ? 4 : 2, 'RLE row counts');\n const counts = new Array<number>(rows);\n for (let i = 0; i < rows; i++) counts[i] = l.psb ? r.u32() : r.u16();\n if (total > r.remaining * 130) throw new PsdError('RLE data is too short for its declared size.');\n const out = new Uint8Array(total);\n let at = 0;\n for (const c of counts) at = packbitsDecodeInto(r.bytes(c), rb, out, at);\n return out;\n}\n\n/** Decodes planar channel data (`planes` planes of `height` rows each). */\nexport function decodePlanes(compression: number, data: Uint8Array, l: PlaneLayout): Uint8Array {\n const total = decodedLength(l);\n switch (compression) {\n case 0:\n if (data.length < total) throw new PsdError(`Raw channel data has ${data.length} of ${total} bytes.`);\n return data.slice(0, total);\n case 1:\n return decodeRle(new PsdReader(data), l, total);\n case 2:\n return inflateChecked(data, total).slice();\n case 3: {\n const v = inflateChecked(data, total).slice();\n unpredict(v, l);\n return v;\n }\n default:\n throw new PsdError(`Unsupported compression method ${compression}.`);\n }\n}\n\n/** RLE-encodes planar data: row counts (u16, or u32 in PSB), then the packed rows. */\nexport function encodeRle(decoded: Uint8Array, l: PlaneLayout): Uint8Array {\n const rows = l.planes * l.height;\n const rb = rowBytes(l.width, l.depth);\n const cs = l.psb ? 4 : 2;\n const packed = new Uint8Array(rows * packbitsBound(rb));\n let at = 0;\n const counts = new Uint8Array(rows * cs);\n for (let row = 0; row < rows; row++) {\n const start = at;\n at = packbitsEncode(decoded.subarray(row * rb, (row + 1) * rb), packed, at);\n const n = at - start;\n if (cs === 2) {\n if (n > 0xffff) throw new PsdError('An RLE row is longer than 65535 bytes; write a PSB instead.');\n counts[row * 2] = n >> 8;\n counts[row * 2 + 1] = n & 255;\n } else {\n counts[row * 4] = n >>> 24;\n counts[row * 4 + 1] = (n >>> 16) & 255;\n counts[row * 4 + 2] = (n >>> 8) & 255;\n counts[row * 4 + 3] = n & 255;\n }\n }\n const out = new Uint8Array(counts.length + at);\n out.set(counts);\n out.set(packed.subarray(0, at), counts.length);\n return out;\n}\n\n/** ZIP without prediction (used by tests to build fixtures). */\nexport function encodeZip(decoded: Uint8Array): Uint8Array {\n return deflate(decoded);\n}\n","// Portions ported from photocraft/crates/psd/src/{header,file,layer,tagged,tree,resources,image_data}.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The PSD / PSB container: header, colour mode data, image resources, layer and mask info\n * (records, channel data, tagged blocks) and the merged image. Channel data stays encoded\n * until a caller decodes it, so inspecting a file is cheap.\n */\n\nimport { PsdError, PsdReader, PsdWriter, decodeLegacyName } from './binary';\nimport { decodePlanes, rowBytes } from './compression';\n\nexport const PSD_MAX_DIMENSION = 30_000;\nexport const PSB_MAX_DIMENSION = 300_000;\nconst MAX_CHANNELS = 56;\nconst MAX_LAYER_CHANNELS = 64;\n\nexport const COLOR_MODES: Record<number, string> = {\n 0: 'bitmap',\n 1: 'grayscale',\n 2: 'indexed',\n 3: 'rgb',\n 4: 'cmyk',\n 7: 'multichannel',\n 8: 'duotone',\n 9: 'lab',\n};\n\n/** Tagged block keys whose length is 8 bytes in a PSB. */\nconst PSB_LONG_KEYS = new Set([\n 'LMsk', 'Lr16', 'Lr32', 'Layr', 'Mt16', 'Mt32', 'Mtrn', 'Alph', 'FMsk', 'lnk2', 'FEid', 'FXid', 'PxSD',\n 'lnk3', 'lnkE', 'pths', 'extd', 'extn', 'FELS', 'cinf', 'artd',\n]);\n\nexport const CHANNEL_ALPHA = -1;\nexport const CHANNEL_USER_MASK = -2;\nexport const CHANNEL_REAL_USER_MASK = -3;\n\nexport const LAYER_FLAG_TRANSPARENCY_PROTECTED = 1;\nexport const LAYER_FLAG_HIDDEN = 2;\n/** Mask flag bit 0, \"position relative to layer\": Photoshop sets it when the mask is unlinked (photocraft reads it so). */\nexport const MASK_FLAG_UNLINKED = 1;\nexport const MASK_FLAG_DISABLED = 2;\nexport const MASK_FLAG_PARAMETERS = 16;\n\n/** Section divider (`lsct`) kinds. */\nexport const SECTION_OTHER = 0;\nexport const SECTION_OPEN = 1;\nexport const SECTION_CLOSED = 2;\nexport const SECTION_DIVIDER = 3;\n\nexport interface PsdHeader {\n psb: boolean;\n channels: number;\n height: number;\n width: number;\n depth: number;\n colorMode: number;\n}\n\nexport interface PsdRect {\n top: number;\n left: number;\n bottom: number;\n right: number;\n}\n\nexport interface PsdChannel {\n id: number;\n /** `null` for a zero-length channel. */\n compression: number | null;\n /** Encoded bytes, without the compression field. */\n data: Uint8Array;\n}\n\nexport interface PsdLayerMask {\n rect: PsdRect;\n defaultColor: number;\n flags: number;\n userDensity?: number;\n userFeather?: number;\n vectorDensity?: number;\n vectorFeather?: number;\n /** The real (vector-combined) user mask, when present. */\n real?: { flags: number; background: number; rect: PsdRect };\n}\n\nexport interface PsdTaggedBlock {\n key: string;\n data: Uint8Array;\n}\n\nexport interface PsdLayerRecord {\n rect: PsdRect;\n channels: PsdChannel[];\n blendKey: string;\n opacity: number;\n clipping: number;\n flags: number;\n mask?: PsdLayerMask;\n /** The legacy (Pascal) name; `layerName` prefers `luni`. */\n legacyName: string;\n blocks: PsdTaggedBlock[];\n}\n\nexport interface PsdResource {\n id: number;\n name: string;\n data: Uint8Array;\n}\n\nexport interface PsdFile {\n header: PsdHeader;\n colorModeData: Uint8Array;\n resources: PsdResource[];\n /** Bottom-most first, as stored. */\n layers: PsdLayerRecord[];\n /** The layer count was negative: the merged image's first extra channel is its alpha. */\n mergedAlpha: boolean;\n globalBlocks: PsdTaggedBlock[];\n imageCompression: number;\n imageData: Uint8Array;\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\nfunction readRect(r: PsdReader): PsdRect {\n return { top: r.i32(), left: r.i32(), bottom: r.i32(), right: r.i32() };\n}\n\nexport function rectSize(rect: PsdRect): { width: number; height: number } {\n const width = rect.right - rect.left;\n const height = rect.bottom - rect.top;\n if (width < 0 || height < 0) throw new PsdError('A layer rectangle has a negative size.');\n if (width > PSB_MAX_DIMENSION || height > PSB_MAX_DIMENSION) throw new PsdError('A layer is larger than 300000 pixels.');\n return { width, height };\n}\n\nfunction readHeader(r: PsdReader): PsdHeader {\n if (r.ascii(4) !== '8BPS') throw new PsdError('Not a PSD file: the signature is not \"8BPS\".');\n const version = r.u16();\n if (version !== 1 && version !== 2) throw new PsdError(`Unsupported PSD version ${version} (1 is PSD, 2 is PSB).`);\n r.skip(6);\n const h: PsdHeader = { psb: version === 2, channels: r.u16(), height: r.u32(), width: r.u32(), depth: r.u16(), colorMode: r.u16() };\n if (h.channels < 1 || h.channels > MAX_CHANNELS) throw new PsdError(`Channel count ${h.channels} is outside 1 to ${MAX_CHANNELS}.`);\n if (h.width < 1 || h.height < 1) throw new PsdError(`Image size ${h.width} × ${h.height} must be at least 1 × 1.`);\n const max = h.psb ? PSB_MAX_DIMENSION : PSD_MAX_DIMENSION;\n if (h.width > max || h.height > max) throw new PsdError(`Image size ${h.width} × ${h.height} exceeds ${max} pixels.`);\n if (![1, 8, 16, 32].includes(h.depth)) throw new PsdError(`Unsupported bit depth ${h.depth}.`);\n return h;\n}\n\nfunction readResources(r: PsdReader): PsdResource[] {\n const s = r.sub(r.u32());\n const out: PsdResource[] = [];\n while (s.remaining >= 12) {\n s.ascii(4); // 8BIM, or another vendor signature\n const id = s.u16();\n const name = decodeLegacyName(s.pascal(2));\n const len = s.u32();\n const data = s.bytes(len);\n if (len % 2 && s.remaining) s.skip(1);\n out.push({ id, name, data });\n }\n return out;\n}\n\nconst isSig = (r: PsdReader, at = 0) => {\n const b = r.data.subarray(r.pos + at, r.pos + at + 4);\n if (b.length < 4) return false;\n const s = String.fromCharCode(b[0], b[1], b[2], b[3]);\n return s === '8BIM' || s === '8B64';\n};\n\n/** Tagged blocks until the end of `r` (or something that is not a block). */\nfunction readBlocks(r: PsdReader, psb: boolean, alignment: number): PsdTaggedBlock[] {\n const out: PsdTaggedBlock[] = [];\n while (r.remaining >= 12 && isSig(r)) {\n r.skip(4);\n const key = r.ascii(4);\n const len = r.len(psb && PSB_LONG_KEYS.has(key));\n const data = r.bytes(len);\n // Writers disagree on padding: take the gap up to the next signature, else the default.\n let pad = -1;\n for (let k = 0; k <= 3; k++) {\n if (k === r.remaining || (k < r.remaining && isSig(r, k))) {\n pad = k;\n break;\n }\n }\n if (pad < 0) {\n const def = (alignment - (len % alignment)) % alignment;\n pad = r.peek(def).every((b) => b === 0) && r.remaining >= def ? def : 0;\n }\n r.skip(pad);\n out.push({ key, data });\n }\n return out;\n}\n\nfunction maskParameterBytes(pf: number): number {\n return (pf & 1 ? 1 : 0) + (pf & 2 ? 8 : 0) + (pf & 4 ? 1 : 0) + (pf & 8 ? 8 : 0);\n}\n\n/** Photoshop writes the real mask before the parameters; the spec says after. Guess which. */\nfunction realBeforeParameters(d: Uint8Array): boolean {\n const fits = (at: number) => {\n if (at >= d.length) return false;\n const end = at + 1 + maskParameterBytes(d[at]);\n return d.length >= end && d.length <= end + 3;\n };\n if (fits(18)) return false;\n if (!fits(36)) return false;\n const known = (pf: number) => (pf & 0xf0) === 0;\n const bgOk = (at: number) => at + 1 < d.length && (d[at + 1] === 0 || d[at + 1] === 255);\n const photoshop = bgOk(18) && known(d[36]);\n const realAt = 19 + maskParameterBytes(d[18]);\n const spec = known(d[18]) && d.length >= realAt + 18 && d.length <= realAt + 21 && bgOk(realAt);\n return photoshop || !spec;\n}\n\nfunction parseMask(d: Uint8Array): PsdLayerMask | undefined {\n try {\n const r = new PsdReader(d);\n const m: PsdLayerMask = { rect: readRect(r), defaultColor: r.u8(), flags: r.u8() };\n const readReal = () => (r.remaining >= 18 ? { flags: r.u8(), background: r.u8(), rect: readRect(r) } : undefined);\n const params = (m.flags & MASK_FLAG_PARAMETERS) !== 0;\n const realFirst = params && realBeforeParameters(d);\n if (realFirst) m.real = readReal();\n if (params) {\n const pf = r.u8();\n if (pf & 1) m.userDensity = r.u8();\n if (pf & 2) m.userFeather = r.f64();\n if (pf & 4) m.vectorDensity = r.u8();\n if (pf & 8) m.vectorFeather = r.f64();\n }\n if (!realFirst) m.real = readReal();\n return m;\n } catch {\n return undefined;\n }\n}\n\nfunction readLayerRecord(r: PsdReader, psb: boolean): { record: PsdLayerRecord; lengths: number[] } {\n const rect = readRect(r);\n const n = r.u16();\n if (n > MAX_LAYER_CHANNELS) throw new PsdError(`A layer has ${n} channels (at most ${MAX_LAYER_CHANNELS}).`);\n const channels: PsdChannel[] = [];\n const lengths: number[] = [];\n for (let i = 0; i < n; i++) {\n channels.push({ id: r.i16(), compression: null, data: new Uint8Array(0) });\n lengths.push(r.len(psb));\n }\n if (r.ascii(4) !== '8BIM') throw new PsdError('A layer record has no \"8BIM\" blend-mode signature.');\n const blendKey = r.ascii(4);\n const opacity = r.u8();\n const clipping = r.u8();\n const flags = r.u8();\n r.u8();\n const x = r.sub(r.u32());\n const maskBytes = x.bytes(x.u32());\n const mask = maskBytes.length ? parseMask(maskBytes) : undefined;\n x.skip(x.u32()); // blending ranges\n const legacyName = decodeLegacyName(x.pascal(4));\n const blocks = readBlocks(x, psb, 2);\n return { record: { rect, channels, blendKey, opacity, clipping, flags, mask, legacyName, blocks }, lengths };\n}\n\nfunction readLayerInfo(data: Uint8Array, psb: boolean): { layers: PsdLayerRecord[]; mergedAlpha: boolean } {\n const r = new PsdReader(data);\n const count = r.i16();\n const n = Math.abs(count);\n r.checkCount(n, 34, 'Layer records');\n const read = [];\n for (let i = 0; i < n; i++) read.push(readLayerRecord(r, psb));\n for (const { record, lengths } of read) {\n record.channels.forEach((ch, i) => {\n const len = lengths[i];\n if (len === 0) return;\n if (len === 1) throw new PsdError('A channel has a data length of 1.');\n ch.compression = r.u16();\n ch.data = r.bytes(len - 2);\n });\n }\n return { layers: read.map((x) => x.record), mergedAlpha: count < 0 };\n}\n\n/** Parses a PSD or PSB. Throws `PsdError` with what was wrong. */\nexport function readPsd(bytes: Uint8Array): PsdFile {\n const r = new PsdReader(bytes);\n const header = readHeader(r);\n const { psb } = header;\n const colorModeData = r.bytes(r.u32());\n const resources = readResources(r);\n const s = r.sub(r.len(psb));\n let layers: PsdLayerRecord[] = [];\n let mergedAlpha = false;\n let globalBlocks: PsdTaggedBlock[] = [];\n if (s.remaining) {\n const liLen = s.len(psb);\n if (liLen > 0) ({ layers, mergedAlpha } = readLayerInfo(s.bytes(liLen), psb));\n if (s.remaining >= 4) {\n s.skip(s.u32()); // global layer mask\n globalBlocks = readBlocks(s, psb, 4);\n // 16- and 32-bit files keep their layers in an Lr16 / Lr32 block.\n const key = header.depth === 16 ? 'Lr16' : header.depth === 32 ? 'Lr32' : null;\n const block = globalBlocks.find((b) => (key ? b.key === key : !layers.length && ['Layr', 'Lr16', 'Lr32'].includes(b.key)));\n if (block) {\n ({ layers, mergedAlpha } = readLayerInfo(block.data, psb));\n globalBlocks = globalBlocks.filter((b) => b !== block);\n }\n }\n }\n const imageCompression = r.remaining >= 2 ? r.u16() : 0;\n return { header, colorModeData, resources, layers, mergedAlpha, globalBlocks, imageCompression, imageData: r.rest() };\n}\n\n// ---------------------------------------------------------------------------\n// Queries\n// ---------------------------------------------------------------------------\n\nexport function layerBlock(rec: PsdLayerRecord, key: string): Uint8Array | undefined {\n return rec.blocks.find((b) => b.key === key)?.data;\n}\n\nexport function layerName(rec: PsdLayerRecord): string {\n const luni = layerBlock(rec, 'luni');\n if (luni) {\n try {\n return new PsdReader(luni).unicode();\n } catch {\n // fall back to the legacy name\n }\n }\n return rec.legacyName;\n}\n\nexport interface SectionDivider {\n kind: number;\n blendKey?: string;\n}\n\nexport function sectionDivider(rec: PsdLayerRecord): SectionDivider | undefined {\n const d = layerBlock(rec, 'lsct') ?? layerBlock(rec, 'lsdk');\n if (!d || d.length < 4) return undefined;\n const r = new PsdReader(d);\n const kind = r.u32();\n if (r.remaining >= 8 && r.ascii(4) === '8BIM') return { kind, blendKey: r.ascii(4) };\n return { kind };\n}\n\nexport function u32Block(rec: PsdLayerRecord, key: string): number | undefined {\n const d = layerBlock(rec, key);\n return d && d.length >= 4 ? new PsdReader(d).u32() : undefined;\n}\n\nexport function u8Block(rec: PsdLayerRecord, key: string): number | undefined {\n const d = layerBlock(rec, key);\n return d && d.length >= 1 ? d[0] : undefined;\n}\n\nexport interface PsdTreeNode {\n /** Index into `PsdFile.layers` (the folder record for a group). */\n index: number;\n children?: PsdTreeNode[];\n}\n\n/** The layer tree, bottom-most first (the stored order, as in our model), from the `lsct` dividers. */\nexport function layerTree(file: PsdFile): PsdTreeNode[] {\n const stack: PsdTreeNode[][] = [[]];\n file.layers.forEach((rec, index) => {\n const kind = sectionDivider(rec)?.kind ?? SECTION_OTHER;\n if (kind === SECTION_DIVIDER) {\n stack.push([]);\n } else if (kind === SECTION_OPEN || kind === SECTION_CLOSED) {\n const children = stack.length > 1 ? stack.pop()! : [];\n stack[stack.length - 1].push({ index, children });\n } else {\n stack[stack.length - 1].push({ index });\n }\n });\n while (stack.length > 1) {\n const children = stack.pop()!;\n stack[stack.length - 1].push(...children);\n }\n return stack[0];\n}\n\nexport function channelRect(rec: PsdLayerRecord, id: number): PsdRect {\n if (id === CHANNEL_USER_MASK && rec.mask) return rec.mask.rect;\n if (id === CHANNEL_REAL_USER_MASK && rec.mask) return rec.mask.real?.rect ?? rec.mask.rect;\n return rec.rect;\n}\n\n/** One layer channel decoded to `depth`-bit planar bytes, or `undefined` if the layer has none. */\nexport function decodeLayerChannel(file: PsdFile, rec: PsdLayerRecord, id: number): Uint8Array | undefined {\n const ch = rec.channels.find((c) => c.id === id);\n if (!ch) return undefined;\n const { width, height } = rectSize(channelRect(rec, id));\n if (ch.compression === null) {\n if (width * height === 0) return new Uint8Array(0);\n throw new PsdError(`Channel ${id} of layer \"${layerName(rec)}\" has no data.`);\n }\n return decodePlanes(ch.compression, ch.data, { planes: 1, width, height, depth: file.header.depth, psb: file.header.psb });\n}\n\n/** The merged image's channels decoded, planar. */\nexport function decodeMergedImage(file: PsdFile): Uint8Array {\n const { channels, width, height, depth, psb } = file.header;\n return decodePlanes(file.imageCompression, file.imageData, { planes: channels, width, height, depth, psb });\n}\n\nexport function resource(file: PsdFile, id: number): Uint8Array | undefined {\n return file.resources.find((r) => r.id === id)?.data;\n}\n\n/** Resolution (1005) in pixels per inch, or `undefined`. */\nexport function resolution(file: PsdFile): number | undefined {\n const d = resource(file, 1005);\n if (!d || d.length < 4) return undefined;\n return new PsdReader(d).u32() / 65536;\n}\n\nexport { rowBytes };\n\n// ---------------------------------------------------------------------------\n// Writing\n// ---------------------------------------------------------------------------\n\nfunction writeRect(w: PsdWriter, r: PsdRect): void {\n w.i32(r.top);\n w.i32(r.left);\n w.i32(r.bottom);\n w.i32(r.right);\n}\n\nfunction writeBlocks(w: PsdWriter, blocks: PsdTaggedBlock[], psb: boolean, alignment: number): void {\n for (const b of blocks) {\n const long = psb && PSB_LONG_KEYS.has(b.key);\n w.ascii(long ? '8B64' : '8BIM');\n w.ascii(b.key);\n // Pad inside the declared length, as Photoshop does, so readers that skip by length stay aligned.\n const pad = (alignment - (b.data.length % alignment)) % alignment;\n if (long) w.u64(b.data.length + pad);\n else w.u32(b.data.length + pad);\n w.bytes(b.data);\n w.zeros(pad);\n }\n}\n\nfunction writeMask(w: PsdWriter, m: PsdLayerMask): void {\n writeRect(w, m.rect);\n w.u8(m.defaultColor);\n const pf = (m.userDensity !== undefined ? 1 : 0) | (m.userFeather !== undefined ? 2 : 0);\n w.u8(pf ? m.flags | MASK_FLAG_PARAMETERS : m.flags & ~MASK_FLAG_PARAMETERS);\n if (pf) {\n w.u8(pf);\n if (m.userDensity !== undefined) w.u8(m.userDensity);\n if (m.userFeather !== undefined) w.f64(m.userFeather);\n }\n // Pad to the 20-byte minimum the spec asks for.\n const len = 18 + (pf ? 1 + maskParameterBytes(pf) : 0);\n w.zeros(Math.max(0, 20 - len) + ((4 - (Math.max(20, len) % 4)) % 4));\n}\n\nfunction writeLayerRecord(w: PsdWriter, rec: PsdLayerRecord, psb: boolean, nameBytes: Uint8Array): void {\n writeRect(w, rec.rect);\n w.u16(rec.channels.length);\n for (const c of rec.channels) {\n w.i16(c.id);\n const len = c.compression === null ? 0 : 2 + c.data.length;\n if (psb) w.u64(len);\n else w.u32(len);\n }\n if (rec.blendKey.length !== 4) throw new PsdError(`Blend key \"${rec.blendKey}\" must be 4 characters.`);\n w.ascii('8BIM');\n w.ascii(rec.blendKey);\n w.u8(rec.opacity);\n w.u8(rec.clipping);\n w.u8(rec.flags);\n w.u8(0);\n const extra = w.beginLen(false);\n const mask = w.beginLen(false);\n if (rec.mask) writeMask(w, rec.mask);\n w.endLen(mask);\n // Blending ranges: composite gray plus one per colour channel, all full.\n w.u32(40);\n for (let i = 0; i < 5; i++) w.bytes([0, 0, 255, 255, 0, 0, 255, 255]);\n w.pascal(nameBytes, 4);\n writeBlocks(w, rec.blocks, psb, 2);\n w.endLen(extra);\n}\n\n/** Serializes a file. Channel data must already be encoded; `encodeName` writes legacy (Pascal) names. */\nexport function writePsd(file: PsdFile, encodeName: (s: string) => Uint8Array): Uint8Array {\n const { header } = file;\n const { psb } = header;\n const w = new PsdWriter();\n w.ascii('8BPS');\n w.u16(psb ? 2 : 1);\n w.zeros(6);\n w.u16(header.channels);\n w.u32(header.height);\n w.u32(header.width);\n w.u16(header.depth);\n w.u16(header.colorMode);\n w.u32(file.colorModeData.length);\n w.bytes(file.colorModeData);\n const res = w.beginLen(false);\n for (const r of file.resources) {\n w.ascii('8BIM');\n w.u16(r.id);\n w.pascal(encodeName(r.name), 2);\n w.u32(r.data.length);\n w.bytes(r.data);\n if (r.data.length % 2) w.u8(0);\n }\n w.endLen(res);\n const lm = w.beginLen(psb);\n if (file.layers.length || file.globalBlocks.length) {\n const li = w.beginLen(psb);\n if (file.layers.length) {\n const start = w.length;\n w.i16(file.mergedAlpha ? -file.layers.length : file.layers.length);\n for (const rec of file.layers) writeLayerRecord(w, rec, psb, encodeName(rec.legacyName));\n for (const rec of file.layers) {\n for (const c of rec.channels) {\n if (c.compression === null) continue;\n w.u16(c.compression);\n w.bytes(c.data);\n }\n }\n if ((w.length - start) % 2) w.u8(0);\n }\n w.endLen(li);\n w.u32(0); // no global layer mask\n writeBlocks(w, file.globalBlocks, psb, 4);\n }\n w.endLen(lm);\n w.u16(file.imageCompression);\n w.bytes(file.imageData);\n return w.result();\n}\n","// Portions ported from photocraft/crates/io/src/pixels.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Planar PSD channels (any colour mode, 1 / 8 / 16 / 32 bits) → 8-bit sRGB RGBA, and back. */\n\nimport { D50, XYZ_D50_TO_SRGB } from '../adjustment-layers';\nimport { linearToSrgb } from '../color';\nimport { PsdError } from './binary';\n\nexport const MODE_BITMAP = 0;\nexport const MODE_GRAYSCALE = 1;\nexport const MODE_INDEXED = 2;\nexport const MODE_RGB = 3;\nexport const MODE_CMYK = 4;\nexport const MODE_MULTICHANNEL = 7;\nexport const MODE_DUOTONE = 8;\nexport const MODE_LAB = 9;\n\n/** Colour channels of a mode (alpha and spot channels come after them). */\nexport function colorChannelCount(mode: number, headerChannels: number): number {\n switch (mode) {\n case MODE_RGB:\n case MODE_LAB:\n return 3;\n case MODE_CMYK:\n return 4;\n case MODE_MULTICHANNEL:\n return Math.min(3, headerChannels);\n default:\n return 1;\n }\n}\n\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\n/** A plane's sample at `i` as 0..1 (`gamma`: 32-bit samples are linear light, so encode them). */\nfunction sampler(plane: Uint8Array | undefined, depth: number, width: number, encode: boolean, fill: number): (i: number) => number {\n if (!plane) return () => fill;\n switch (depth) {\n case 8:\n return (i) => plane[i] / 255;\n case 16:\n return (i) => ((plane[2 * i] << 8) | plane[2 * i + 1]) / 65535;\n case 32: {\n const view = new DataView(plane.buffer, plane.byteOffset, plane.byteLength);\n return (i) => {\n const v = clamp01(view.getFloat32(4 * i, false) || 0);\n return encode ? linearToSrgb(v) : v;\n };\n }\n case 1: {\n const rb = Math.ceil(width / 8);\n // Bitmap: a set bit is black.\n return (i) => {\n const x = i % width;\n const y = (i - x) / width;\n return (plane[y * rb + (x >> 3)] >> (7 - (x & 7))) & 1 ? 0 : 1;\n };\n }\n default:\n throw new PsdError(`Unsupported bit depth ${depth}.`);\n }\n}\n\nfunction labToSrgb(l: number, a: number, b: number): [number, number, number] {\n const fy = (l + 16) / 116;\n const f = (t: number) => (t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29));\n const x = f(fy + a / 500) * D50[0];\n const y = f(fy) * D50[1];\n const z = f(fy - b / 200) * D50[2];\n const m = XYZ_D50_TO_SRGB;\n return [\n linearToSrgb(clamp01(m[0][0] * x + m[0][1] * y + m[0][2] * z)),\n linearToSrgb(clamp01(m[1][0] * x + m[1][1] * y + m[1][2] * z)),\n linearToSrgb(clamp01(m[2][0] * x + m[2][1] * y + m[2][2] * z)),\n ];\n}\n\nexport interface ChannelSet {\n width: number;\n height: number;\n depth: number;\n mode: number;\n /** Colour planes in mode order (missing planes read as 0, or no ink for CMYK). */\n color: Array<Uint8Array | undefined>;\n /** Missing: opaque. */\n alpha?: Uint8Array;\n /** Indexed: 768-byte palette (all reds, all greens, all blues). */\n palette?: Uint8Array;\n}\n\n/** Interleaved 8-bit sRGB RGBA. */\nexport function toRgba8(set: ChannelSet): Uint8ClampedArray {\n const { width, height, depth, mode } = set;\n const n = width * height;\n const out = new Uint8ClampedArray(n * 4);\n const linear = depth === 32;\n const isLab = mode === MODE_LAB;\n const cmyk = mode === MODE_CMYK;\n // CMYK planes store 1 for no ink; a missing plane is no ink too.\n const planes = set.color.map((p) => sampler(p, depth, width, linear && !isLab && !cmyk, cmyk ? 1 : 0));\n const alpha = sampler(set.alpha, depth, width, false, 1);\n const fast8 = depth === 8 && mode === MODE_RGB && set.color.every(Boolean);\n if (fast8) {\n const [r, g, b] = set.color as Uint8Array[];\n const a = set.alpha;\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n out[o] = r[i];\n out[o + 1] = g[i];\n out[o + 2] = b[i];\n out[o + 3] = a ? a[i] : 255;\n }\n return out;\n }\n const palette = set.palette;\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n let r: number;\n let g: number;\n let b: number;\n switch (mode) {\n case MODE_RGB:\n case MODE_MULTICHANNEL:\n r = planes[0](i);\n g = planes[1] ? planes[1](i) : r;\n b = planes[2] ? planes[2](i) : r;\n break;\n case MODE_CMYK: {\n const k = planes[3] ? planes[3](i) : 1;\n r = planes[0](i) * k;\n g = planes[1](i) * k;\n b = planes[2](i) * k;\n break;\n }\n case MODE_LAB:\n [r, g, b] = labToSrgb(planes[0](i) * 100, planes[1](i) * 255 - 128, planes[2](i) * 255 - 128);\n break;\n case MODE_INDEXED: {\n const idx = Math.round(planes[0](i) * 255);\n if (palette && palette.length >= 768) {\n out[o] = palette[idx];\n out[o + 1] = palette[256 + idx];\n out[o + 2] = palette[512 + idx];\n out[o + 3] = Math.round(alpha(i) * 255);\n continue;\n }\n r = g = b = idx / 255;\n break;\n }\n default:\n r = g = b = planes[0](i);\n }\n out[o] = Math.round(r * 255);\n out[o + 1] = Math.round(g * 255);\n out[o + 2] = Math.round(b * 255);\n out[o + 3] = Math.round(alpha(i) * 255);\n }\n return out;\n}\n\n/** An 8-bit coverage plane (masks) from any depth. */\nexport function toMask8(plane: Uint8Array, width: number, height: number, depth: number): Uint8ClampedArray {\n if (depth === 8) return new Uint8ClampedArray(plane.buffer, plane.byteOffset, width * height);\n const s = sampler(plane, depth, width, false, 0);\n const out = new Uint8ClampedArray(width * height);\n for (let i = 0; i < out.length; i++) out[i] = Math.round(s(i) * 255);\n return out;\n}\n\n/** Splits interleaved RGBA into planes `[r, g, b, a]` (8-bit). */\nexport function rgbaToPlanes(data: ArrayLike<number>, n: number): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {\n const planes: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] = [new Uint8Array(n), new Uint8Array(n), new Uint8Array(n), new Uint8Array(n)];\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n planes[0][i] = data[o];\n planes[1][i] = data[o + 1];\n planes[2][i] = data[o + 2];\n planes[3][i] = data[o + 3];\n }\n return planes;\n}\n\n/**\n * Photoshop stores a transparent merged image matted over white (colour × alpha + white × (1 −\n * alpha)). `unmatteWhite` recovers the colour in place; `matteWhite` applies the matte.\n */\nexport function unmatteWhite(data: Uint8ClampedArray): void {\n for (let i = 0; i < data.length; i += 4) {\n const a = data[i + 3];\n if (a === 255 || a === 0) continue;\n const k = 255 - a;\n for (let c = 0; c < 3; c++) data[i + c] = Math.round(((data[i + c] - k) * 255) / a);\n }\n}\n\nexport function matteWhite(data: ArrayLike<number>): Uint8ClampedArray {\n const out = Uint8ClampedArray.from(data);\n for (let i = 0; i < out.length; i += 4) {\n const a = out[i + 3];\n if (a === 255) continue;\n for (let c = 0; c < 3; c++) out[i + c] = Math.round((out[i + c] * a + 255 * (255 - a)) / 255);\n }\n return out;\n}\n","// Portions ported from photocraft-main/crates/io/src/effects_map.rs and blocks.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Layer effects ↔ PSD: the object-based effects descriptor (`lfx2`, `lfxs` on groups, `lmfx` with\n * several instances per kind) both ways, and the legacy `lrFX` block on read. Key names follow the\n * descriptors Photoshop writes (documented by reverse-engineering in psd-tools and ag-psd, MIT).\n *\n * Reading reports what it could not represent exactly (`notes`), so import warns only for that.\n */\n\nimport type { BlendMode } from '../blend';\nimport {\n defaultLayerEffect,\n effectColorHex,\n effectsScale,\n scaleLayerEffect,\n newEffectId,\n parseEffectColor,\n type ContourPoint,\n type EffectContour,\n type EffectFill,\n type EffectGradient,\n type GradientStyle,\n type LayerEffect,\n type LayerEffectKind,\n type LayerEffects,\n} from '../../raster/effects/model';\nimport { PsdReader, PsdWriter } from './binary';\nimport { descBool, descGet, descNumber, descObject, descriptor, parseVersionedDescriptor, versionedDescriptorBytes, type DescValue, type Descriptor } from './descriptor';\nimport { decodePsdColor } from './adjust';\nimport { PSD_BLEND_KEYS } from './import';\n\n/** Descriptor blend ids (four-character codes) → our modes. */\nconst BLEND_CODES: Array<[BlendMode, string]> = [\n ['normal', 'Nrml'],\n ['darken', 'Drkn'],\n ['multiply', 'Mltp'],\n ['color-burn', 'CBrn'],\n ['linear-burn', 'linearBurn'],\n ['darker-color', 'darkerColor'],\n ['lighten', 'Lghn'],\n ['screen', 'Scrn'],\n ['color-dodge', 'CDdg'],\n ['linear-dodge', 'linearDodge'],\n ['lighter-color', 'lighterColor'],\n ['overlay', 'Ovrl'],\n ['soft-light', 'SftL'],\n ['hard-light', 'HrdL'],\n ['vivid-light', 'vividLight'],\n ['linear-light', 'linearLight'],\n ['pin-light', 'pinLight'],\n ['hard-mix', 'hardMix'],\n ['difference', 'Dfrn'],\n ['exclusion', 'Xclu'],\n ['subtract', 'blendSubtraction'],\n ['divide', 'blendDivide'],\n ['hue', 'H '],\n ['saturation', 'Strt'],\n ['color', 'Clr '],\n ['luminosity', 'Lmns'],\n];\n\n/** Long string ids some writers use instead of the four-character codes. */\nconst BLEND_LONG: Array<[BlendMode, string]> = [\n ['normal', 'normal'],\n ['darken', 'darken'],\n ['multiply', 'multiply'],\n ['color-burn', 'colorBurn'],\n ['lighten', 'lighten'],\n ['screen', 'screen'],\n ['color-dodge', 'colorDodge'],\n ['overlay', 'overlay'],\n ['soft-light', 'softLight'],\n ['hard-light', 'hardLight'],\n ['difference', 'difference'],\n ['exclusion', 'exclusion'],\n ['hue', 'hue'],\n ['saturation', 'saturation'],\n ['color', 'color'],\n ['luminosity', 'luminosity'],\n];\n\nconst BLEND_OF = new Map<string, BlendMode>([...BLEND_CODES, ...BLEND_LONG].map(([m, id]) => [id, m]));\nconst BLEND_ID = new Map<BlendMode, string>(BLEND_CODES);\n\n/** What reading met that our model cannot hold exactly (one short phrase each). */\ntype Notes = Set<string>;\n\n// ---------------------------------------------------------------------------\n// Descriptor helpers\n// ---------------------------------------------------------------------------\n\nconst num = (d: Descriptor, key: string, fallback: number) => descNumber(d, key) ?? fallback;\nconst pct = (d: Descriptor, key: string, fallback: number) => num(d, key, fallback * 100) / 100;\nconst bool = (d: Descriptor, key: string) => descBool(d, key) === true;\nconst enumOf = (d: Descriptor, key: string): string | undefined => {\n const v = descGet(d, key);\n return v?.type === 'enum' ? v.value : undefined;\n};\nconst text = (d: Descriptor | undefined, key: string): string => {\n const v = d ? descGet(d, key) : undefined;\n return v?.type === 'TEXT' ? v.value : '';\n};\nconst list = (d: Descriptor, key: string): Descriptor[] => {\n const v = descGet(d, key);\n if (v?.type !== 'VlLs') return [];\n return v.items.flatMap((it) => (it.type === 'Objc' || it.type === 'GlbO' ? [it.value] : []));\n};\nconst clamp = (v: number, lo: number, hi: number) => (Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : lo);\n\nconst unit = (u: string, value: number): DescValue => ({ type: 'UntF', unit: u, value });\nconst dbl = (value: number): DescValue => ({ type: 'doub', value });\nconst long = (value: number): DescValue => ({ type: 'long', value: Math.round(value) });\nconst boolV = (value: boolean): DescValue => ({ type: 'bool', value });\nconst enumV = (typeId: string, value: string): DescValue => ({ type: 'enum', typeId, value });\nconst textV = (value: string): DescValue => ({ type: 'TEXT', value });\nconst obj = (d: Descriptor): DescValue => ({ type: 'Objc', value: d });\n\nfunction blendOf(d: Descriptor, key: string, fallback: BlendMode, notes: Notes): BlendMode {\n const id = enumOf(d, key);\n if (id === undefined) return fallback;\n const m = BLEND_OF.get(id);\n if (m) return m;\n notes.add(id === 'Dslv' || id === 'dissolve' ? 'Dissolve became Normal' : `an unknown blend mode \"${id}\" became Normal`);\n return 'normal';\n}\n\n/** A descriptor colour (RGB, HSB, CMYK, Lab, grayscale, or 0..1 floats) as `#rrggbb`. */\nfunction colorOf(d: Descriptor | undefined, fallback = '#000000'): string {\n if (!d) return fallback;\n const g = (k: string) => descNumber(d, k);\n if (g('redFloat') !== undefined) return effectColorHex([g('redFloat')!, g('greenFloat') ?? 0, g('blueFloat') ?? 0]);\n switch (d.classId) {\n case 'HSBC': {\n const rgb = decodePsdColor(1, [((g('H ') ?? 0) / 360) * 65535, ((g('Strt') ?? 0) / 100) * 65535, ((g('Brgh') ?? 0) / 100) * 65535]);\n return rgb ? effectColorHex(rgb) : fallback;\n }\n case 'CMYC': {\n const inv = (k: string) => (1 - (g(k) ?? 0) / 100) * 65535;\n const rgb = decodePsdColor(2, [inv('Cyn '), inv('Mgnt'), inv('Ylw '), inv('Blck')]);\n return rgb ? effectColorHex(rgb) : fallback;\n }\n case 'LbCl': {\n const rgb = decodePsdColor(7, [(g('Lmnc') ?? 0) * 100, Math.round((g('A ') ?? 0) * 100) & 0xffff, Math.round((g('B ') ?? 0) * 100) & 0xffff]);\n return rgb ? effectColorHex(rgb) : fallback;\n }\n case 'Grsc': {\n const v = 1 - (g('Gry ') ?? 0) / 100;\n return effectColorHex([v, v, v]);\n }\n }\n const r = g('Rd ');\n const gr = g('Grn ');\n const b = g('Bl ');\n if (r === undefined || gr === undefined || b === undefined) return fallback;\n return effectColorHex([r / 255, gr / 255, b / 255]);\n}\n\nfunction colorDesc(hex: string): Descriptor {\n const [r, g, b] = parseEffectColor(hex) ?? [0, 0, 0];\n return descriptor('RGBC', [\n ['Rd ', dbl(r * 255)],\n ['Grn ', dbl(g * 255)],\n ['Bl ', dbl(b * 255)],\n ]);\n}\n\nfunction contourOf(d: Descriptor, key: string): EffectContour | undefined {\n const c = descObject(d, key);\n if (!c) return undefined;\n const name = text(c, 'Nm ');\n const points: ContourPoint[] = list(c, 'Crv ').map((p) => {\n const out: ContourPoint = { input: clamp(num(p, 'Hrzn', 0) / 255, 0, 1), output: clamp(num(p, 'Vrtc', 0) / 255, 0, 1) };\n // `Cnty` (continuity) false marks a corner; absent means smooth.\n if (descBool(p, 'Cnty') === false) out.corner = true;\n return out;\n });\n const linear = points.length === 2 && points[0].input === 0 && points[0].output === 0 && points[1].input === 1 && points[1].output === 1 && !points.some((p) => p.corner);\n if (points.length < 2 || (linear && (!name || name === 'Linear'))) return undefined;\n return { name, points };\n}\n\nfunction contourDesc(c: EffectContour | undefined): DescValue {\n const name = c?.name ?? 'Linear';\n const points = c?.points ?? [\n { input: 0, output: 0 },\n { input: 1, output: 1 },\n ];\n const pts: DescValue = {\n type: 'VlLs',\n items: points.map((p) => {\n const items: Array<[string, DescValue]> = [\n ['Hrzn', dbl(p.input * 255)],\n ['Vrtc', dbl(p.output * 255)],\n ];\n if (p.corner) items.push(['Cnty', boolV(false)]);\n return obj(descriptor('CrPt', items));\n }),\n };\n return obj(\n descriptor('ShpC', [\n ['Nm ', textV(name)],\n ['Crv ', pts],\n ])\n );\n}\n\nconst GRADIENT_STYLES: Array<[GradientStyle, string]> = [\n ['linear', 'Lnr '],\n ['radial', 'Rdl '],\n ['angle', 'Angl'],\n ['reflected', 'Rflc'],\n ['diamond', 'Dmnd'],\n];\n\n/** A `Grad` descriptor's stops: midpoints become an extra stop each; smoothness is drawn linearly. */\nfunction gradientOf(d: Descriptor, notes: Notes): EffectGradient {\n const g: EffectGradient = {\n stops: [\n { position: 0, color: '#000000' },\n { position: 1, color: '#ffffff' },\n ],\n style: (GRADIENT_STYLES.find(([, id]) => id === enumOf(d, 'Type'))?.[0] ?? 'linear') as GradientStyle,\n angle: clamp(num(d, 'Angl', 90), -360, 360),\n scale: clamp(pct(d, 'Scl ', 1), 0.1, 1.5),\n reverse: bool(d, 'Rvrs'),\n align: descBool(d, 'Algn') !== false,\n offset: { x: 0, y: 0 },\n };\n const off = descObject(d, 'Ofst');\n if (off) g.offset = { x: clamp(pct(off, 'Hrzn', 0), -1, 1), y: clamp(pct(off, 'Vrtc', 0), -1, 1) };\n const grad = descObject(d, 'Grad');\n if (!grad) return g;\n if (enumOf(grad, 'GrdF') === 'ClNs') {\n notes.add('noise gradients became black to white');\n return g;\n }\n const raw = list(grad, 'Clrs')\n .map((s) => {\n const type = enumOf(s, 'Type');\n if (type === 'FrgC' || type === 'BckC') notes.add('foreground / background gradient stops became black / white');\n const color = type === 'BckC' ? '#ffffff' : colorOf(descObject(s, 'Clr '));\n return { position: clamp(num(s, 'Lctn', 0) / 4096, 0, 1), color, mid: clamp(num(s, 'Mdpn', 50) / 100, 0.05, 0.95) };\n })\n .sort((a, b) => a.position - b.position);\n if (raw.length) {\n const stops: EffectGradient['stops'] = [];\n raw.forEach((s, i) => {\n // A midpoint (stored on the stop that ends its segment) becomes an even mix at that point.\n if (i > 0 && Math.abs(s.mid - 0.5) > 1e-3) {\n const a = raw[i - 1];\n const ca = parseEffectColor(a.color)!;\n const cb = parseEffectColor(s.color)!;\n stops.push({ position: a.position + (s.position - a.position) * s.mid, color: effectColorHex(ca.map((v, k) => (v + cb[k]) / 2)) });\n notes.add('gradient midpoints are drawn as straight segments');\n }\n stops.push({ position: s.position, color: s.color });\n });\n g.stops = stops;\n }\n const opacity = list(grad, 'Trns')\n .map((s) => ({ position: clamp(num(s, 'Lctn', 0) / 4096, 0, 1), opacity: clamp(num(s, 'Opct', 100) / 100, 0, 1) }))\n .sort((a, b) => a.position - b.position);\n if (opacity.some((o) => o.opacity < 1)) g.opacityStops = opacity;\n if (num(grad, 'Intr', 0) > 0) notes.add('gradient smoothness is drawn as straight segments');\n return g;\n}\n\nfunction gradientItems(g: EffectGradient): Array<[string, DescValue]> {\n const clrs: DescValue = {\n type: 'VlLs',\n items: g.stops.map((s) =>\n obj(\n descriptor('Clrt', [\n ['Clr ', obj(colorDesc(s.color))],\n ['Type', enumV('Clry', 'UsrS')],\n ['Lctn', long(s.position * 4096)],\n ['Mdpn', long(50)],\n ])\n )\n ),\n };\n const op = g.opacityStops?.length ? g.opacityStops : g.stops.map((s) => ({ position: s.position, opacity: 1 }));\n const trns: DescValue = {\n type: 'VlLs',\n items: op.map((s) =>\n obj(\n descriptor('TrnS', [\n ['Opct', unit('#Prc', s.opacity * 100)],\n ['Lctn', long(s.position * 4096)],\n ['Mdpn', long(50)],\n ])\n )\n ),\n };\n const grad = descriptor('Grdn', [\n ['Nm ', textV('Custom')],\n ['GrdF', enumV('GrdF', 'CstS')],\n ['Intr', dbl(0)],\n ['Clrs', clrs],\n ['Trns', trns],\n ]);\n return [\n ['Grad', obj(grad)],\n ['Angl', unit('#Ang', g.angle)],\n ['Type', enumV('GrdT', GRADIENT_STYLES.find(([s]) => s === g.style)![1])],\n ['Rvrs', boolV(g.reverse)],\n ['Algn', boolV(g.align)],\n ['Scl ', unit('#Prc', g.scale * 100)],\n [\n 'Ofst',\n obj(\n descriptor('Pnt ', [\n ['Hrzn', unit('#Prc', g.offset.x * 100)],\n ['Vrtc', unit('#Prc', g.offset.y * 100)],\n ])\n ),\n ],\n ];\n}\n\nfunction patternRefOf(d: Descriptor): { patternId: string; name: string } {\n const p = descObject(d, 'Ptrn');\n return { patternId: text(p, 'Idnt') || text(p, 'Nm '), name: text(p, 'Nm ') };\n}\n\nfunction patternRefDesc(patternId: string, name: string | undefined): DescValue {\n return obj(\n descriptor('Ptrn', [\n ['Nm ', textV(name ?? patternId)],\n ['Idnt', textV(patternId)],\n ])\n );\n}\n\nfunction placementOf(d: Descriptor): { angle: number; link: boolean; phase: { x: number; y: number } } {\n const phase = descObject(d, 'phase');\n return {\n angle: clamp(num(d, 'Angl', 0), -360, 360),\n link: descBool(d, 'Algn') !== false,\n phase: phase ? { x: num(phase, 'Hrzn', 0), y: num(phase, 'Vrtc', 0) } : { x: 0, y: 0 },\n };\n}\n\nfunction placementItems(angle: number, link: boolean, phase: { x: number; y: number }): Array<[string, DescValue]> {\n const out: Array<[string, DescValue]> = [];\n if (angle) out.push(['Angl', unit('#Ang', angle)]);\n out.push(['Algn', boolV(link)]);\n out.push([\n 'phase',\n obj(\n descriptor('Pnt ', [\n ['Hrzn', dbl(phase.x)],\n ['Vrtc', dbl(phase.y)],\n ])\n ),\n ]);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\n/** Single-instance key and multi-instance list key per kind, in Photoshop's order. */\nconst KINDS: Array<[string, string, LayerEffectKind]> = [\n ['DrSh', 'dropShadowMulti', 'drop-shadow'],\n ['IrSh', 'innerShadowMulti', 'inner-shadow'],\n ['OrGl', '', 'outer-glow'],\n ['IrGl', '', 'inner-glow'],\n ['ebbl', '', 'bevel-emboss'],\n ['ChFX', '', 'satin'],\n ['SoFi', 'solidFillMulti', 'color-overlay'],\n ['GrFl', 'gradientFillMulti', 'gradient-overlay'],\n ['patternFill', '', 'pattern-overlay'],\n ['FrFX', 'frameFXMulti', 'stroke'],\n];\n\nfunction common(d: Descriptor, kind: LayerEffectKind, notes: Notes) {\n const def = defaultLayerEffect(kind, 'x') as { blend: BlendMode; opacity: number };\n return { id: newEffectId(), enabled: descBool(d, 'enab') !== false, blend: blendOf(d, 'Md ', def.blend, notes), opacity: clamp(pct(d, 'Opct', def.opacity), 0, 1) };\n}\n\nfunction readOne(kind: LayerEffectKind, d: Descriptor, notes: Notes): LayerEffect {\n const size = (key: string, fallback: number) => clamp(num(d, key, fallback), 0, 250);\n switch (kind) {\n case 'drop-shadow':\n case 'inner-shadow': {\n const base = {\n ...common(d, kind, notes),\n color: colorOf(descObject(d, 'Clr ')),\n angle: clamp(num(d, 'lagl', 120), -360, 360),\n useGlobalLight: descBool(d, 'uglg') !== false,\n distance: clamp(num(d, 'Dstn', 5), 0, 30000),\n size: size('blur', 5),\n contour: contourOf(d, 'TrnS'),\n antiAlias: bool(d, 'AntA'),\n noise: clamp(pct(d, 'Nose', 0), 0, 1),\n };\n const spread = clamp(pct(d, 'Ckmt', 0), 0, 1);\n return kind === 'drop-shadow'\n ? { ...base, kind, spread, knocksOut: descBool(d, 'layerConceals') !== false }\n : { ...base, kind, choke: spread };\n }\n case 'outer-glow':\n case 'inner-glow': {\n const fill: EffectFill = descGet(d, 'Grad') ? { type: 'gradient', gradient: gradientOf(d, notes) } : { type: 'color', color: colorOf(descObject(d, 'Clr '), '#ffffbe') };\n const jitter = clamp(pct(d, 'ShdN', 0), 0, 1);\n if (jitter > 0) notes.add('glow jitter is not drawn');\n const base = {\n ...common(d, kind, notes),\n fill: fill as Extract<EffectFill, { type: 'color' | 'gradient' }>,\n technique: enumOf(d, 'GlwT') === 'PrBL' ? ('precise' as const) : ('softer' as const),\n size: size('blur', 5),\n contour: contourOf(d, 'TrnS'),\n antiAlias: bool(d, 'AntA'),\n range: clamp(pct(d, 'Inpr', 0.5), 0.01, 1),\n jitter,\n noise: clamp(pct(d, 'Nose', 0), 0, 1),\n };\n const spread = clamp(pct(d, 'Ckmt', 0), 0, 1);\n return kind === 'outer-glow' ? { ...base, kind, spread } : { ...base, kind, choke: spread, source: enumOf(d, 'glwS') === 'SrcC' ? 'center' : 'edge' };\n }\n case 'bevel-emboss': {\n const styles: Record<string, 'inner-bevel' | 'outer-bevel' | 'emboss' | 'pillow-emboss' | 'stroke-emboss'> = { OtrB: 'outer-bevel', InrB: 'inner-bevel', Embs: 'emboss', PlEb: 'pillow-emboss', strokeEmboss: 'stroke-emboss' };\n const out: LayerEffect = {\n id: newEffectId(),\n enabled: descBool(d, 'enab') !== false,\n kind,\n style: styles[enumOf(d, 'bvlS') ?? ''] ?? 'inner-bevel',\n technique: enumOf(d, 'bvlT') === 'PrBL' ? 'chisel-hard' : enumOf(d, 'bvlT') === 'Slmt' ? 'chisel-soft' : 'smooth',\n depth: clamp(pct(d, 'srgR', 1), 0, 10),\n direction: enumOf(d, 'bvlD') === 'Out ' ? 'down' : 'up',\n size: size('blur', 5),\n soften: clamp(num(d, 'Sftn', 0), 0, 16),\n angle: clamp(num(d, 'lagl', 120), -360, 360),\n altitude: clamp(num(d, 'Lald', 30), 0, 90),\n useGlobalLight: descBool(d, 'uglg') !== false,\n glossContour: contourOf(d, 'TrnS'),\n highlightColor: colorOf(descObject(d, 'hglC'), '#ffffff'),\n highlightBlend: blendOf(d, 'hglM', 'screen', notes),\n highlightOpacity: clamp(pct(d, 'hglO', 0.75), 0, 1),\n shadowColor: colorOf(descObject(d, 'sdwC')),\n shadowBlend: blendOf(d, 'sdwM', 'multiply', notes),\n shadowOpacity: clamp(pct(d, 'sdwO', 0.75), 0, 1),\n };\n if (bool(d, 'useShape')) out.contourElement = { contour: contourOf(d, 'MpgS') ?? { name: 'Linear', points: [{ input: 0, output: 0 }, { input: 1, output: 1 }] }, range: clamp(pct(d, 'Inpr', 0.5), 0, 1), antiAlias: bool(d, 'AntA') };\n if (bool(d, 'useTexture')) {\n const ref = patternRefOf(d);\n const place = placementOf(d);\n out.texture = { patternId: ref.patternId, name: ref.name, scale: clamp(pct(d, 'Scl ', 1), 0.01, 10), depth: clamp(pct(d, 'textureDepth', 1), -10, 10), invert: bool(d, 'InvT'), link: place.link, phase: place.phase };\n }\n return out;\n }\n case 'satin':\n return {\n ...common(d, kind, notes),\n kind,\n color: colorOf(descObject(d, 'Clr ')),\n angle: clamp(num(d, 'lagl', 19), -360, 360),\n distance: size('Dstn', 11),\n size: size('blur', 14),\n contour: contourOf(d, 'MpgS'),\n antiAlias: bool(d, 'AntA'),\n invert: bool(d, 'Invr'),\n };\n case 'color-overlay':\n return { ...common(d, kind, notes), kind, color: colorOf(descObject(d, 'Clr ')) };\n case 'gradient-overlay':\n return { ...common(d, kind, notes), kind, gradient: gradientOf(d, notes), dither: bool(d, 'Dthr') };\n case 'pattern-overlay': {\n const ref = patternRefOf(d);\n const place = placementOf(d);\n return { ...common(d, kind, notes), kind, patternId: ref.patternId, name: ref.name, scale: clamp(pct(d, 'Scl ', 1), 0.01, 10), angle: place.angle, link: place.link, phase: place.phase };\n }\n case 'stroke': {\n const paint = enumOf(d, 'PntT');\n let fill: EffectFill;\n if (paint === 'GrFl') fill = { type: 'gradient', gradient: gradientOf(d, notes) };\n else if (paint === 'Ptrn') {\n const ref = patternRefOf(d);\n fill = { type: 'pattern', patternId: ref.patternId, name: ref.name, scale: clamp(pct(d, 'Scl ', 1), 0.01, 10) };\n } else fill = { type: 'color', color: colorOf(descObject(d, 'Clr ')) };\n const pos = enumOf(d, 'Styl');\n return { ...common(d, kind, notes), kind, size: size('Sz ', 3), position: pos === 'InsF' ? 'inside' : pos === 'CtrF' ? 'center' : 'outside', fill };\n }\n }\n}\n\nexport interface ParsedLayerEffects {\n effects: LayerEffects;\n /** What could not be held exactly (empty when the mapping is exact). */\n notes: string[];\n}\n\n/**\n * Parses `lfx2` / `lfxs` / `lmfx` block data (u32 version 0, then a versioned descriptor).\n * Effects marked `present: false` are left out. Returns `null` when the data is not a descriptor.\n */\nexport function parseLfx2(data: Uint8Array): ParsedLayerEffects | null {\n let d: Descriptor;\n try {\n d = parseVersionedDescriptor(data.subarray(4));\n } catch {\n return null;\n }\n const notes: Notes = new Set();\n const items: LayerEffect[] = [];\n for (const [single, multi, kind] of KINDS) {\n const many = multi ? list(d, multi) : [];\n const one = descObject(d, single);\n for (const x of many.length ? many : one ? [one] : []) {\n if (descBool(x, 'present') === false) continue;\n items.push(readOne(kind, x, notes));\n }\n }\n // `Scl ` is not a live multiplier: Photoshop stores the sizes in pixels already, and the scale\n // only records the document's resolution relative to 72 ppi (208.33 % at 150 ppi).\n const effects: LayerEffects = { enabled: descBool(d, 'masterFXSwitch') !== false, items };\n return { effects, notes: [...notes] };\n}\n\n// ---------------------------------------------------------------------------\n// Writing\n// ---------------------------------------------------------------------------\n\nfunction commonItems(e: { enabled: boolean; blend: BlendMode; opacity: number }): Array<[string, DescValue]> {\n return [\n ['enab', boolV(e.enabled)],\n ['present', boolV(true)],\n ['showInDialog', boolV(true)],\n ['Md ', enumV('BlnM', BLEND_ID.get(e.blend) ?? 'Nrml')],\n ['Opct', unit('#Prc', e.opacity * 100)],\n ];\n}\n\nfunction writeOne(e: LayerEffect): Descriptor {\n switch (e.kind) {\n case 'drop-shadow':\n case 'inner-shadow': {\n const inner = e.kind === 'inner-shadow';\n const items: Array<[string, DescValue]> = [\n ...commonItems(e),\n ['Clr ', obj(colorDesc(e.color))],\n ['uglg', boolV(e.useGlobalLight)],\n ['lagl', unit('#Ang', e.angle)],\n ['Dstn', unit('#Pxl', e.distance)],\n ['Ckmt', unit('#Pxl', (inner ? e.choke : e.spread) * 100)],\n ['blur', unit('#Pxl', e.size)],\n ['Nose', unit('#Prc', e.noise * 100)],\n ['AntA', boolV(e.antiAlias)],\n ['TrnS', contourDesc(e.contour)],\n ];\n if (e.kind === 'drop-shadow') items.push(['layerConceals', boolV(e.knocksOut)]);\n return descriptor(inner ? 'IrSh' : 'DrSh', items);\n }\n case 'outer-glow':\n case 'inner-glow': {\n const inner = e.kind === 'inner-glow';\n const items: Array<[string, DescValue]> = [...commonItems(e)];\n if (e.fill.type === 'gradient') items.push(...gradientItems(e.fill.gradient));\n else items.push(['Clr ', obj(colorDesc(e.fill.color))]);\n items.push(\n ['GlwT', enumV('BETE', e.technique === 'precise' ? 'PrBL' : 'SfBL')],\n ['Ckmt', unit('#Pxl', (inner ? e.choke : e.spread) * 100)],\n ['blur', unit('#Pxl', e.size)],\n ['Nose', unit('#Prc', e.noise * 100)],\n ['ShdN', unit('#Prc', e.jitter * 100)],\n ['AntA', boolV(e.antiAlias)],\n ['TrnS', contourDesc(e.contour)],\n ['Inpr', unit('#Prc', e.range * 100)]\n );\n if (e.kind === 'inner-glow') items.push(['glwS', enumV('IGSr', e.source === 'center' ? 'SrcC' : 'SrcE')]);\n return descriptor(inner ? 'IrGl' : 'OrGl', items);\n }\n case 'bevel-emboss': {\n const style = { 'outer-bevel': 'OtrB', 'inner-bevel': 'InrB', emboss: 'Embs', 'pillow-emboss': 'PlEb', 'stroke-emboss': 'strokeEmboss' }[e.style];\n const tech = { smooth: 'SfBL', 'chisel-hard': 'PrBL', 'chisel-soft': 'Slmt' }[e.technique];\n const items: Array<[string, DescValue]> = [\n ['enab', boolV(e.enabled)],\n ['present', boolV(true)],\n ['showInDialog', boolV(true)],\n ['hglM', enumV('BlnM', BLEND_ID.get(e.highlightBlend) ?? 'Scrn')],\n ['hglC', obj(colorDesc(e.highlightColor))],\n ['hglO', unit('#Prc', e.highlightOpacity * 100)],\n ['sdwM', enumV('BlnM', BLEND_ID.get(e.shadowBlend) ?? 'Mltp')],\n ['sdwC', obj(colorDesc(e.shadowColor))],\n ['sdwO', unit('#Prc', e.shadowOpacity * 100)],\n ['bvlT', enumV('bvlT', tech)],\n ['bvlS', enumV('BESl', style)],\n ['uglg', boolV(e.useGlobalLight)],\n ['lagl', unit('#Ang', e.angle)],\n ['Lald', unit('#Ang', e.altitude)],\n ['srgR', unit('#Prc', e.depth * 100)],\n ['blur', unit('#Pxl', e.size)],\n ['bvlD', enumV('BESs', e.direction === 'up' ? 'In ' : 'Out ')],\n ['TrnS', contourDesc(e.glossContour)],\n ['Sftn', unit('#Pxl', e.soften)],\n ];\n if (e.contourElement) {\n items.push(['useShape', boolV(true)], ['MpgS', contourDesc(e.contourElement.contour)], ['AntA', boolV(e.contourElement.antiAlias)], ['Inpr', unit('#Prc', e.contourElement.range * 100)]);\n } else items.push(['useShape', boolV(false)]);\n if (e.texture) {\n const t = e.texture;\n items.push(\n ['useTexture', boolV(true)],\n ['InvT', boolV(t.invert)],\n ['Scl ', unit('#Prc', t.scale * 100)],\n ['textureDepth', unit('#Prc', t.depth * 100)],\n ['Ptrn', patternRefDesc(t.patternId, t.name)],\n ...placementItems(0, t.link ?? true, t.phase ?? { x: 0, y: 0 })\n );\n } else items.push(['useTexture', boolV(false)]);\n return descriptor('ebbl', items);\n }\n case 'satin':\n return descriptor('ChFX', [\n ...commonItems(e),\n ['Clr ', obj(colorDesc(e.color))],\n ['AntA', boolV(e.antiAlias)],\n ['Invr', boolV(e.invert)],\n ['lagl', unit('#Ang', e.angle)],\n ['Dstn', unit('#Pxl', e.distance)],\n ['blur', unit('#Pxl', e.size)],\n ['MpgS', contourDesc(e.contour)],\n ]);\n case 'color-overlay':\n return descriptor('SoFi', [...commonItems(e), ['Clr ', obj(colorDesc(e.color))]]);\n case 'gradient-overlay':\n return descriptor('GrFl', [...commonItems(e), ...gradientItems(e.gradient), ['Dthr', boolV(e.dither)]]);\n case 'pattern-overlay':\n return descriptor('patternFill', [\n ...commonItems(e),\n ['Ptrn', patternRefDesc(e.patternId, e.name)],\n ['Scl ', unit('#Prc', e.scale * 100)],\n ...placementItems(e.angle ?? 0, e.link ?? true, e.phase ?? { x: 0, y: 0 }),\n ]);\n case 'stroke': {\n const items: Array<[string, DescValue]> = [\n ...commonItems(e),\n ['Styl', enumV('FStl', e.position === 'inside' ? 'InsF' : e.position === 'center' ? 'CtrF' : 'OutF')],\n ['Sz ', unit('#Pxl', e.size)],\n ];\n const f = e.fill;\n if (f.type === 'color') items.push(['PntT', enumV('FrFl', 'SClr')], ['Clr ', obj(colorDesc(f.color))]);\n else if (f.type === 'gradient') items.push(['PntT', enumV('FrFl', 'GrFl')], ...gradientItems(f.gradient));\n else items.push(['PntT', enumV('FrFl', 'Ptrn')], ['Ptrn', patternRefDesc(f.patternId, f.name)], ['Scl ', unit('#Prc', f.scale * 100)]);\n return descriptor('FrFX', items);\n }\n }\n}\n\n/**\n * Writes effects as `lfx2` block data, Scale Effects baked into the sizes (Photoshop's `Scl ` is\n * written as 100 %). Several instances of one kind go into its multi key\n * (`dropShadowMulti`, `frameFXMulti` …); kinds without one keep only the first instance.\n * Returns the data and how many instances had to be left out.\n */\nexport function writeLfx2(effects: LayerEffects): { data: Uint8Array; dropped: number } {\n const items: Array<[string, DescValue]> = [\n ['Scl ', unit('#Prc', 100)],\n ['masterFXSwitch', boolV(effects.enabled)],\n ];\n // Scale Effects is baked into the sizes: Photoshop reads them as pixels.\n const scale = effectsScale(effects);\n let dropped = 0;\n for (const [single, multi, kind] of KINDS) {\n const of = effects.items.filter((e) => e.kind === kind).map((e) => writeOne(scaleLayerEffect(e, scale)));\n if (!of.length) continue;\n if (of.length === 1) items.push([single, obj(of[0])]);\n else if (multi) items.push([multi, { type: 'VlLs', items: of.map(obj) }]);\n else {\n items.push([single, obj(of[0])]);\n dropped += of.length - 1;\n }\n }\n const body = versionedDescriptorBytes(descriptor('null', items));\n const w = new PsdWriter();\n w.u32(0);\n w.bytes(body);\n // Layer blocks keep their pad byte inside the length, as Photoshop writes them.\n if (w.length % 2) w.u8(0);\n return { data: w.result(), dropped };\n}\n\n// ---------------------------------------------------------------------------\n// Legacy effects (`lrFX`, Photoshop 5)\n// ---------------------------------------------------------------------------\n\n/** Parses legacy `lrFX` effects: drop / inner shadows, outer / inner glows and solid fill. Bevels are skipped. */\nexport function parseLrfx(data: Uint8Array): ParsedLayerEffects | null {\n try {\n const r = new PsdReader(data);\n r.u16();\n const count = r.u16();\n const items: LayerEffect[] = [];\n const notes: Notes = new Set();\n const fixed = (v: number) => (v | 0) / 65536;\n for (let i = 0; i < count; i++) {\n if (new TextDecoder().decode(r.bytes(4)) !== '8BIM') return null;\n const key = new TextDecoder().decode(r.bytes(4));\n const size = r.u32();\n const b = new PsdReader(r.bytes(size));\n const colorAt = (rd: PsdReader) => {\n rd.u16();\n const c = [rd.u16(), rd.u16(), rd.u16(), rd.u16()];\n return effectColorHex([c[0] >> 8, c[1] >> 8, c[2] >> 8].map((v) => v / 255));\n };\n const blendAt = (rd: PsdReader) => {\n rd.skip(4);\n const k = new TextDecoder().decode(rd.bytes(4));\n const m = PSD_BLEND_KEYS[k];\n return (m && m !== 'pass-through' ? m : 'normal') as BlendMode;\n };\n if (key === 'dsdw' || key === 'isdw') {\n b.u32();\n const blur = fixed(b.u32());\n b.u32();\n const angle = fixed(b.u32());\n const distance = fixed(b.u32());\n const color = colorAt(b);\n const blend = blendAt(b);\n const enabled = b.u8() !== 0;\n const useGlobalLight = b.u8() !== 0;\n const opacity = b.u8() / 255;\n const base = { id: newEffectId(), enabled, blend, opacity, color, angle, useGlobalLight, distance: clamp(distance, 0, 30000), size: clamp(blur, 0, 250), antiAlias: false, noise: 0 };\n items.push(key === 'dsdw' ? { ...base, kind: 'drop-shadow', spread: 0, knocksOut: true } : { ...base, kind: 'inner-shadow', choke: 0 });\n } else if (key === 'oglw' || key === 'iglw') {\n b.u32();\n const blur = fixed(b.u32());\n b.u32();\n const color = colorAt(b);\n const blend = blendAt(b);\n const enabled = b.u8() !== 0;\n const opacity = b.u8() / 255;\n const base = { id: newEffectId(), enabled, blend, opacity, fill: { type: 'color' as const, color }, technique: 'softer' as const, size: clamp(blur, 0, 250), antiAlias: false, range: 0.5, jitter: 0, noise: 0 };\n items.push(key === 'oglw' ? { ...base, kind: 'outer-glow', spread: 0 } : { ...base, kind: 'inner-glow', choke: 0, source: 'edge' });\n } else if (key === 'sofi') {\n b.u32();\n const blend = blendAt(b);\n const color = colorAt(b);\n const opacity = b.u8() / 255;\n const enabled = b.u8() !== 0;\n items.push({ id: newEffectId(), kind: 'color-overlay', enabled, blend, opacity, color });\n } else if (key === 'bevl') notes.add('legacy bevels are not read');\n }\n return { effects: { enabled: true, items }, notes: [...notes] };\n } catch {\n return null;\n }\n}\n","// Portions ported from photocraft-main/crates/psd/src/patterns.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Patterns: the global `Patt` / `Pat2` / `Pat3` tagged blocks (8 / 16 / 32-bit documents), which\n * pattern overlays, pattern strokes and bevel textures refer to by id.\n *\n * ```text\n * pattern := version u32 (1), image mode u32, height u16, width u16,\n * name (Unicode string), unique id (Pascal string, unpadded),\n * [indexed: 768-byte palette], virtual memory array list\n * VMA list := version u32 (3), length u32, rect (top, left, bottom, right i32),\n * channel count u32, then count + 2 arrays (user mask, sheet mask)\n * array := written u32 (0 = absent), length u32 (0 = absent), depth u32, rect,\n * depth u16, compression u8 (0 raw, 1 PackBits), data\n * ```\n *\n * Each pattern in a block is prefixed by its u32 length and padded to 4 bytes. Photoshop writes\n * the channel count as 24 and marks only the real channels as written; the first written arrays\n * are the colour channels and the next one is transparency.\n */\n\nimport type { EffectPattern } from '../../raster/effects/patterns';\nimport { PsdError, PsdReader, PsdWriter } from './binary';\nimport { decodePlanes, encodeRle } from './compression';\nimport { MODE_CMYK, MODE_INDEXED, MODE_LAB, MODE_RGB, toRgba8 } from './pixels';\n\nconst MAX_EDGE = 30000;\n/** Pattern block keys, by document depth. */\nexport const PATTERN_BLOCK_KEYS = ['Patt', 'Pat2', 'Pat3'];\n\n/** One pattern tile as stored: planar samples. */\nexport interface PsdPattern {\n mode: number;\n width: number;\n height: number;\n name: string;\n id: string;\n palette?: Uint8Array;\n depth: number;\n channels: Uint8Array[];\n alpha?: Uint8Array;\n}\n\n/** Colour channels of a PSD colour mode. */\nfunction modeChannels(mode: number): number {\n return mode === MODE_RGB || mode === MODE_LAB ? 3 : mode === MODE_CMYK ? 4 : 1;\n}\n\nconst rowBytes = (width: number, depth: number) => (depth === 1 ? Math.ceil(width / 8) : width * (depth / 8));\n\nfunction readPattern(r: PsdReader): PsdPattern {\n const version = r.u32();\n if (version !== 1) throw new PsdError(`Pattern version ${version}, expected 1.`);\n const mode = r.u32();\n r.u16();\n r.u16();\n const name = r.unicode();\n const idLength = r.u8();\n const id = new TextDecoder().decode(r.bytes(idLength));\n let palette: Uint8Array | undefined;\n if (mode === MODE_INDEXED) {\n palette = r.bytes(768).slice();\n // Some writers follow the palette with 4 more bytes.\n const next = r.peek(4);\n if (next.length === 4 && !(next[0] === 0 && next[1] === 0 && next[2] === 0 && next[3] === 3)) r.skip(4);\n }\n const vmaVersion = r.u32();\n if (vmaVersion !== 3) throw new PsdError(`Pattern virtual memory array list version ${vmaVersion}, expected 3.`);\n const length = r.u32();\n const body = new PsdReader(r.bytes(length));\n const [top, left, bottom, right] = [body.i32(), body.i32(), body.i32(), body.i32()];\n const width = right - left;\n const height = bottom - top;\n if (!(width > 0 && height > 0 && width <= MAX_EDGE && height <= MAX_EDGE)) throw new PsdError(`Pattern \"${name}\" has an invalid size ${width} × ${height}.`);\n const count = body.u32();\n if (count > 64) throw new PsdError(`Pattern \"${name}\" has ${count} channels.`);\n const planes: Uint8Array[] = [];\n let depth = 8;\n for (let i = 0; i < count + 2 && body.remaining > 0; i++) {\n if (body.u32() === 0) continue;\n const arrayLength = body.u32();\n if (arrayLength === 0) continue;\n if (arrayLength < 23) throw new PsdError(`Pattern \"${name}\" has a short channel.`);\n const d32 = body.u32();\n const [ct, cl, cb, cr] = [body.i32(), body.i32(), body.i32(), body.i32()];\n const d16 = body.u16();\n const compression = body.u8();\n const data = body.bytes(arrayLength - 23);\n const d = [1, 8, 16, 32].includes(d16) ? d16 : d32;\n if (![1, 8, 16, 32].includes(d)) throw new PsdError(`Pattern \"${name}\" has depth ${d}.`);\n if (ct < top || cl < left || cb > bottom || cr > right || cb < ct || cr < cl) throw new PsdError(`Pattern \"${name}\" has a channel outside its rectangle.`);\n if (compression > 1) throw new PsdError(`Pattern \"${name}\" uses compression ${compression}.`);\n depth = d;\n const cw = cr - cl;\n const ch = cb - ct;\n const plane = decodePlanes(compression, data, { planes: 1, width: cw, height: ch, depth: d, psb: false });\n if (cw === width && ch === height) {\n planes.push(plane);\n continue;\n }\n // A smaller channel rectangle is padding: place it in a full plane.\n if (d === 1) throw new PsdError(`Pattern \"${name}\" has a cropped bitmap channel.`);\n const bps = d / 8;\n const out = new Uint8Array(rowBytes(width, d) * height);\n for (let y = 0; y < ch; y++) {\n out.set(plane.subarray(y * cw * bps, (y + 1) * cw * bps), ((ct - top + y) * width + (cl - left)) * bps);\n }\n planes.push(out);\n }\n const nc = modeChannels(mode);\n if (planes.length < nc) throw new PsdError(`Pattern \"${name}\" has fewer channels than its colour mode.`);\n return { mode, width, height, name, id, palette, depth, channels: planes.slice(0, nc), alpha: planes[nc] };\n}\n\n/** The patterns of a `Patt` / `Pat2` / `Pat3` block. Throws `PsdError` on malformed data. */\nexport function parsePatternBlock(data: Uint8Array): PsdPattern[] {\n const r = new PsdReader(data);\n const out: PsdPattern[] = [];\n while (r.remaining >= 4) {\n const length = r.u32();\n if (length === 0) break;\n out.push(readPattern(new PsdReader(r.bytes(length))));\n r.skip(Math.min((4 - (length % 4)) % 4, r.remaining));\n }\n return out;\n}\n\n/** A stored pattern as straight 8-bit sRGB RGBA, for rendering. */\nexport function psdPatternToEffectPattern(p: PsdPattern): EffectPattern {\n const data = toRgba8({ width: p.width, height: p.height, depth: p.depth, mode: p.mode, color: p.channels, alpha: p.alpha, palette: p.palette });\n return { id: p.id, name: p.name, width: p.width, height: p.height, data };\n}\n\n/** Writes 8-bit RGB(A) patterns as `Patt` block data (PackBits channels, transparency in the user mask slot). */\nexport function writePatternBlock(patterns: readonly EffectPattern[]): Uint8Array {\n const out = new PsdWriter();\n for (const p of patterns) {\n if (!(p.width > 0 && p.height > 0) || p.width > 32767 || p.height > 32767) throw new PsdError(`Pattern \"${p.name}\" is ${p.width} × ${p.height}; patterns are 1 to 32767 px.`);\n const one = new PsdWriter();\n one.u32(1);\n one.u32(MODE_RGB);\n one.u16(p.height);\n one.u16(p.width);\n one.unicode(p.name, true);\n const id = new TextEncoder().encode(p.id).subarray(0, 255);\n one.u8(id.length);\n one.bytes(id);\n const n = p.width * p.height;\n const plane = (c: number) => {\n const v = new Uint8Array(n);\n for (let i = 0; i < n; i++) v[i] = p.data[i * 4 + c];\n return v;\n };\n const alpha = plane(3);\n const opaque = alpha.every((v) => v === 255);\n const body = new PsdWriter();\n for (const v of [0, 0, p.height, p.width]) body.i32(v);\n body.u32(24);\n const array = (v: Uint8Array) => {\n const data = encodeRle(v, { planes: 1, width: p.width, height: p.height, depth: 8, psb: false });\n body.u32(1);\n body.u32(23 + data.length);\n body.u32(8);\n for (const x of [0, 0, p.height, p.width]) body.i32(x);\n body.u16(8);\n body.u8(1);\n body.bytes(data);\n };\n for (let c = 0; c < 3; c++) array(plane(c));\n for (let c = 3; c < 24; c++) body.u32(0);\n if (opaque) body.u32(0);\n else array(alpha);\n body.u32(0);\n const b = body.result();\n one.u32(3);\n one.u32(b.length);\n one.bytes(b);\n const bytes = one.result();\n out.u32(bytes.length);\n out.bytes(bytes);\n for (let k = 0; k < (4 - (bytes.length % 4)) % 4; k++) out.u8(0);\n }\n return out.result();\n}\n","// Portions ported from photocraft/crates/io/src/psd_import.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * PSD / PSB → our layer tree. Everything the model holds maps directly; everything else comes in\n * as close as it can (type, smart-object, fill and shape layers as their stored pixels) with a\n * warning naming the layers.\n */\n\nimport type { BlendMode } from '../blend';\nimport { emptySurface, surfaceFromPixels } from '../../raster/surface';\nimport type { GroupLayer, ImageLayer, LayerBlendMode, LayerLocks, PixelLayer, RasterMask } from '../layers/model';\nimport { ADJUSTMENT_KEYS, adjustmentLabel, parseAdjustmentBlock } from './adjust';\nimport { PsdError } from './binary';\nimport {\n CHANNEL_ALPHA,\n CHANNEL_REAL_USER_MASK,\n CHANNEL_USER_MASK,\n COLOR_MODES,\n LAYER_FLAG_HIDDEN,\n MASK_FLAG_DISABLED,\n MASK_FLAG_UNLINKED,\n SECTION_CLOSED,\n type PsdFile,\n type PsdLayerRecord,\n type PsdRect,\n type PsdTreeNode,\n decodeLayerChannel,\n decodeMergedImage,\n layerBlock,\n layerName,\n layerTree,\n readPsd,\n rectSize,\n resolution,\n resource,\n rowBytes,\n sectionDivider,\n u32Block,\n u8Block,\n} from './file';\nimport { MODE_INDEXED, colorChannelCount, toMask8, toRgba8, unmatteWhite } from './pixels';\nimport { parseLfx2, parseLrfx } from './effects';\nimport { PATTERN_BLOCK_KEYS, parsePatternBlock, psdPatternToEffectPattern } from './patterns';\nimport { builtinPattern, type EffectPattern } from '../../raster/effects/patterns';\nimport type { GlobalLight, LayerEffect, LayerEffects } from '../../raster/effects/model';\n\n/** PSD blend keys → our blend modes (`diss` is the one we lack). */\nexport const PSD_BLEND_KEYS: Record<string, LayerBlendMode> = {\n pass: 'pass-through',\n norm: 'normal',\n dark: 'darken',\n 'mul ': 'multiply',\n idiv: 'color-burn',\n lbrn: 'linear-burn',\n dkCl: 'darker-color',\n lite: 'lighten',\n scrn: 'screen',\n 'div ': 'color-dodge',\n lddg: 'linear-dodge',\n lgCl: 'lighter-color',\n over: 'overlay',\n sLit: 'soft-light',\n hLit: 'hard-light',\n vLit: 'vivid-light',\n lLit: 'linear-light',\n pLit: 'pin-light',\n hMix: 'hard-mix',\n diff: 'difference',\n smud: 'exclusion',\n fsub: 'subtract',\n fdiv: 'divide',\n 'hue ': 'hue',\n 'sat ': 'saturation',\n colr: 'color',\n 'lum ': 'luminosity',\n};\n\n/** Our blend modes → PSD keys. `pigment` has none (export writes `norm`, see `toPsd`). */\nexport const BLEND_TO_PSD: Partial<Record<LayerBlendMode, string>> = Object.fromEntries(\n Object.entries(PSD_BLEND_KEYS).map(([k, v]) => [v, k])\n) as Partial<Record<LayerBlendMode, string>>;\n\nconst FILL_KEYS = ['SoCo', 'GdFl', 'PtFl'];\nconst TYPE_KEYS = ['TySh', 'tySh'];\nconst SMART_KEYS = ['SoLd', 'SoLE', 'PlLd'];\nconst VECTOR_KEYS = ['vmsk', 'vsms'];\n\n/** What happened to one PSD layer on import. */\nexport interface PsdLayerSummary {\n name: string;\n /** Our layer type, or `null` when the layer was dropped. */\n type: 'pixel' | 'adjustment' | 'group' | null;\n /** `mapped`: held exactly; `approximated`: imported with losses; `dropped`: left out. */\n status: 'mapped' | 'approximated' | 'dropped';\n /** What Photoshop calls it (`pixel`, `type`, `smart-object`, `fill`, `shape`, `Levels` …). */\n psdKind: string;\n /** Why it is not `mapped`. */\n notes: string[];\n /** Our layer id (when imported). */\n id?: string;\n depth: number;\n}\n\nexport interface PsdImportResult {\n width: number;\n height: number;\n /** Bottom to top, as in our model. */\n layers: ImageLayer[];\n /** One sentence per problem, naming the affected layers. */\n warnings: string[];\n /** Every PSD layer, bottom first, a group before its children. */\n summary: PsdLayerSummary[];\n /** Pixels per inch, when the file says. */\n resolution?: number;\n /** The patterns embedded in the file (`Patt` / `Pat2` / `Pat3`), which pattern effects use. */\n patterns: EffectPattern[];\n /** The global light (image resources 1037 and 1049), when the file has one. */\n globalLight?: GlobalLight;\n info: { psb: boolean; depth: number; colorMode: string; channels: number };\n}\n\nexport interface PsdImportOptions {\n /** Makes layer ids (defaults to `psd-<n>`). */\n newId?: (hint: string) => string;\n /** Skip decoding pixels (inspection only). */\n skipPixels?: boolean;\n}\n\n/** A PSD byte (0..255) as the whole percent Photoshop shows, 0..1. */\nconst percent = (v: number) => Math.round((v / 255) * 100) / 100;\n\nclass Warnings {\n private groups = new Map<string, string[]>();\n readonly extra: string[] = [];\n\n add(message: string, layer: string): void {\n const list = this.groups.get(message) ?? [];\n if (!list.includes(layer)) list.push(layer);\n this.groups.set(message, list);\n }\n\n list(): string[] {\n const out = [...this.extra];\n for (const [message, layers] of this.groups) {\n const names = layers.map((n) => `\"${n}\"`);\n const shown = names.length > 6 ? `${names.slice(0, 6).join(', ')} and ${names.length - 6} more` : names.join(', ');\n out.push(`${message} (${layers.length === 1 ? 'layer' : 'layers'} ${shown}).`);\n }\n return out;\n }\n}\n\nfunction psdKind(rec: PsdLayerRecord): string {\n if (rec.blocks.some((b) => TYPE_KEYS.includes(b.key))) return 'type';\n if (rec.blocks.some((b) => SMART_KEYS.includes(b.key))) return 'smart-object';\n const fill = rec.blocks.some((b) => FILL_KEYS.includes(b.key));\n const vector = rec.blocks.some((b) => VECTOR_KEYS.includes(b.key) || b.key === 'vstk');\n if (fill && vector) return 'shape';\n if (fill) return 'fill';\n const adj = rec.blocks.find((b) => ADJUSTMENT_KEYS.includes(b.key));\n if (adj) return adjustmentLabel(adj.key);\n return 'pixel';\n}\n\nfunction locksOf(rec: PsdLayerRecord): LayerLocks | undefined {\n const v = u32Block(rec, 'lspf');\n if (!v) return undefined;\n const locks: LayerLocks = {};\n if (v & 0x80000000) locks.all = true;\n if (v & 1) locks.transparency = true;\n if (v & 2) locks.pixels = true;\n if (v & 4) locks.position = true;\n return Object.keys(locks).length ? locks : undefined;\n}\n\ninterface Ctx {\n file: PsdFile;\n warn: Warnings;\n newId: (hint: string) => string;\n skipPixels: boolean;\n /** Every PSD layer, bottom first, a group before its children. */\n summary: PsdLayerSummary[];\n colorCount: number;\n palette?: Uint8Array;\n patterns: Map<string, EffectPattern>;\n}\n\nfunction decodeOr(ctx: Ctx, rec: PsdLayerRecord, id: number, name: string): Uint8Array | undefined {\n try {\n return decodeLayerChannel(ctx.file, rec, id);\n } catch (e) {\n ctx.warn.add(`A channel could not be decoded (${e instanceof Error ? e.message : String(e)}) and the layer is treated as empty`, name);\n return undefined;\n }\n}\n\n/** A rectangle's size, or 0 × 0 (with a warning) for a malformed one. */\nfunction safeSize(ctx: Ctx, rect: PsdRect, name: string): { width: number; height: number } {\n try {\n return rectSize(rect);\n } catch (e) {\n ctx.warn.add(`A malformed rectangle (${e instanceof Error ? e.message : String(e)}) left the layer or its mask empty`, name);\n return { width: 0, height: 0 };\n }\n}\n\nfunction recordPixels(ctx: Ctx, rec: PsdLayerRecord, name: string): PixelLayer['surface'] {\n const { width, height } = safeSize(ctx, rec.rect, name);\n if (!width || !height || ctx.skipPixels) return emptySurface(4);\n const ids = [...Array.from({ length: ctx.colorCount }, (_, i) => i), CHANNEL_ALPHA];\n const planes = ids.map((id) => (rec.channels.some((c) => c.id === id) ? decodeOr(ctx, rec, id, name) ?? null : undefined));\n // A channel that is in the file but unreadable leaves the layer empty (as the warning says).\n if (planes.some((p) => p === null)) return emptySurface(4);\n if (planes.every((p) => p === undefined)) return emptySurface(4);\n const data = toRgba8({\n width,\n height,\n depth: ctx.file.header.depth,\n mode: ctx.file.header.colorMode,\n color: planes.slice(0, ctx.colorCount) as Array<Uint8Array | undefined>,\n alpha: planes[ctx.colorCount] as Uint8Array | undefined,\n palette: ctx.palette,\n });\n return surfaceFromPixels({ width, height, data }, 4, rec.rect.left, rec.rect.top);\n}\n\nfunction recordMask(ctx: Ctx, rec: PsdLayerRecord, name: string): RasterMask | undefined {\n const m = rec.mask;\n if (!m) return undefined;\n const useReal = rec.channels.some((c) => c.id === CHANNEL_REAL_USER_MASK) && m.real;\n const id = useReal ? CHANNEL_REAL_USER_MASK : CHANNEL_USER_MASK;\n if (!rec.channels.some((c) => c.id === id)) return undefined;\n const rect = useReal ? m.real!.rect : m.rect;\n const def: 0 | 255 = (useReal ? m.real!.background : m.defaultColor) >= 128 ? 255 : 0;\n const flags = useReal ? m.real!.flags : m.flags;\n let surface = emptySurface(1, def);\n const { width, height } = safeSize(ctx, rect, name);\n if (width && height && !ctx.skipPixels) {\n const plane = decodeOr(ctx, rec, id, name);\n if (plane) surface = surfaceFromPixels({ width, height, data: toMask8(plane, width, height, ctx.file.header.depth) }, 1, rect.left, rect.top, def);\n }\n const mask: RasterMask = { surface, defaultValue: def };\n if (m.userDensity !== undefined && m.userDensity < 255) mask.density = percent(m.userDensity);\n if (m.userFeather !== undefined && m.userFeather > 0) mask.feather = Math.min(1000, m.userFeather);\n if (flags & MASK_FLAG_DISABLED) mask.enabled = false;\n if (flags & MASK_FLAG_UNLINKED) mask.linked = false;\n return mask;\n}\n\nfunction baseProps(ctx: Ctx, rec: PsdLayerRecord, name: string, notes: string[]) {\n let blend = PSD_BLEND_KEYS[rec.blendKey];\n if (rec.blendKey === 'diss') {\n ctx.warn.add('Dissolve is not supported and became Normal', name);\n notes.push('Dissolve became Normal');\n } else if (!blend) {\n ctx.warn.add(`An unknown blend mode became Normal`, name);\n notes.push(`unknown blend mode \"${rec.blendKey}\"`);\n }\n if (!blend || blend === 'pass-through') blend = 'normal';\n const props = {\n name,\n visible: (rec.flags & LAYER_FLAG_HIDDEN) === 0,\n opacity: percent(rec.opacity),\n blend: blend as BlendMode,\n } as { name: string; visible: boolean; opacity: number; blend: BlendMode; clipped?: boolean; locks?: LayerLocks };\n if (rec.clipping) props.clipped = true;\n const locks = locksOf(rec);\n if (locks) props.locks = locks;\n if (rec.blocks.some((b) => b.key === 'artb' || b.key === 'artd' || b.key === 'abdd')) {\n ctx.warn.add('Artboards are not supported; they came in as plain groups, without their background colour and clipping', name);\n notes.push('artboard imported as a group');\n }\n if (rec.blocks.some((b) => VECTOR_KEYS.includes(b.key)) && psdKind(rec) === 'pixel') {\n ctx.warn.add('Vector masks are not supported and were ignored', name);\n notes.push('vector mask ignored');\n }\n return props;\n}\n\n/** Pattern ids an effect paints with. */\nfunction patternIds(e: LayerEffect): string[] {\n if (e.kind === 'pattern-overlay') return [e.patternId];\n if (e.kind === 'stroke' && e.fill.type === 'pattern') return [e.fill.patternId];\n if (e.kind === 'bevel-emboss' && e.texture) return [e.texture.patternId];\n return [];\n}\n\n/**\n * A layer's effects: `lmfx` (several instances per kind) over `lfx2` (`lfxs` on groups), else\n * the legacy `lrFX`. The block read is kept so export writes it back while the effects are\n * unchanged; warnings name only what is not drawn exactly.\n */\nfunction recordEffects(ctx: Ctx, rec: PsdLayerRecord, name: string, notes: string[], group: boolean): LayerEffects | undefined {\n const key = ['lmfx', 'lfx2', 'lfxs'].find((k) => layerBlock(rec, k));\n if (key) {\n const data = layerBlock(rec, key)!;\n const parsed = parseLfx2(data);\n if (!parsed) {\n ctx.warn.add('Layer effects could not be read; they are kept for export but not drawn', name);\n notes.push('layer effects could not be read');\n return { enabled: true, items: [], psd: { key, data: data.slice() } };\n }\n for (const n of parsed.notes) {\n ctx.warn.add(`Layer effects were approximated: ${n}`, name);\n notes.push(`effects: ${n}`);\n }\n for (const e of parsed.effects.items) {\n for (const id of patternIds(e)) {\n if (ctx.patterns.has(id) || builtinPattern(id)) continue;\n ctx.warn.add('A pattern effect uses a pattern the file does not contain, and paints nothing', name);\n notes.push(`pattern \"${id}\" is not in the file`);\n }\n }\n return { ...parsed.effects, psd: { key, data: data.slice() } };\n }\n const legacy = layerBlock(rec, 'lrFX');\n if (legacy && !group) {\n // Photoshop ignores legacy effects on groups.\n const parsed = parseLrfx(legacy);\n if (!parsed) return undefined;\n for (const n of parsed.notes) {\n ctx.warn.add(`Layer effects were approximated: ${n}`, name);\n notes.push(`effects: ${n}`);\n }\n return parsed.effects.items.length ? parsed.effects : undefined;\n }\n return undefined;\n}\n\nfunction importNode(ctx: Ctx, node: PsdTreeNode, depth: number): ImageLayer | null {\n const rec = ctx.file.layers[node.index];\n const name = layerName(rec);\n const notes: string[] = [];\n const kind = psdKind(rec);\n const entry: PsdLayerSummary = { name, type: null, status: 'mapped', psdKind: node.children ? 'group' : kind, notes, depth };\n ctx.summary.push(entry);\n\n if (node.children) {\n const props = baseProps(ctx, rec, name, notes);\n const divider = sectionDivider(rec);\n const key = divider?.blendKey ?? rec.blendKey;\n const groupBlend: LayerBlendMode = key === 'pass' ? 'pass-through' : (PSD_BLEND_KEYS[key] ?? 'normal');\n const children: ImageLayer[] = [];\n for (const child of node.children) {\n const layer = importNode(ctx, child, depth + 1);\n if (layer) children.push(layer);\n }\n const group: GroupLayer = { type: 'group', id: ctx.newId(name), ...props, blend: groupBlend, children };\n const effects = recordEffects(ctx, rec, name, notes, true);\n if (effects) group.effects = effects;\n if (divider?.kind === SECTION_CLOSED) group.collapsed = true;\n const mask = recordMask(ctx, rec, name);\n if (mask) group.mask = mask;\n entry.type = 'group';\n entry.id = group.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return group;\n }\n\n const adjBlock = rec.blocks.find((b) => ADJUSTMENT_KEYS.includes(b.key));\n if (adjBlock) {\n const parsed = parseAdjustmentBlock(adjBlock.key, adjBlock.data);\n if (!parsed) {\n const label = adjustmentLabel(adjBlock.key);\n ctx.warn.add(`${label} adjustment layers are not supported and were left out`, name);\n notes.push(`${label} is not supported`);\n entry.status = 'dropped';\n return null;\n }\n const props = baseProps(ctx, rec, name, notes);\n if (parsed.approximated) {\n ctx.warn.add(`A ${adjustmentLabel(adjBlock.key)} layer was approximated: ${parsed.approximated}`, name);\n notes.push(parsed.approximated);\n }\n const layer = { type: 'adjustment', id: ctx.newId(name), ...props, kind: parsed.kind, params: parsed.params } as ImageLayer;\n const mask = recordMask(ctx, rec, name);\n if (mask) layer.mask = mask;\n entry.type = 'adjustment';\n entry.id = layer.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return layer;\n }\n\n const props = baseProps(ctx, rec, name, notes);\n if (kind !== 'pixel') {\n const what = kind === 'smart-object' ? 'Smart objects' : kind === 'type' ? 'Type layers' : kind === 'shape' ? 'Shape layers' : 'Fill layers';\n ctx.warn.add(`${what} came in as pixel layers (their stored pixels)`, name);\n notes.push(`${kind} layer imported as pixels`);\n }\n const layer: PixelLayer = { type: 'pixel', id: ctx.newId(name), ...props, surface: recordPixels(ctx, rec, name), offset: { x: 0, y: 0 } };\n const fill = u8Block(rec, 'iOpa');\n if (fill !== undefined && fill < 255) layer.fillOpacity = percent(fill);\n const effects = recordEffects(ctx, rec, name, notes, false);\n if (effects) layer.effects = effects;\n const mask = recordMask(ctx, rec, name);\n if (mask) layer.mask = mask;\n entry.type = 'pixel';\n entry.id = layer.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return layer;\n}\n\nfunction mergedLayer(ctx: Ctx): PixelLayer | null {\n const { header } = ctx.file;\n const { width, height } = header;\n let planes: Uint8Array;\n try {\n planes = decodeMergedImage(ctx.file);\n } catch (e) {\n ctx.warn.extra.push(`The merged image could not be decoded (${e instanceof Error ? e.message : String(e)}).`);\n return null;\n }\n const size = rowBytes(width, header.depth) * height;\n const plane = (i: number) => (i < header.channels ? planes.subarray(i * size, (i + 1) * size) : undefined);\n const cc = ctx.colorCount;\n const hasAlpha = header.channels > cc && (ctx.file.layers.length === 0 || ctx.file.mergedAlpha);\n const data = toRgba8({\n width,\n height,\n depth: header.depth,\n mode: header.colorMode,\n color: Array.from({ length: cc }, (_, i) => plane(i)),\n alpha: hasAlpha ? plane(cc) : undefined,\n palette: ctx.palette,\n });\n if (hasAlpha) unmatteWhite(data);\n return {\n type: 'pixel',\n id: ctx.newId('Background'),\n name: 'Background',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n}\n\n/** Maps a parsed PSD to our layers. */\nexport function psdToLayers(file: PsdFile, options: PsdImportOptions = {}): PsdImportResult {\n const { header } = file;\n const warn = new Warnings();\n let n = 0;\n const ctx: Ctx = {\n file,\n warn,\n newId: options.newId ?? (() => `psd-${++n}`),\n skipPixels: options.skipPixels ?? false,\n summary: [],\n colorCount: colorChannelCount(header.colorMode, header.channels),\n palette: header.colorMode === MODE_INDEXED ? file.colorModeData : undefined,\n patterns: new Map(),\n };\n for (const block of file.globalBlocks) {\n if (!PATTERN_BLOCK_KEYS.includes(block.key)) continue;\n try {\n for (const p of parsePatternBlock(block.data)) ctx.patterns.set(p.id, psdPatternToEffectPattern(p));\n } catch (e) {\n warn.extra.push(`The file's patterns could not be read (${e instanceof Error ? e.message : String(e)}); pattern effects paint nothing.`);\n }\n }\n const mode = COLOR_MODES[header.colorMode] ?? `mode ${header.colorMode}`;\n if (mode !== 'rgb' || header.depth !== 8) {\n warn.extra.push(`The file is ${header.depth}-bit ${mode}; it was converted to 8-bit RGB.`);\n }\n if (resource(file, 1039)) warn.extra.push('The embedded ICC profile is ignored; colours are read as sRGB.');\n\n let layers: ImageLayer[] = [];\n if (file.layers.length) {\n const tree = layerTree(file);\n const built: ImageLayer[] = [];\n for (const node of tree) {\n const layer = importNode(ctx, node, 0);\n if (layer) built.push(layer);\n }\n layers = built;\n if (!layers.length && !ctx.skipPixels) {\n // Every layer was left out: the merged image is still the best picture of the file.\n const bg = mergedLayer(ctx);\n if (bg) {\n layers = [bg];\n warn.extra.push('No layer could be imported, so the merged image was opened as the Background.');\n }\n }\n } else if (!ctx.skipPixels) {\n const bg = mergedLayer(ctx);\n if (bg) layers = [bg];\n }\n const i32 = (b: Uint8Array | undefined) => (b && b.length >= 4 ? new DataView(b.buffer, b.byteOffset, 4).getInt32(0) : undefined);\n const angle = i32(resource(file, 1037));\n const altitude = i32(resource(file, 1049));\n return {\n width: header.width,\n height: header.height,\n layers,\n warnings: warn.list(),\n summary: ctx.summary,\n resolution: resolution(file),\n patterns: [...ctx.patterns.values()],\n ...(angle !== undefined || altitude !== undefined\n ? { globalLight: { angle: Math.max(-360, Math.min(360, angle ?? 120)), altitude: Math.max(0, Math.min(90, altitude ?? 30)) } }\n : {}),\n info: { psb: header.psb, depth: header.depth, colorMode: mode, channels: header.channels },\n };\n}\n\n/** Parses and maps PSD / PSB bytes. Throws `PsdError` for files that cannot be read at all. */\nexport function importPsd(bytes: Uint8Array, options: PsdImportOptions = {}): PsdImportResult {\n if (bytes.length < 26) throw new PsdError('Not a PSD file: it is shorter than a PSD header.');\n return psdToLayers(readPsd(bytes), options);\n}\n","// Portions ported from photocraft/crates/io/src/psd_export.rs and photocraft/crates/psd/src/builder.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Our layer tree → an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, RLE-compressed, plus the merged image the caller rendered.\n *\n * The stack must be *prepared*: annotation layers rasterized, layer filters and parametric masks\n * baked, and no layer reading the document source (see `ImageEditorController.toPsd`).\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport { contentBounds, readRegion } from '../../raster/surface';\nimport { isActiveFilter, type ImageLayer, type RasterMask } from '../layers/model';\nimport type { CropBox } from '../types';\nimport { PsdError, PsdWriter, encodeLegacyName } from './binary';\nimport { writeAdjustmentBlock } from './adjust';\nimport { encodeRle } from './compression';\nimport {\n CHANNEL_ALPHA,\n CHANNEL_USER_MASK,\n LAYER_FLAG_HIDDEN,\n LAYER_FLAG_TRANSPARENCY_PROTECTED,\n MASK_FLAG_DISABLED,\n MASK_FLAG_UNLINKED,\n PSD_MAX_DIMENSION,\n SECTION_CLOSED,\n SECTION_DIVIDER,\n SECTION_OPEN,\n type PsdChannel,\n type PsdLayerMask,\n type PsdLayerRecord,\n type PsdRect,\n type PsdResource,\n type PsdTaggedBlock,\n writePsd,\n} from './file';\nimport { BLEND_TO_PSD } from './import';\nimport { matteWhite, rgbaToPlanes } from './pixels';\nimport { parseLfx2, writeLfx2 } from './effects';\nimport { writePatternBlock } from './patterns';\nimport { effectsFingerprint, type GlobalLight, type LayerEffect } from '../../raster/effects/model';\nimport { builtinPattern, type EffectPattern } from '../../raster/effects/patterns';\n\nexport interface PsdExportInput {\n width: number;\n height: number;\n /** Prepared stack, bottom to top. */\n layers: readonly ImageLayer[];\n /** The composite, `width × height` RGBA, written as the merged image. */\n merged: ArrayLike<number>;\n /** Pixels per inch (default 72). */\n resolution?: number;\n /** The document's patterns; those the effects use are embedded (built-in ones too). */\n patterns?: readonly EffectPattern[];\n /** Written as image resources 1037 and 1049. */\n globalLight?: GlobalLight;\n}\n\nexport interface PsdExportResult {\n bytes: Uint8Array;\n psb: boolean;\n /** Layer records written (group folders and dividers included). */\n records: number;\n /** One sentence per loss, naming the layers. */\n warnings: string[];\n}\n\n/** Pattern ids an effect paints with. */\nfunction usedPatterns(e: LayerEffect): string[] {\n if (e.kind === 'pattern-overlay') return [e.patternId];\n if (e.kind === 'stroke' && e.fill.type === 'pattern') return [e.fill.patternId];\n if (e.kind === 'bevel-emboss' && e.texture) return [e.texture.patternId];\n return [];\n}\n\nconst padEven = (d: Uint8Array) => {\n if (d.length % 2 === 0) return d;\n const out = new Uint8Array(d.length + 1);\n out.set(d);\n return out;\n};\n\n/** Layer flag bits 3 and 4: \"bit 4 is useful\" and \"pixel data irrelevant\" (group records). */\nconst FLAG_PIXELS_IRRELEVANT = 8 | 16;\nconst EMPTY_RECT: PsdRect = { top: 0, left: 0, bottom: 0, right: 0 };\n\nfunction unicodeBlock(s: string): Uint8Array {\n const w = new PsdWriter();\n w.unicode(s);\n return w.result();\n}\n\nfunction u32Bytes(v: number): Uint8Array {\n const w = new PsdWriter();\n w.u32(v);\n return w.result();\n}\n\nfunction sectionBlock(kind: number, blendKey?: string): Uint8Array {\n const w = new PsdWriter();\n w.u32(kind);\n if (blendKey) {\n w.ascii('8BIM');\n w.ascii(blendKey);\n }\n return w.result();\n}\n\nconst toRect = (r: CropBox): PsdRect => ({ top: r.y, left: r.x, bottom: r.y + r.height, right: r.x + r.width });\n\nfunction rleChannel(id: number, plane: Uint8Array, width: number, height: number, psb: boolean): PsdChannel {\n return { id, compression: 1, data: encodeRle(plane, { planes: 1, width, height, depth: 8, psb }) };\n}\n\nconst emptyChannel = (id: number): PsdChannel => ({ id, compression: 0, data: new Uint8Array(0) });\n\nfunction maskChannel(mask: RasterMask, ox: number, oy: number, psb: boolean): { channel: PsdChannel; mask: PsdLayerMask } {\n if (mask.source) throw new PsdError('A parametric mask must be baked before export.');\n const bounds = contentBounds(mask.surface);\n const def = mask.defaultValue;\n const flags = (mask.enabled === false ? MASK_FLAG_DISABLED : 0) | (mask.linked === false ? MASK_FLAG_UNLINKED : 0);\n const m: PsdLayerMask = { rect: EMPTY_RECT, defaultColor: def, flags };\n if ((mask.density ?? 1) < 1) m.userDensity = Math.round(Math.max(0, mask.density ?? 1) * 255);\n if ((mask.feather ?? 0) > 0) m.userFeather = mask.feather;\n if (!bounds) return { channel: emptyChannel(CHANNEL_USER_MASK), mask: m };\n const plane = readRegion(mask.surface, bounds);\n m.rect = toRect({ ...bounds, x: bounds.x + ox, y: bounds.y + oy });\n return { channel: rleChannel(CHANNEL_USER_MASK, new Uint8Array(plane.buffer, plane.byteOffset, plane.length), bounds.width, bounds.height, psb), mask: m };\n}\n\nfunction lockBits(layer: ImageLayer): number {\n const l = layer.locks;\n if (!l) return 0;\n return (l.all ? 0x80000000 : 0) | (l.transparency ? 1 : 0) | (l.pixels ? 2 : 0) | (l.position ? 4 : 0);\n}\n\n/** Writes the prepared stack as PSD (or PSB) bytes. */\nexport function exportPsd(input: PsdExportInput): PsdExportResult {\n const { width, height } = input;\n if (width < 1 || height < 1) throw new PsdError('The document is empty.');\n const psb = width > PSD_MAX_DIMENSION || height > PSD_MAX_DIMENSION;\n const records: PsdLayerRecord[] = [];\n let nextId = 0;\n const notes = new Map<string, string[]>();\n const note = (msg: string, name: string) => notes.set(msg, [...(notes.get(msg) ?? []), name]);\n const patternIds = new Set<string>();\n\n /**\n * A layer's effects block: the block it was read from while the effects still map to the same\n * values (or could not be read and were not changed), else regenerated.\n */\n const effectsBlock = (layer: ImageLayer): PsdTaggedBlock | null => {\n const fx = layer.effects;\n if (!fx) return null;\n for (const e of fx.items) for (const id of usedPatterns(e)) patternIds.add(id);\n const key = layer.type === 'group' ? 'lfxs' : 'lfx2';\n if (fx.psd) {\n const original = parseLfx2(fx.psd.data);\n if (original ? effectsFingerprint(original.effects) === effectsFingerprint(fx) : !fx.items.length) return { key, data: padEven(fx.psd.data) };\n }\n if (!fx.items.length && fx.enabled && (fx.scale ?? 1) === 1) return null;\n const { data, dropped } = writeLfx2(fx);\n if (dropped) note('Only the first outer glow, inner glow, bevel, satin and pattern overlay of a layer can be written; the others were left out', layer.name);\n return { key, data };\n };\n\n const common = (layer: ImageLayer, extra: PsdTaggedBlock[] = []): Pick<PsdLayerRecord, 'opacity' | 'clipping' | 'flags' | 'legacyName' | 'blocks'> => {\n const name = layer.name || (layer.type === 'adjustment' ? adjustmentKindLabel(layer.kind) : '');\n const blocks: PsdTaggedBlock[] = [{ key: 'luni', data: unicodeBlock(name) }, { key: 'lyid', data: u32Bytes(++nextId) }, ...extra];\n const fx = effectsBlock(layer);\n if (fx) blocks.push(fx);\n const locks = lockBits(layer);\n if (locks) blocks.push({ key: 'lspf', data: u32Bytes(locks >>> 0) });\n return {\n opacity: Math.round(Math.min(1, Math.max(0, layer.opacity)) * 255),\n clipping: layer.clipped ? 1 : 0,\n flags: (layer.visible ? 0 : LAYER_FLAG_HIDDEN) | (layer.locks?.transparency ? LAYER_FLAG_TRANSPARENCY_PROTECTED : 0),\n legacyName: name,\n blocks,\n };\n };\n\n const write = (layer: ImageLayer): void => {\n switch (layer.type) {\n case 'group': {\n records.push({\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey: 'norm',\n opacity: 255,\n clipping: 0,\n flags: FLAG_PIXELS_IRRELEVANT | (layer.visible ? 0 : LAYER_FLAG_HIDDEN),\n legacyName: '</Layer group>',\n blocks: [\n { key: 'luni', data: unicodeBlock('</Layer group>') },\n { key: 'lsct', data: sectionBlock(SECTION_DIVIDER) },\n ],\n });\n for (const child of layer.children) write(child);\n const blendKey = BLEND_TO_PSD[layer.blend] ?? 'norm';\n const base = common(layer, [{ key: 'lsct', data: sectionBlock(layer.collapsed ? SECTION_CLOSED : SECTION_OPEN, blendKey) }]);\n const rec: PsdLayerRecord = {\n ...base,\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey,\n flags: base.flags | FLAG_PIXELS_IRRELEVANT,\n };\n if (layer.mask) {\n const m = maskChannel(layer.mask, 0, 0, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'adjustment': {\n const block = writeAdjustmentBlock(layer.kind, layer.params);\n const rec: PsdLayerRecord = {\n ...common(layer, [{ key: block.key, data: block.data }]),\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey: BLEND_TO_PSD[layer.blend] ?? 'norm',\n };\n if (layer.mask) {\n const m = maskChannel(layer.mask, 0, 0, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'pixel': {\n if (layer.source === 'document') throw new PsdError(`Layer \"${layer.name}\" reads the document image; resolve its pixels before export.`);\n if (layer.filters?.some(isActiveFilter)) throw new PsdError(`Layer \"${layer.name}\" has live filters; bake them before export.`);\n const extra: PsdTaggedBlock[] = [];\n if (layer.fillOpacity !== undefined && layer.fillOpacity < 1) {\n extra.push({ key: 'iOpa', data: Uint8Array.from([Math.round(Math.max(0, layer.fillOpacity) * 255), 0, 0, 0]) });\n }\n const rec: PsdLayerRecord = { ...common(layer, extra), rect: EMPTY_RECT, channels: [], blendKey: BLEND_TO_PSD[layer.blend] ?? 'norm' };\n const bounds = contentBounds(layer.surface);\n if (bounds) {\n const n = bounds.width * bounds.height;\n const [r, g, b, a] = rgbaToPlanes(readRegion(layer.surface, bounds), n);\n rec.rect = toRect({ ...bounds, x: bounds.x + layer.offset.x, y: bounds.y + layer.offset.y });\n rec.channels = [\n rleChannel(CHANNEL_ALPHA, a, bounds.width, bounds.height, psb),\n rleChannel(0, r, bounds.width, bounds.height, psb),\n rleChannel(1, g, bounds.width, bounds.height, psb),\n rleChannel(2, b, bounds.width, bounds.height, psb),\n ];\n } else {\n rec.channels = [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel);\n }\n if (layer.mask) {\n const m = maskChannel(layer.mask, layer.offset.x, layer.offset.y, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'annotation':\n throw new PsdError(`Annotation layer \"${layer.name}\" must be rasterized before export.`);\n }\n };\n for (const layer of input.layers) write(layer);\n if (records.length > 32767) throw new PsdError('A PSD holds at most 32767 layer records.');\n\n // Merged image: RGB, plus alpha when anything is transparent.\n const n = width * height;\n if (input.merged.length < n * 4) throw new PsdError(`The merged image has ${input.merged.length} bytes, expected ${n * 4}.`);\n // Photoshop's merged image is matted over white where it is transparent.\n const [r, g, b, a] = rgbaToPlanes(matteWhite(input.merged), n);\n const transparent = a.some((v) => v !== 255);\n const planes = transparent ? [r, g, b, a] : [r, g, b];\n const merged = new Uint8Array(n * planes.length);\n planes.forEach((p, i) => merged.set(p, i * n));\n\n const res = Math.max(1, input.resolution ?? 72);\n const rw = new PsdWriter();\n for (let k = 0; k < 2; k++) {\n rw.u32(Math.round(res * 65536));\n rw.u16(1); // pixels per inch\n rw.u16(1); // inches\n }\n const version = new PsdWriter();\n version.u32(1);\n version.u8(1); // the merged image is real\n version.unicode('Pixerate Editor');\n version.unicode('Pixerate Editor');\n version.u32(1);\n const resources: PsdResource[] = [\n { id: 1005, name: '', data: rw.result() },\n { id: 1057, name: '', data: version.result() },\n ];\n if (input.globalLight) {\n const i32 = (v: number) => {\n const w = new PsdWriter();\n w.i32(Math.round(v));\n return w.result();\n };\n resources.push({ id: 1037, name: '', data: i32(input.globalLight.angle) }, { id: 1049, name: '', data: i32(input.globalLight.altitude) });\n }\n // The patterns the effects use, so Photoshop can draw them.\n const globalBlocks: PsdTaggedBlock[] = [];\n if (patternIds.size) {\n const byId = new Map((input.patterns ?? []).map((p) => [p.id, p]));\n const embed: EffectPattern[] = [];\n for (const id of patternIds) {\n const p = byId.get(id) ?? builtinPattern(id);\n if (p) embed.push(p);\n else note(`Pattern \"${id}\" is not in the document and was not embedded`, id);\n }\n if (embed.length) globalBlocks.push({ key: 'Patt', data: writePatternBlock(embed) });\n }\n\n const bytes = writePsd(\n {\n header: { psb, channels: planes.length, width, height, depth: 8, colorMode: 3 },\n colorModeData: new Uint8Array(0),\n resources,\n layers: records,\n mergedAlpha: transparent && records.length > 0,\n globalBlocks,\n imageCompression: 1,\n imageData: encodeRle(merged, { planes: planes.length, width, height, depth: 8, psb }),\n },\n encodeLegacyName\n );\n const warnings = [...notes].map(([msg, names]) => `${msg} (${names.map((n) => `\"${n}\"`).join(', ')}).`);\n return { bytes, psb, records: records.length, warnings };\n}\n","// Portions ported from photocraft/crates/engine/src/transform_cmds.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Free Transform sessions: what a transform moves (a layer's pixels with its linked mask, a lone\n * unlinked mask, the selected pixels as a floating piece, a copy of them on a new layer, or just\n * the selection outline), a live preview while the geometry changes, and one undo step when it is\n * committed with full-quality resampling.\n */\n\nimport {\n TiledSurface,\n contentBounds,\n readRegion,\n surfaceFromPixels,\n writeRegion,\n type RasterRect,\n} from '../raster/surface';\nimport {\n compositeSurfaceOver,\n reduceSurface,\n transformSurface,\n TransformTooLargeError,\n warpSurface,\n isInterpolation,\n type Interpolation,\n type ResampleOptions,\n} from '../raster/transform';\nimport { multiplyHomographies, type Homography } from '../geom/homography';\nimport { findLayer, insertLayer, mapLayer, newLayerId, type ImageLayer, type LayerTarget, type PixelLayer, type RasterMask, type SmartObjectLayer } from './layers/model';\nimport { LayerOperationError, requireLayer, requireUnlocked } from './layers/ops';\nimport { contentsBox, isContentsDecoded, placementGeometry, renderSmartObject, setSmartPlacement } from './layers/smart';\nimport { createSelectionMask, type SelectionMask } from './selection/mask';\nimport {\n geometryHomography,\n hasWarp,\n identityTransform,\n isIdentityTransform,\n transformMap,\n type FreeTransformGeometry,\n} from './transform';\nimport type { CropBox } from './types';\n\n/**\n * What a Free Transform moves:\n * - `layer`: the layer's pixels with its linked mask (or, with `channel: 'mask'` on an unlinked\n * mask, the mask alone);\n * - `selection`: the selected pixels (and the same part of a linked mask), lifted and committed\n * back into the layer; the selection outline moves with them;\n * - `selection-copy`: Layer via Copy, then the copy is transformed on a new layer above;\n * - `selection-outline`: Transform Selection, only the outline;\n * - `auto` (default): `selection` when something is selected, else `layer`.\n */\nexport type FreeTransformContent = 'auto' | 'layer' | 'selection' | 'selection-copy' | 'selection-outline';\n\nexport const FREE_TRANSFORM_CONTENTS: readonly FreeTransformContent[] = ['auto', 'layer', 'selection', 'selection-copy', 'selection-outline'];\n\nexport interface FreeTransformInit {\n /** The layer (default the active one); not needed for `selection-outline`. */\n layerId?: string;\n content?: FreeTransformContent;\n /**\n * `pixels` (default) moves the pixels with a linked mask; `mask` moves an unlinked mask alone (a\n * linked one moves with its pixels). Defaults to the active target on the active layer.\n */\n channel?: LayerTarget;\n /** Resampling of the committed result (default bicubic). */\n interpolation?: Interpolation;\n}\n\n/** How a live preview is drawn (the UI knows these, the controller doesn't). */\nexport interface FreeTransformPreview {\n /** Screen pixels per document pixel: below 1 the preview samples a reduced copy in blocks. */\n scale?: number;\n /** Only this part of the document (the visible area) is resampled. */\n clip?: CropBox;\n}\n\n/** What a committed transform changed. */\nexport interface FreeTransformResult {\n /** The layer that holds the result (the new layer for `selection-copy`; `null` for `selection-outline`). */\n layerId: string | null;\n content: Exclude<FreeTransformContent, 'auto'>;\n /** What moved. */\n moved: Array<'pixels' | 'mask' | 'selection'>;\n /** The frame the transform started from (document pixels). */\n box: CropBox;\n /** Where the frame's corners went. */\n corners: FreeTransformGeometry['corners'];\n /** Document bounds of the moved content after the transform (pixels, or the mask or the outline). */\n bounds: CropBox | null;\n /** False when the geometry was the identity and nothing was recorded. */\n changed: boolean;\n}\n\n/** @internal What the session needs from the controller. */\nexport interface FreeTransformHost {\n layers(): ImageLayer[];\n activeLayerId(): string | null;\n activeTarget(): LayerTarget;\n selection(): SelectionMask | null;\n canvas(): { width: number; height: number };\n /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */\n pixelsOf(layer: PixelLayer | SmartObjectLayer): TiledSurface;\n /** Opaque pre-transform snapshot. */\n capture(): unknown;\n /** Shows a live state (no history). */\n show(layers: ImageLayer[], selection: SelectionMask | null, activeLayerId: string | null): void;\n /** Records `before` as the undo step of the state now shown. */\n record(before: unknown): void;\n /** Puts `before` back (no history). */\n restore(before: unknown): void;\n}\n\n/** A surface that moves: in layer space (`offset` to document), composited back or replacing. */\ninterface Part {\n what: 'pixels' | 'mask' | 'selection';\n offset: { x: number; y: number };\n /** RGBA (pixels, or a mask part as grey + alpha) or one channel (a whole mask, the outline). */\n moving: TiledSurface;\n /** What stays: composited under the moved part (`null`: the moved part replaces the surface). */\n base: TiledSurface | null;\n /** Reduced copies for previews, by factor. */\n reduced: Map<number, TiledSurface>;\n}\n\nconst fail = (message: string): never => {\n throw new LayerOperationError(message);\n};\n\nconst T = (x: number, y: number): Homography => [1, 0, x, 0, 1, y, 0, 0, 1];\n\n/** Preview block size for a screen scale: 1 at 100 % and above, then powers of two up to 8. */\nexport function previewStep(scale: number | undefined): number {\n if (!scale || !(scale > 0) || scale >= 1) return 1;\n return Math.min(8, 2 ** Math.floor(Math.log2(1 / scale)));\n}\n\nfunction toDoc(r: RasterRect | null, o: { x: number; y: number }): CropBox | null {\n return r ? { x: r.x + o.x, y: r.y + o.y, width: r.width, height: r.height } : null;\n}\n\nfunction selectionSurface(sel: SelectionMask): TiledSurface {\n return surfaceFromPixels({ width: sel.width, height: sel.height, data: sel.data }, 1, 0, 0, 0);\n}\n\n/**\n * Lifts the selected part of an RGBA surface (layer space): the lifted pixels (alpha × coverage)\n * and the rest (alpha × (1 − coverage); fully lifted pixels become transparent black).\n */\nfunction liftPixels(surface: TiledSurface, sel: SelectionMask, o: { x: number; y: number }): { lifted: TiledSurface; rest: TiledSurface } {\n const sb = sel.bounds!;\n const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, area);\n const kept = new Uint8ClampedArray(px);\n for (let y = 0; y < area.height; y++) {\n for (let x = 0; x < area.width; x++) {\n const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;\n const i = (y * area.width + x) * 4;\n if (k <= 0) {\n px[i] = px[i + 1] = px[i + 2] = px[i + 3] = 0;\n continue;\n }\n px[i + 3] = px[i + 3] * k;\n if (!px[i + 3]) px[i] = px[i + 1] = px[i + 2] = 0;\n kept[i + 3] = kept[i + 3] * (1 - k);\n if (!kept[i + 3]) kept[i] = kept[i + 1] = kept[i + 2] = 0;\n }\n }\n return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: px }, 4, area.x, area.y), rest: writeRegion(surface, area, kept) };\n}\n\n/**\n * Lifts the selected part of a mask (layer space): the values as grey + alpha (alpha = coverage)\n * and the rest (the selected values replaced by the default). Everything selected moves,\n * default-valued parts included, so the mask stays aligned with the moved pixels.\n */\nfunction liftMask(mask: TiledSurface, sel: SelectionMask, o: { x: number; y: number }): { lifted: TiledSurface; rest: TiledSurface } {\n const sb = sel.bounds!;\n const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };\n const v = readRegion(mask, area);\n const def = mask.defaultValue;\n const lifted = new Uint8ClampedArray(area.width * area.height * 4);\n const rest = new Uint8ClampedArray(v.length);\n for (let y = 0; y < area.height; y++) {\n for (let x = 0; x < area.width; x++) {\n const i = y * area.width + x;\n const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;\n const g = v[i];\n if (k > 0) {\n lifted[i * 4] = lifted[i * 4 + 1] = lifted[i * 4 + 2] = g;\n lifted[i * 4 + 3] = k * 255;\n }\n rest[i] = g * (1 - k) + def * k;\n }\n }\n return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: lifted }, 4, area.x, area.y), rest: writeRegion(mask, area, rest) };\n}\n\n/**\n * A Free Transform in progress (see `ImageEditorController.beginFreeTransform`). Change the\n * geometry with `update` (each change shows a live preview without history), then `commit()`\n * (one undo step, resampled at full quality) or `cancel()` (everything back as it was).\n */\nexport class FreeTransformSession {\n readonly content: Exclude<FreeTransformContent, 'auto'>;\n /** The layer being transformed (`null` for `selection-outline`). */\n readonly layerId: string | null;\n /** The new layer of a `selection-copy`. */\n readonly copyId: string | null = null;\n /** The frame (document pixels). */\n readonly box: CropBox;\n /** What moves. */\n readonly moves: ReadonlyArray<'pixels' | 'mask' | 'selection'>;\n /**\n * The geometry the transform starts from: the identity over `box`, or for a smart object its\n * placement (`box` is then the contents' rectangle in contents pixels, and the transform edits\n * the placement without resampling anything twice).\n */\n readonly startGeometry: FreeTransformGeometry;\n /** True when the layer is a smart object (the transform sets its placement). */\n readonly smartObject: boolean = false;\n private geometryValue: FreeTransformGeometry;\n private readonly smart: SmartObjectLayer | null = null;\n private readonly smartReduced = new Map<number, TiledSurface>();\n private readonly startInterp: Interpolation;\n private smartMasksMoved = false;\n private interp: Interpolation;\n private readonly parts: Part[] = [];\n private readonly before: unknown;\n private readonly startLayers: ImageLayer[];\n private readonly startSelection: SelectionMask | null;\n private readonly startActive: string | null;\n /** The stack this session last showed: anything else means the document changed under it. */\n private shown: ImageLayer[];\n private closed = false;\n\n constructor(\n private readonly host: FreeTransformHost,\n init: FreeTransformInit = {}\n ) {\n if (init.interpolation !== undefined && !isInterpolation(init.interpolation)) fail(`Unknown interpolation \"${String(init.interpolation)}\"; use nearest, bilinear or bicubic.`);\n this.interp = init.interpolation ?? 'bicubic';\n const sel = host.selection();\n const selected = Boolean(sel?.bounds);\n let content = init.content ?? 'auto';\n if (!(['auto', 'layer', 'selection', 'selection-copy', 'selection-outline'] as const).includes(content)) {\n fail(`Unknown content \"${String(content)}\"; use auto, layer, selection, selection-copy or selection-outline.`);\n }\n const targetId = init.layerId ?? host.activeLayerId();\n const target = targetId && content !== 'selection-outline' ? findLayer(host.layers(), targetId)?.layer : undefined;\n const smartTarget = target?.type === 'smart' && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : 'pixels')) !== 'mask';\n // A smart object transforms as a whole, selection or not (as in Photoshop).\n if (content === 'auto') content = selected && !smartTarget ? 'selection' : 'layer';\n this.content = content;\n if (content !== 'layer' && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);\n const layers = host.layers();\n this.startLayers = layers;\n this.startSelection = sel;\n this.startActive = host.activeLayerId();\n this.before = host.capture();\n const moves: Array<'pixels' | 'mask' | 'selection'> = [];\n let box: CropBox | null = null;\n\n if (content === 'selection-outline') {\n this.layerId = null;\n this.parts.push({ what: 'selection', offset: { x: 0, y: 0 }, moving: selectionSurface(sel!), base: null, reduced: new Map() });\n moves.push('selection');\n box = { ...sel!.bounds! };\n } else {\n const id = init.layerId ?? host.activeLayerId();\n if (!id) fail('No layer given and none is active; pass layerId (see list_layers).');\n const layer = requireLayer(layers, id!).layer;\n this.layerId = layer.id;\n const mask = layer.mask && !layer.mask.source ? layer.mask : undefined;\n const channel: LayerTarget = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : 'pixels');\n if (channel !== 'pixels' && channel !== 'mask') fail(`Unknown channel \"${String(channel)}\"; use pixels or mask.`);\n const linked = mask?.linked !== false;\n const maskAlone = channel === 'mask' && (!linked || layer.type !== 'pixel');\n if (channel === 'mask' && !mask) fail(layer.mask?.source ? `Layer \"${layer.name || id}\" has a depth mask, which is generated, not transformed.` : `Layer \"${layer.name || id}\" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);\n if (!maskAlone && layer.type !== 'pixel' && layer.type !== 'smart') fail(`Layer \"${layer.name || id}\" is a ${layer.type} layer; Free Transform works on pixel layers, smart objects, layer masks (channel mask) and selections.`);\n if (!layer.visible && content !== 'selection-copy') fail(`Layer \"${layer.name || id}\" is hidden; show it before you transform it.`);\n if (content !== 'selection-copy') {\n requireUnlocked(layer, 'position', 'transform it');\n if (!maskAlone) requireUnlocked(layer, 'pixels', 'transform it');\n }\n const o = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const pixels = layer.type === 'pixel' ? host.pixelsOf(layer) : null;\n if (layer.type === 'smart' && !maskAlone) {\n if (content !== 'layer') fail(`Layer \"${layer.name || id}\" is a smart object, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);\n if (!isContentsDecoded(layer.contents)) fail(`The contents of \"${layer.name || id}\" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);\n this.smart = layer;\n this.smartObject = true;\n this.startGeometry = placementGeometry(layer);\n if (init.interpolation === undefined && layer.placement.interpolation) this.interp = layer.placement.interpolation;\n this.startInterp = this.interp;\n this.box = contentsBox(layer.contents);\n this.moves = mask && linked ? ['pixels', 'mask'] : ['pixels'];\n this.geometryValue = this.startGeometry;\n this.shown = this.startLayers;\n return;\n }\n if (maskAlone) {\n if (content === 'selection-copy') fail('Layer via Copy copies pixels; transform a mask with content layer or selection.');\n if (content === 'selection') {\n const { lifted, rest } = liftMask(mask!.surface, sel!, o);\n this.parts.push({ what: 'mask', offset: o, moving: lifted, base: rest, reduced: new Map() });\n box = toDoc(contentBounds(lifted), o);\n } else {\n this.parts.push({ what: 'mask', offset: o, moving: mask!.surface, base: null, reduced: new Map() });\n box = toDoc(contentBounds(mask!.surface), o);\n }\n moves.push('mask');\n } else if (content === 'layer') {\n this.parts.push({ what: 'pixels', offset: o, moving: pixels!, base: null, reduced: new Map() });\n moves.push('pixels');\n box = toDoc(contentBounds(pixels!), o);\n if (mask && linked) {\n this.parts.push({ what: 'mask', offset: o, moving: mask.surface, base: null, reduced: new Map() });\n moves.push('mask');\n if (!box) box = toDoc(contentBounds(mask.surface), o);\n }\n } else if (content === 'selection') {\n const { lifted, rest } = liftPixels(pixels!, sel!, o);\n this.parts.push({ what: 'pixels', offset: o, moving: lifted, base: rest, reduced: new Map() });\n moves.push('pixels');\n box = toDoc(contentBounds(lifted), o);\n if (mask && linked) {\n const m = liftMask(mask.surface, sel!, o);\n this.parts.push({ what: 'mask', offset: o, moving: m.lifted, base: m.rest, reduced: new Map() });\n moves.push('mask');\n }\n } else {\n // selection-copy: the copy goes on a new layer above, with the source layer's offset.\n const { lifted } = liftPixels(pixels!, sel!, o);\n box = toDoc(contentBounds(lifted), o);\n if (box) {\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: `${layer.name || 'Layer'} copy`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: lifted,\n offset: { ...o },\n };\n (this as { copyId: string | null }).copyId = copy.id;\n const loc = findLayer(layers, layer.id)!;\n this.startLayers = insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1);\n this.parts.push({ what: 'pixels', offset: o, moving: lifted, base: null, reduced: new Map() });\n moves.push('pixels');\n }\n }\n if (content !== 'layer') {\n this.parts.push({ what: 'selection', offset: { x: 0, y: 0 }, moving: selectionSurface(sel!), base: null, reduced: new Map() });\n moves.push('selection');\n }\n }\n if (!box && moves[0] === 'mask') fail('The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).');\n if (!box) fail(content === 'layer' ? 'The layer has no pixels to transform.' : 'The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).');\n this.box = box!;\n this.moves = moves;\n this.startGeometry = identityTransform(box!);\n this.startInterp = this.interp;\n this.geometryValue = this.startGeometry;\n this.shown = this.startLayers;\n if (this.copyId) host.show(this.startLayers, sel, this.copyId);\n this.shown = host.layers();\n }\n\n /** The current geometry. */\n get geometry(): FreeTransformGeometry {\n return this.geometryValue;\n }\n\n get interpolation(): Interpolation {\n return this.interp;\n }\n\n get isOpen(): boolean {\n return !this.closed;\n }\n\n private check(): void {\n if (this.closed) fail('This transform has already been committed or cancelled; begin a new one.');\n if (this.host.layers() !== this.shown) {\n this.closed = true;\n fail('The document changed while the transform was open, so it was abandoned; begin a new one.');\n }\n }\n\n /**\n * Changes the geometry (`corners`, `warp`) or the interpolation and, unless `preview` is `false`,\n * shows the result live (no history). Returns the geometry.\n */\n update(changes: { corners?: FreeTransformGeometry['corners']; warp?: FreeTransformGeometry['warp']; interpolation?: Interpolation }, preview: FreeTransformPreview | false = {}): FreeTransformGeometry {\n this.check();\n if (changes.interpolation !== undefined) {\n if (!isInterpolation(changes.interpolation)) fail(`Unknown interpolation \"${String(changes.interpolation)}\"; use nearest, bilinear or bicubic.`);\n this.interp = changes.interpolation;\n }\n const next: FreeTransformGeometry = {\n box: this.box,\n corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })) as unknown as FreeTransformGeometry['corners'],\n warp: changes.warp === undefined ? this.geometryValue.warp ?? null : changes.warp,\n };\n if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail('corners must be four finite points: top-left, top-right, bottom-right, bottom-left.');\n if (!transformMap(next)) fail('These corners fold the frame over itself or through the horizon; keep the quad convex.');\n this.geometryValue = next;\n if (preview !== false) this.render(preview);\n return next;\n }\n\n /** Resamples every part for the current geometry and shows it (or returns it, for commit). */\n private resample(final: boolean, preview: FreeTransformPreview = {}): { layers: ImageLayer[]; selection: SelectionMask | null } {\n if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };\n const g = this.geometryValue;\n const identity = isIdentityTransform(g);\n const warp = hasWarp(g);\n const h = warp ? null : geometryHomography(g);\n const f = warp ? transformMap(g) : null;\n const step = final ? 1 : previewStep(preview.scale);\n const interpolation: Interpolation = final ? this.interp : this.interp === 'nearest' ? 'nearest' : 'bilinear';\n let layers = this.startLayers;\n let selection = this.startSelection;\n const targetId = this.copyId ?? this.layerId;\n for (const part of this.parts) {\n const o = part.offset;\n let moved: TiledSurface;\n if (identity) moved = part.moving;\n else {\n let src = part.moving;\n let k = 1;\n if (step > 1) {\n k = step;\n let r = part.reduced.get(k);\n if (!r) {\n r = reduceSurface(part.moving, k);\n part.reduced.set(k, r);\n }\n src = r;\n }\n const opts: ResampleOptions = { interpolation, step: step > 1 ? step : undefined };\n if (preview.clip && !final) opts.clip = { x: Math.floor(preview.clip.x - o.x), y: Math.floor(preview.clip.y - o.y), width: Math.ceil(preview.clip.width) + 1, height: Math.ceil(preview.clip.height) + 1 };\n try {\n if (f) {\n moved = warpSurface(src, (p) => {\n const q = f({ x: p.x * k + o.x, y: p.y * k + o.y });\n return q && { x: q.x - o.x, y: q.y - o.y };\n }, opts);\n } else {\n // Layer space → document → transform → layer space, from the reduced copy's pixels.\n const hl = multiplyHomographies(multiplyHomographies(multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], T(o.x, o.y)), h!), T(-o.x, -o.y));\n moved = transformSurface(src, hl, opts);\n }\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;\n if (part.what === 'selection') {\n const { width, height } = this.host.canvas();\n const data = new Uint8Array(readRegion(result, { x: 0, y: 0, width, height }).buffer);\n const next = createSelectionMask(width, height, data);\n selection = next.bounds ? next : null;\n } else if (part.what === 'pixels') {\n layers = mapLayer(layers, targetId!, (l) => {\n const next = { ...(l as PixelLayer), surface: result };\n delete next.source;\n return next;\n });\n } else {\n layers = mapLayer(layers, targetId!, (l) => ({ ...l, mask: { ...(l.mask as RasterMask), surface: result } }) as ImageLayer);\n }\n }\n return { layers, selection };\n }\n\n /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */\n private resampleSmart(final: boolean, preview: FreeTransformPreview): ImageLayer[] {\n const layer = this.smart!;\n const g = this.geometryValue;\n const placement = { corners: g.corners, warp: g.warp ?? null, interpolation: this.interp };\n const canvas = this.host.canvas();\n try {\n if (final) {\n const res = setSmartPlacement(this.startLayers, layer.id, placement, canvas);\n this.smartMasksMoved = res.masksMoved;\n return res.layers;\n }\n const step = previewStep(preview.scale);\n const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });\n return mapLayer(this.startLayers, layer.id, (l) => ({ ...(l as SmartObjectLayer), placement, surface }));\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n\n /** Whether the geometry (or the interpolation of a smart object) differs from where it started. */\n private isChanged(): boolean {\n const g = this.geometryValue;\n if (!this.smart) return !isIdentityTransform(g);\n const a = this.startGeometry;\n const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);\n const warp = (w: FreeTransformGeometry['warp'], box: CropBox) => (w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : '');\n return !same || warp(g.warp, g.box) !== warp(a.warp, a.box) || this.interp !== this.startInterp;\n }\n\n private render(preview: FreeTransformPreview): void {\n const { layers, selection } = this.resample(false, preview);\n this.host.show(layers, selection, this.copyId ?? this.startActive);\n this.shown = this.host.layers();\n }\n\n /** Finishes the transform with full-quality resampling, as one undo step. */\n commit(): FreeTransformResult {\n this.check();\n const changed = this.isChanged() || Boolean(this.copyId);\n if (!changed) {\n this.closed = true;\n this.host.restore(this.before);\n return this.result(false, this.startLayers, this.startSelection);\n }\n const { layers, selection } = this.resample(true);\n this.closed = true;\n this.host.show(layers, selection, this.copyId ?? this.startActive);\n this.host.record(this.before);\n return this.result(true, layers, selection);\n }\n\n /** Abandons the transform and puts everything back (no history). */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.host.restore(this.before);\n }\n\n private result(changed: boolean, layers: ImageLayer[], selection: SelectionMask | null): FreeTransformResult {\n let bounds: CropBox | null = null;\n const id = this.copyId ?? this.layerId;\n const layer = id ? findLayer(layers, id)?.layer : undefined;\n if (this.moves.includes('pixels') && layer?.type === 'pixel') bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);\n else if (this.moves.includes('pixels') && layer?.type === 'smart') bounds = contentBounds(layer.surface);\n else if (this.moves.includes('mask') && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 });\n else if (this.moves.includes('selection')) bounds = selection?.bounds ? { ...selection.bounds } : null;\n return {\n layerId: this.copyId ?? this.layerId,\n content: this.content,\n moved: this.smart ? this.moves.filter((m) => m !== 'mask' || this.smartMasksMoved) : [...this.moves],\n box: { ...this.box },\n corners: this.geometryValue.corners,\n bounds,\n changed,\n };\n }\n}\n","import {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentParamsMap,\n Annotation,\n CreateAnnotationPayload,\n AspectRatio,\n CropBox,\n DepthMaskOptions,\n ExportOptions,\n ImageAdjustments,\n ImageDimensions,\n ImageEditorFit,\n ImageEditorOptions,\n ImageEditorState,\n ImageEditorTool,\n ImageTransform,\n ImagePoint,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { ImageEditorRenderer, RenderOptions } from './renderer';\nimport { clampCropToImage, isQuarterTurnSwapped } from './geometry';\nimport { LATEST_PROCESS_VERSION, LEGACY_PROCESS_VERSION, normalizeProcessVersion } from './color';\nimport { createAdjustmentLayer, normalizeAdjustmentLayer, normalizeAdjustmentLayers } from './adjustment-layers';\nimport type { BlendMode } from './blend';\nimport type { ImageHistogram } from './histogram';\nimport { applyAdjustments } from './filters';\nimport {\n combineSelections,\n createSelectionMask,\n deserializeSelection,\n fullSelection,\n invertSelection as invertSelectionMask,\n resizeSelection,\n selectionFromPixels,\n serializeSelection,\n type PixelBuffer,\n type SelectionChannel,\n type SelectionMask,\n type SelectionMode,\n} from './selection/mask';\nimport { ellipseSelection, polygonSelection, rectSelection, type ShapeSelectionOptions } from './selection/shapes';\nimport { colorRange, magicWand, type ColorRangeOptions, type ColorRangeSample, type MagicWandOptions } from './selection/wand';\nimport { modifySelection as modifySelectionMask, type SelectionModifyOp } from './selection/modify';\nimport { refineSelection as refineSelectionMask, type RefineEdgeParams } from './selection/refine';\nimport type { LayerMaskProps } from './selection/layer-mask';\nimport type { AdjustmentLayerMask } from './types';\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES, DEFAULT_TRANSFORM_VALUES } from './layers/defaults';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isActiveFilter,\n mapLayer,\n newLayerId,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type GroupLayer,\n type LayerBlendMode,\n type ImageLayer,\n type LayerFilter,\n type LayerTarget,\n type PixelLayer,\n type RasterMask,\n type SmartContents,\n type SmartFilter,\n type SmartObjectLayer,\n type SmartPlacement,\n} from './layers/model';\nimport {\n addSmartFilter as addSmartFilterOp,\n contentsSurface,\n convertToSmartObject as convertToSmartObjectOp,\n copySmartObject as copySmartObjectOp,\n documentContents,\n instancesOf,\n isContentsDecoded,\n moveSmartFilter as moveSmartFilterOp,\n normalizeSmartFilter,\n offsetSmartObject,\n placementGeometry,\n rasterContents,\n rasterizeSmartObject as rasterizeSmartObjectOp,\n removeSmartFilter as removeSmartFilterOp,\n replaceContents as replaceContentsOp,\n requireSmartObject,\n setSmartFilterMask as setSmartFilterMaskOp,\n setSmartFiltersEnabled as setSmartFiltersEnabledOp,\n setSmartPlacement,\n updateSmartFilter as updateSmartFilterOp,\n withFilterMaskSurface,\n} from './layers/smart';\nimport {\n BACKGROUND_LAYER_ID,\n BACKGROUND_LAYER_NAME,\n BLUR_FILTER_ID,\n DEVELOP_FILTER_ID,\n adjustmentNodeFromLegacy,\n annotationLayerFor,\n annotationLayerName,\n depthRasterMask,\n developTarget,\n isDefaultDepthMask,\n layersFromLegacy,\n legacyMaskOf,\n legacyView,\n rasterMaskFromLegacy,\n type LayerStack,\n} from './layers/migrate';\nimport {\n LAYERED_SCHEMA_VERSION,\n deserializeLayers,\n deserializePatterns,\n serializePatterns,\n resolveExternalAssets,\n serializeLayers,\n serializeLayersExternal,\n type ImageEditorAssetStore,\n type SerializedLayeredImageEditorState,\n} from './layers/serialize';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport {\n SurfaceDraft,\n TILE,\n collectTiles,\n contentBounds,\n emptySurface,\n packRgba,\n readPixel,\n readRegion,\n solidSurface,\n surfaceFromPixels,\n tilesInRect,\n translateSurface,\n writeRegion,\n type PixelSurface,\n type Tile,\n type TiledSurface,\n type TileKey,\n TiledSurface as TiledSurfaceClass,\n finishTile,\n tileKey,\n} from '../raster/surface';\nimport {\n LayerOperationError,\n addLayerEffect as addLayerEffectOp,\n addLayerFilter as addLayerFilterOp,\n layerEffectsOf,\n removeLayerEffect as removeLayerEffectOp,\n setLayerEffects as setLayerEffectsOp,\n setLayerEffectsEnabled as setLayerEffectsEnabledOp,\n updateLayerEffect as updateLayerEffectOp,\n duplicateLayer as duplicateLayerOp,\n groupLayers as groupLayersOp,\n moveLayer as moveLayerOp,\n normalizeFilter,\n offsetLayer as offsetLayerOp,\n parseBlend,\n placeLayer,\n removeLayer as removeLayerOp,\n removeLayerFilter as removeLayerFilterOp,\n requireLayer,\n requireUnlocked,\n setLayerMask as setLayerMaskOp,\n setLayerProperties as setLayerPropertiesOp,\n ungroupLayer as ungroupLayerOp,\n unionRect,\n updateLayerFilter as updateLayerFilterOp,\n type LayerPlacement,\n type LayerProperties,\n} from './layers/ops';\nimport { coveringRect } from './layers/render';\nimport {\n CoverageMap,\n DEFAULT_BRUSH,\n DEFAULT_MIXER,\n EMPTY_MIXER_LOAD,\n MixerStroke,\n StrokeCoverage,\n applyStrokeToTile,\n cleanMixer,\n loadMixer,\n normalizeBrush,\n normalizeMixer,\n type BrushSettings,\n type MixerLoad,\n type MixerSettings,\n type MixerTarget,\n type StrokeApply,\n type StrokePoint,\n} from '../paint';\nimport { base64ToBytes, bytesToBase64, decodePng, encodePng, isPng, parseDataUrl } from '../raster/png';\nimport type { Interpolation } from '../raster/transform';\nimport { getAnnotationBounds } from './geometry';\nimport { importPsd, type PsdImportResult } from './psd/import';\nimport { exportPsd } from './psd/export';\nimport { FreeTransformSession, type FreeTransformHost, type FreeTransformInit, type FreeTransformResult } from './free-transform';\nimport {\n anchorPoint,\n applyParams,\n cornersFromMatrix,\n describeTransform,\n flipCorners,\n identityTransform,\n matrixFromParams,\n transformedBox,\n isValidWarp,\n presetWarp,\n type AffineMatrix,\n type Quad,\n type TransformParams,\n type WarpMesh,\n type WarpPreset,\n} from './transform';\nimport { WARP_STYLES } from '../geom/warp';\nimport { applyMatrix, multiplyMatrices, translateMatrix } from '../geom/affine';\nimport type { EffectPattern } from '../raster/effects/patterns';\nimport { DEFAULT_GLOBAL_LIGHT, effectsMargin, type GlobalLight, type LayerEffect, type LayerEffectKind, type LayerEffects } from '../raster/effects/model';\n\n/** What `getSmartObject` reports. */\nexport interface SmartObjectInfo {\n layerId: string;\n contents: {\n /** Shared by linked instances. */\n id: string;\n name: string;\n kind: 'raster' | 'document';\n /** Pixel size of the contents. */\n width: number;\n height: number;\n /** False for a file that could not be decoded here (the layer shows its stored pixels). */\n decoded: boolean;\n /** The type of the file the contents came from (`psb`, `png`, `jpg`, …), or `null`. */\n fileType: string | null;\n /** The external file of linked contents, or `null` for embedded ones. */\n linkedPath: string | null;\n /** Every smart object placed from these contents (this one included), bottom first. */\n instances: string[];\n /** Layers in a nested document (0 for raster contents). */\n layerCount: number;\n };\n placement: {\n /** Where the contents' corners land: top-left, top-right, bottom-right, bottom-left (document pixels). */\n corners: Quad;\n /** The warp mesh in contents pixels, or `null`. */\n warp: WarpMesh | null;\n interpolation: Interpolation;\n /** False for distort, perspective and warp (then only corners and bounds describe it). */\n affine: boolean;\n /** Percent of the contents' own size. */\n scaleX: number;\n scaleY: number;\n rotate: number;\n skewX: number;\n /** Where the contents' centre is (document pixels). */\n center: { x: number; y: number };\n /** Bounds of the placed outline. */\n bounds: CropBox | null;\n };\n smartFilters: {\n enabled: boolean;\n /** Bottom (applied first) to top. Photoshop filters we keep but do not draw have kind `psd`. */\n filters: Array<Record<string, unknown>>;\n mask: { enabled: boolean; linked: boolean; density: number; feather: number; bounds: CropBox | null; defaultValue: number } | null;\n };\n /** Document bounds of the rendered pixels. */\n bounds: CropBox | null;\n}\n\n/** The document set aside while smart object contents are open. */\ninterface SavedDocument {\n state: ImageEditorState;\n undo: HistoryEntry[];\n redo: HistoryEntry[];\n initial: HistoryEntry;\n element: CanvasImageSource | null;\n samplePixels: PixelBuffer | null;\n samplePixelsOverride: boolean;\n psdResolution: number | undefined;\n}\n\n/** A nested document's layers as `exportPsd` takes them (filters baked, smart objects and annotations as their pixels). */\nfunction prepareNestedForPsd(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n let next: ImageLayer = l;\n if (l.type === 'group') next = { ...l, children: prepareNestedForPsd(l.children) };\n else if (l.type === 'smart') next = rasterizeSmartObjectOp([l], l.id).layer;\n else if (l.type === 'pixel' && l.filters?.length) {\n const p: PixelLayer = { ...l };\n if (l.filters.some(isActiveFilter)) {\n const b = l.surface.bounds;\n if (b) {\n const r = { x: b.x + l.offset.x - 64, y: b.y + l.offset.y - 64, width: b.width + 128, height: b.height + 128 };\n const plain: PixelLayer = { ...l, visible: true, opacity: 1, blend: 'normal' };\n delete plain.mask;\n delete plain.clipped;\n delete plain.effects;\n delete plain.fillOpacity;\n const data = new LayerCompositor().composite([plain], r, { width: r.x + r.width, height: r.y + r.height });\n p.surface = surfaceFromPixels({ width: r.width, height: r.height, data }, 4, r.x - l.offset.x, r.y - l.offset.y);\n }\n }\n delete p.filters;\n next = p;\n }\n if (next.mask?.source) {\n next = { ...next } as ImageLayer;\n delete next.mask;\n }\n out.push(next);\n }\n return out;\n}\n\n/** Latest serialized schema version (`3`: layers). Documents with the old shape still write 1 or 2. */\nexport const IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;\n\n/** Changes to an adjustment layer; `params` is merged into the current parameters. */\nexport interface AdjustmentLayerUpdate<K extends AdjustmentKind = AdjustmentKind> {\n params?: Partial<AdjustmentParamsMap[K]>;\n enabled?: boolean;\n opacity?: number;\n blend?: BlendMode;\n name?: string;\n /** A layer mask, or `null` to remove it. */\n mask?: AdjustmentLayerMask | null;\n}\n\n/** Options shared by the selection commands. */\nexport interface SelectionCommandOptions {\n /** How the result combines with the current selection (default `replace`). */\n mode?: SelectionMode;\n /** Feather the new shape by this many pixels before combining (default 0). */\n feather?: number;\n}\n\n/** Where an AI result goes (see `applyInpaintedImage`, `applyBackgroundRemovedImage`). */\nexport interface AIResultOptions {\n /** `new-layer`: a new layer (inpainting) or a mask (background removal); `replace`: the Background's pixels. Default: by `isMultiLayer`. */\n target?: 'new-layer' | 'replace';\n}\n\n/** Where the selection tools read colours from. */\nexport interface SampleOptions {\n /**\n * `composite` (default): the image, as before (with several layers: the composite of the pixel\n * layers, or with `sampleMerged` of every layer); `active-layer`: only the active pixel layer.\n */\n sample?: 'composite' | 'active-layer';\n /** Sample the adjusted image (base adjustments and layers) instead of the source pixels. */\n sampleMerged?: boolean;\n /** Pixels to use instead of reading the source image (natural size, RGBA). */\n pixels?: PixelBuffer;\n}\n\n/** A new adjustment layer: its kind plus optional initial values and position. */\nexport interface AdjustmentLayerInit<K extends AdjustmentKind = AdjustmentKind> extends AdjustmentLayerUpdate<K> {\n kind: K;\n id?: string;\n /** Put the new layer directly above this layer (in its group). */\n aboveId?: string;\n /** Put the new layer at the top of this group. */\n parentId?: string;\n}\n\n/** A new pixel layer: empty, a solid colour over the canvas, or given pixels, plus its properties and position. */\nexport interface PixelLayerInit extends LayerPlacement {\n id?: string;\n name?: string;\n /** Fill the canvas with this colour (`#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` 0..255). */\n color?: string | number[];\n /** RGBA pixels placed at `x`, `y` (document pixels). */\n pixels?: PixelBuffer;\n x?: number;\n y?: number;\n visible?: boolean;\n /** 0..1 (default 1). */\n opacity?: number;\n fillOpacity?: number;\n blend?: BlendMode;\n}\n\n/** The paint tools' options (view state). */\nexport interface PaintOptions {\n brush: BrushSettings;\n /** `#rrggbb`: what the brush and Fill paint. */\n foreground: string;\n /** `#rrggbb`: what the eraser paints on transparency-locked layers and masks. */\n background: string;\n /** Mixer Brush options (wet, load, mix, load / clean each stroke, RGB or pigment mixing). */\n mixer: MixerSettings;\n /** What the Mixer Brush carries now: its reservoir, how much is left and the picked-up colour. */\n mixerLoad: MixerLoad;\n}\n\n/** Paint tools a stroke can use. */\nexport type StrokeTool = 'brush' | 'eraser' | 'mixer';\n\n/** A stroke: tool, target (default the active layer and target) and brush / colour overrides. */\nexport interface StrokeInit {\n tool?: StrokeTool;\n layerId?: string;\n channel?: LayerTarget;\n /** Overrides of the paint options' brush. */\n brush?: Partial<BrushSettings>;\n /**\n * Paint colour (default the foreground). Masks take its grey value. For the mixer, the colour\n * the reservoir is loaded with: given, it loads the brush whatever `loadEachStroke` says.\n */\n color?: string | number[];\n /** Overrides of the paint options' Mixer Brush settings (tool `mixer`). */\n mixer?: Partial<MixerSettings>;\n /** Eraser colour on locked transparency / masks (default the background). */\n background?: string | number[];\n /** Kept with the stroke for replays (v1 brushes have no jitter). */\n seed?: number;\n}\n\nexport interface FillOptions {\n layerId?: string;\n channel?: LayerTarget;\n /** Default the foreground colour. */\n color?: string | number[];\n /** 0..1 (default 1). */\n opacity?: number;\n blend?: BlendMode;\n /** Limit to the selection when there is one (default true). */\n useSelection?: boolean;\n}\n\n/** What a stroke, fill or clear changed. */\nexport interface PaintStrokeResult {\n layerId: string;\n channel: LayerTarget;\n /** Document rectangle that changed (null when nothing did). */\n bounds: CropBox | null;\n dabs: number;\n /** The Mixer Brush's load after the stroke (tool `mixer` only). */\n mixerLoad?: MixerLoad;\n}\n\n/** An opaque pre-stroke history snapshot. */\nexport type HistoryEntryHandle = { readonly __historyEntry: unique symbol };\n\n/** Composites `coverage` into the tiles of `base` under `rects` (document space), returning the new surface. */\nfunction paintCoverageInto(base: TiledSurface, coverage: CoverageMap, apply: StrokeApply, rects: CropBox[]): TiledSurface {\n const tiles = new Map(base.tiles);\n const done = new Set<TileKey>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - apply.offset.x, y: r.y - apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n if (done.has(key)) continue;\n done.add(key);\n const src = base.tiles.get(key)?.data ?? defaultTile(base);\n const data = applyStrokeToTile(coverage, src, base.channels, t.tx, t.ty, apply);\n const tile = finishTile(data, base.channels, base.defaultValue);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n }\n return new TiledSurfaceClass(base.channels, tiles, base.defaultValue);\n}\n\nfunction defaultTile(s: TiledSurface): Uint8ClampedArray {\n const d = new Uint8ClampedArray(TILE * TILE * s.channels);\n if (s.defaultValue) d.fill(s.defaultValue);\n return d;\n}\n\n/** How a Mixer Brush stroke starts (see `PaintStrokeSession`). */\nexport interface MixerStrokeInit {\n /** Makes the stroke over the session's live pixels. */\n create(pixels: MixerTarget): MixerStroke;\n /** Stores the brush load when the stroke ends and returns it (after any clean). */\n end(load: MixerLoad): MixerLoad;\n}\n\n/**\n * A stroke in progress (see `beginStroke`): `push` input points as they arrive, then `end` (one\n * undo step) or `cancel`. Brush and eraser strokes build up a coverage map composited over the\n * pre-stroke pixels; Mixer Brush strokes paint dab by dab into the live pixels, so each dab picks\n * up what the dabs before it laid down.\n */\nexport class PaintStrokeSession {\n private readonly before: HistoryEntryHandle;\n private readonly base: TiledSurface;\n private readonly live: Map<TileKey, Tile>;\n private readonly touched = new Set<TileKey>();\n private readonly stroke: StrokeCoverage | null;\n private readonly mixer: MixerStroke | null;\n private readonly mixerInit: MixerStrokeInit | null = null;\n /** Mixer tiles written since the last update: they get new tile objects so the compositor sees them change. */\n private readonly fresh = new Set<TileKey>();\n private last: StrokePoint | null = null;\n private closed = false;\n\n constructor(\n private readonly controller: ImageEditorController,\n readonly target: { id: string; channel: LayerTarget; surface: TiledSurface },\n stroke: StrokeCoverage | MixerStrokeInit,\n private readonly apply: StrokeApply\n ) {\n this.before = controller._captureForStroke();\n this.base = target.surface;\n this.live = new Map(this.base.tiles);\n if (stroke instanceof StrokeCoverage) {\n this.stroke = stroke;\n this.mixer = null;\n } else {\n this.stroke = null;\n this.mixer = stroke.create(this.mixerPixels());\n this.mixerInit = stroke;\n }\n }\n\n /** The live pixels a mixer stroke reads and writes; base tiles are copied on first write. */\n private mixerPixels(): MixerTarget {\n const a = this.apply;\n return {\n offset: a.offset,\n lockTransparency: a.lockTransparency,\n selection: a.selection,\n readTile: (tx, ty) => this.live.get(tileKey(tx, ty))?.data ?? (this.base.defaultValue ? defaultTile(this.base) : null),\n writeTile: (tx, ty) => {\n const key = tileKey(tx, ty);\n if (!this.touched.has(key)) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n this.live.set(key, { data: new Uint8ClampedArray(src), uniform: null });\n this.touched.add(key);\n }\n this.fresh.add(key);\n return this.live.get(key)!.data;\n },\n };\n }\n\n get layerId(): string {\n return this.target.id;\n }\n\n /** Adds input points (document pixels); the stack shows the stroke so far. */\n push(points: readonly StrokePoint[]): void {\n if (this.closed || !points.length) return;\n this.last = points[points.length - 1];\n this.update(this.mixer ? this.mixer.push(points) : this.stroke!.push(points));\n }\n\n private update(rects: CropBox[]): void {\n if (!rects.length) return;\n const ch = this.base.channels;\n if (this.mixer) {\n for (const key of this.fresh) this.live.set(key, { data: this.live.get(key)!.data, uniform: null });\n this.fresh.clear();\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n return;\n }\n const stroke = this.stroke!;\n // Per target tile, the union of the new dabs inside it: only that part is recomputed.\n const regions = new Map<TileKey, { tx: number; ty: number; r: CropBox }>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n const cur = regions.get(key);\n regions.set(key, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r)! : r });\n }\n }\n for (const [key, { tx, ty, r }] of regions) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n const prev = this.touched.has(key) ? this.live.get(key)?.data : undefined;\n this.touched.add(key);\n const data = applyStrokeToTile(stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});\n this.live.set(key, { data, uniform: null });\n }\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n }\n\n /** Finishes the stroke (one undo step) and returns what changed. */\n end(): PaintStrokeResult {\n if (this.closed) throw new LayerOperationError('This stroke has already ended.');\n const last = this.last ?? undefined;\n this.update(this.mixer ? this.mixer.finish(last) : this.stroke!.finish(last));\n this.closed = true;\n const ch = this.base.channels;\n const tiles = new Map(this.base.tiles);\n let changed = false;\n for (const key of this.touched) {\n const live = this.live.get(key)!;\n const tile = finishTile(live.data, ch, this.base.defaultValue);\n const prev = this.base.tiles.get(key);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n if (!sameTile(prev, tile)) changed = true;\n }\n const surface = changed ? new TiledSurfaceClass(ch, tiles, this.base.defaultValue) : null;\n const mixerLoad = this.mixer ? this.mixerInit!.end(this.mixer.load) : undefined;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);\n const b = this.mixer ? this.mixer.bounds : this.stroke!.coverage.bounds;\n const result: PaintStrokeResult = { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: (this.mixer ?? this.stroke!).dabCount };\n if (mixerLoad) result.mixerLoad = mixerLoad;\n return result;\n }\n\n /** Abandons the stroke and restores the layer. */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);\n }\n}\n\nfunction sameTile(a: Tile | undefined, b: Tile | null): boolean {\n if (!a || !b) return !a && !b;\n if (a === b) return true;\n if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;\n for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;\n return true;\n}\n\n/** Where a new annotation goes. */\nexport interface AnnotationPlacement {\n /** Put the annotation layer directly above this layer (default: the top of the stack). */\n aboveId?: string;\n}\n\n/** The image a history entry was recorded against. */\ninterface SourceSnapshot {\n sourceUrl: string | null;\n element: CanvasImageSource | null;\n dimensions: ImageDimensions;\n}\n\n/**\n * Undo/redo entry: the document as immutable objects. Layers, surfaces, tiles and selections are\n * shared with the other entries and the current state, so an entry costs about the tiles it changed.\n */\ninterface HistoryEntry {\n layers: ImageLayer[];\n activeLayerId: string | null;\n activeTarget: LayerTarget;\n crop: CropBox | null;\n transform: ImageTransform;\n outputOpacity: number;\n legacyOutputBlur?: number;\n globalLight?: GlobalLight;\n patterns?: EffectPattern[];\n selection: SelectionMask | null;\n source: SourceSnapshot;\n}\n\nexport const DEFAULT_ADJUSTMENTS: ImageAdjustments = DEFAULT_ADJUSTMENTS_VALUES;\n\n/**\n * Fills in defaults for stored adjustments. A stored object without `process` was saved before\n * process versions existed and keeps the legacy rendering.\n */\nexport function normalizeStoredAdjustments(stored: Partial<ImageAdjustments> | undefined | null): ImageAdjustments {\n if (!stored) return { ...DEFAULT_ADJUSTMENTS };\n const out: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS,\n ...stored,\n process: normalizeProcessVersion(stored.process),\n };\n // Layers are deep-copied and cleaned; an empty or missing list is left out entirely.\n const layers = normalizeAdjustmentLayers(stored.layers);\n if (layers.length) out.layers = layers;\n else delete out.layers;\n return out;\n}\n\nconst isLayered = (data: { version?: unknown } | null | undefined): data is SerializedLayeredImageEditorState =>\n Boolean(data && data.version === LAYERED_SCHEMA_VERSION && Array.isArray((data as { layers?: unknown }).layers));\n\n/**\n * Brings a stored editor state of any schema version to a clean state of the same family:\n * version 1 (and a missing version) has no adjustment layers, version 2 has them, and both stay\n * in the document-level schema so old states load and render exactly as before. Version 3\n * (layers) is returned with its document fields cleaned. To lift a v1 / v2 state into layers,\n * use {@link migrateToLayeredState}.\n */\nexport function migrateSerializedImageEditorState(\n data: Partial<SerializedImageEditorState> & { version?: number }\n): SerializedImageEditorState {\n if (isLayered(data)) {\n const out: SerializedLayeredImageEditorState = {\n ...data,\n crop: data.crop ? { ...data.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...data.transform },\n opacity: typeof data.opacity === 'number' ? Math.min(1, Math.max(0, data.opacity)) : 1,\n assets: { ...(data.assets ?? {}) },\n activeLayerId: typeof data.activeLayerId === 'string' ? data.activeLayerId : null,\n };\n return out;\n }\n const legacy = data as Partial<LegacySerializedImageEditorState>;\n const adjustments = normalizeStoredAdjustments(legacy.adjustments);\n return {\n version: adjustments.layers?.length ? 2 : 1,\n crop: legacy.crop ? { ...legacy.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...legacy.transform },\n adjustments: serializeAdjustments(adjustments),\n depthMask: {\n ...DEFAULT_DEPTH_MASK,\n ...legacy.depthMask,\n depthRange: [...(legacy.depthMask?.depthRange ?? DEFAULT_DEPTH_MASK.depthRange)] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(legacy.annotations ?? [])),\n ...(legacy.selection ? { selection: { ...legacy.selection } } : {}),\n };\n}\n\n/**\n * Lifts a stored state of any version into the layered schema (v3) for a canvas of\n * `width × height`: the source becomes the Background layer with the base sliders as its\n * `develop` filter and the depth mask as its parametric mask, adjustment layers go above it and\n * annotations on top (see `layersFromLegacy`).\n */\nexport function migrateToLayeredState(\n data: Partial<SerializedImageEditorState> & { version?: number },\n size: ImageDimensions\n): SerializedLayeredImageEditorState {\n const clean = migrateSerializedImageEditorState(data);\n if (isLayered(clean)) return clean;\n const stack = layersFromLegacy(clean, Math.round(size.width), Math.round(size.height));\n const { layers, assets } = serializeLayers(stack.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width: Math.round(size.width),\n height: Math.round(size.height),\n layers,\n assets,\n activeLayerId: null,\n crop: clean.crop,\n transform: clean.transform,\n opacity: stack.outputOpacity,\n };\n if (stack.legacyOutputBlur !== undefined) out.legacyOutputBlur = stack.legacyOutputBlur;\n if (clean.selection) out.selection = clean.selection;\n return out;\n}\n\nexport const DEFAULT_TRANSFORM: ImageTransform = DEFAULT_TRANSFORM_VALUES;\n\nexport const DEFAULT_DEPTH_MASK: DepthMaskOptions = DEFAULT_DEPTH_MASK_VALUES;\n\nconst DEVELOP_KEYS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/**\n * ImageEditorController manages the complete lifecycle, canvas rendering pipeline,\n * tool interaction states, undo/redo history, serialization, and image export.\n */\nexport class ImageEditorController {\n private state: ImageEditorState;\n private renderer: ImageEditorRenderer;\n private imageElement: CanvasImageSource | null = null;\n private listeners = new Set<(state: ImageEditorState) => void>();\n private histogramListeners = new Set<(histogram: ImageHistogram | null) => void>();\n private onStateChange?: (state: ImageEditorState) => void;\n\n // History stacks\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private maxHistoryDepth: number;\n private maxHistoryBytes: number;\n private isApplyingHistory = false;\n private initialSnapshot: HistoryEntry;\n /** Pixels the selection tools sample (host-provided or read back from the source). */\n private samplePixels: PixelBuffer | null = null;\n private samplePixelsOverride = false;\n /** Decoded source pixels (the Background's surface), by the buffer or element they came from. */\n private sourceSurfaces = new WeakMap<object, PixelSurface>();\n private viewKey: unknown[] = [];\n /** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */\n private compositor = new LayerCompositor();\n\n constructor(options: ImageEditorOptions = {}) {\n this.maxHistoryDepth = options.maxHistoryDepth ?? 50;\n this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;\n this.onStateChange = options.onStateChange;\n this.renderer = new ImageEditorRenderer();\n this.renderer.onPreviewHistogram = (histogram) => {\n this.histogramListeners.forEach((l) => l(histogram));\n };\n this.renderer.layerSources = {\n surfaceOf: (layer) => this.surfaceOf(layer),\n sourceIsOpaque: () => this.sourceIsOpaque(),\n version: () => `${this.sourceVersion()}:${this.patternsVersion()}`,\n globalLight: () => this.state.globalLight,\n pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null,\n };\n\n this.state = {\n sourceUrl: typeof options.image === 'string' ? options.image : null,\n imageDimensions: { width: 800, height: 600 },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM },\n adjustments: { ...DEFAULT_ADJUSTMENTS },\n depthMask: { ...DEFAULT_DEPTH_MASK },\n annotations: [],\n selectedAnnotationId: null,\n activeTool: 'select',\n zoom: 1,\n pan: { x: 0, y: 0 },\n isOriginalCompared: false,\n selection: null,\n quickMask: false,\n layers: [],\n activeLayerId: null,\n activeTarget: 'pixels',\n outputOpacity: 1,\n paint: { brush: { ...DEFAULT_BRUSH }, foreground: '#000000', background: '#ffffff', mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD },\n fit: options.fit ?? 'margin',\n };\n\n if (options.image instanceof Object && 'width' in options.image) {\n this.imageElement = options.image as CanvasImageSource;\n this.state.sourceUrl = (options.image as any).src || null;\n this.state.imageDimensions = {\n width: (options.image as any).naturalWidth || (options.image as any).width || 800,\n height: (options.image as any).naturalHeight || (options.image as any).height || 600,\n };\n }\n\n this.applyStored(options.initialState ?? {});\n if (options.initialState?.selection) {\n try {\n this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));\n } catch {\n // A damaged selection is dropped; the rest of the state still loads.\n }\n }\n\n this.initialSnapshot = this.captureHistoryEntry();\n\n if (typeof options.image === 'string') {\n this.loadImage(options.image);\n }\n }\n\n // --- Image Loading ---\n\n /**\n * Loads a new source image and makes it the baseline that `reset()` returns to.\n * Does not record history; use `applyInpaintedImage` / `applyBackgroundRemovedImage`\n * for undoable source replacement.\n */\n public async loadImage(src: string | HTMLImageElement): Promise<void> {\n // Keep the URL visible synchronously (UI bindings compare against it while loading).\n this.abandonContents();\n if (typeof src === 'string') this.state.sourceUrl = src;\n const source = await this.resolveSource(src);\n const prev = this.state.imageDimensions;\n this.imageElement = source.element;\n this.state.sourceUrl = source.sourceUrl;\n this.state.imageDimensions = source.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (prev.width !== source.dimensions.width || prev.height !== source.dimensions.height) {\n // Masks lifted from v1 / v2 documents follow the image, as they did when they were resampled at render.\n this.state.layers = resizeLegacyMasks(this.state.layers, prev, source.dimensions);\n }\n this.dropMismatchedSelection();\n\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n }\n\n private async resolveSource(src: string | HTMLImageElement): Promise<SourceSnapshot> {\n if (typeof src === 'string') {\n try {\n const img = await this.renderer.imageCache.get(src);\n return {\n sourceUrl: src,\n element: img,\n dimensions: {\n width: img.naturalWidth || img.width || 800,\n height: img.naturalHeight || img.height || 600,\n },\n };\n } catch {\n // In SSR / non-DOM environments, retain current dimensions\n return { sourceUrl: src, element: null, dimensions: { ...this.state.imageDimensions } };\n }\n }\n return {\n sourceUrl: (src as any).src || null,\n element: src,\n dimensions: {\n width: src.naturalWidth || src.width || 800,\n height: src.naturalHeight || src.height || 600,\n },\n };\n }\n\n public getImageElement(): CanvasImageSource | null {\n return this.imageElement;\n }\n\n // --- State & Subscription ---\n\n public getState(): ImageEditorState {\n this.syncViews();\n return this.state;\n }\n\n public subscribe(listener: (state: ImageEditorState) => void): () => void {\n this.syncViews();\n this.listeners.add(listener);\n listener(this.state);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Where layer pixels changed since the last viewport render, when only strokes changed them\n * (the renderer then recomposites just those rectangles); `'all'` after any other change.\n */\n private pendingDirty: CropBox[] | 'all' = 'all';\n\n private notify(strokeRects?: CropBox[]): void {\n if (strokeRects && this.pendingDirty !== 'all') this.pendingDirty.push(...strokeRects);\n else if (!strokeRects) this.pendingDirty = 'all';\n this.syncViews();\n const currentState = { ...this.state };\n this.onStateChange?.(currentState);\n this.listeners.forEach((l) => l(currentState));\n }\n\n /** Recomputes the compatibility views (`adjustments`, `depthMask`, `annotations`) when the stack changed. */\n private syncViews(): void {\n const s = this.state;\n const key = [s.layers, s.outputOpacity, s.legacyOutputBlur, s.activeLayerId, s.imageDimensions.width, s.imageDimensions.height];\n if (key.length === this.viewKey.length && key.every((v, i) => v === this.viewKey[i])) return;\n this.viewKey = key;\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, s.activeLayerId);\n s.adjustments = view.adjustments;\n s.depthMask = view.depthMask;\n s.annotations = view.annotations;\n }\n\n private stack(): LayerStack {\n const out: LayerStack = { layers: this.state.layers, outputOpacity: this.state.outputOpacity };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n return out;\n }\n\n // --- History (Undo / Redo / Reset) ---\n\n private captureHistoryEntry(): HistoryEntry {\n const s = this.state;\n const entry: HistoryEntry = {\n layers: s.layers,\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n crop: s.crop ? { ...s.crop } : null,\n transform: { ...s.transform },\n outputOpacity: s.outputOpacity,\n selection: s.selection ?? null,\n source: {\n sourceUrl: s.sourceUrl,\n element: this.imageElement,\n dimensions: { ...s.imageDimensions },\n },\n };\n if (s.legacyOutputBlur !== undefined) entry.legacyOutputBlur = s.legacyOutputBlur;\n if (s.globalLight) entry.globalLight = { ...s.globalLight };\n if (s.patterns) entry.patterns = s.patterns;\n return entry;\n }\n\n private restoreHistoryEntry(entry: HistoryEntry): void {\n const s = this.state;\n s.layers = entry.layers;\n s.activeLayerId = entry.activeLayerId;\n s.activeTarget = entry.activeTarget;\n s.crop = entry.crop ? { ...entry.crop } : null;\n s.transform = { ...entry.transform };\n s.outputOpacity = entry.outputOpacity;\n if (entry.legacyOutputBlur !== undefined) s.legacyOutputBlur = entry.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n if (entry.globalLight) s.globalLight = { ...entry.globalLight };\n else delete s.globalLight;\n if (entry.patterns) s.patterns = entry.patterns;\n else delete s.patterns;\n s.selection = entry.selection;\n if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;\n this.imageElement = entry.source.element;\n s.sourceUrl = entry.source.sourceUrl;\n s.imageDimensions = { ...entry.source.dimensions };\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) {\n s.selectedAnnotationId = null;\n }\n }\n\n private recordHistory(): void {\n if (this.isApplyingHistory) return;\n this.undoStack.push(this.captureHistoryEntry());\n if (this.undoStack.length > this.maxHistoryDepth) {\n this.undoStack.shift();\n }\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n\n /**\n * Bytes of pixel tiles held only by undo / redo steps (not by the current state). The oldest\n * undo steps are dropped while it exceeds `maxHistoryBytes`.\n */\n public get historyBytes(): number {\n const live = new Set<Tile>();\n collectStateTiles(this.state.layers, live);\n const held = new Set<Tile>();\n for (const e of [...this.undoStack, ...this.redoStack]) collectStateTiles(e.layers, held);\n let n = 0;\n for (const t of held) if (!live.has(t)) n += t.data.byteLength;\n return n;\n }\n\n private trimHistoryBytes(): void {\n if (!(this.maxHistoryBytes > 0) || this.undoStack.length <= 1) return;\n while (this.undoStack.length > 1 && this.historyBytes > this.maxHistoryBytes) this.undoStack.shift();\n }\n\n public get canUndo(): boolean {\n return this.undoStack.length > 0;\n }\n\n public get canRedo(): boolean {\n return this.redoStack.length > 0;\n }\n\n public undo(): boolean {\n if (!this.canUndo) return false;\n this.isApplyingHistory = true;\n this.redoStack.push(this.captureHistoryEntry());\n\n const prev = this.undoStack.pop()!;\n this.restoreHistoryEntry(prev);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public redo(): boolean {\n if (!this.canRedo) return false;\n this.isApplyingHistory = true;\n this.undoStack.push(this.captureHistoryEntry());\n\n const next = this.redoStack.pop()!;\n this.restoreHistoryEntry(next);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public reset(): void {\n this.recordHistory();\n this.restoreHistoryEntry(this.initialSnapshot);\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.state.activeTool = 'select';\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n // --- Active Tool ---\n\n public setTool(tool: ImageEditorTool): void {\n if (this.state.activeTool === tool) return;\n this.state.activeTool = tool;\n if (tool !== 'select') {\n this.state.selectedAnnotationId = null;\n }\n // If switching to crop tool and no crop is defined, initialize crop to full image\n if (tool === 'crop' && !this.state.crop) {\n this.state.crop = {\n x: 0,\n y: 0,\n width: this.state.imageDimensions.width,\n height: this.state.imageDimensions.height,\n };\n }\n this.notify();\n }\n\n // --- Crop ---\n\n public setCrop(crop: CropBox | null, addToHistory = true): void {\n if (addToHistory) {\n this.recordHistory();\n }\n this.state.crop = crop;\n this.notify();\n }\n\n public applyCropPreset(ratio: AspectRatio): void {\n this.recordHistory();\n const { width, height } = this.state.imageDimensions;\n // `ratio` is the on-screen aspect; the crop box lives in natural image space.\n if (ratio && isQuarterTurnSwapped(this.state.transform)) {\n ratio = 1 / ratio;\n }\n if (!ratio) {\n // Freeform: full image\n this.state.crop = { x: 0, y: 0, width, height };\n } else {\n let targetW = width;\n let targetH = targetW / ratio;\n\n if (targetH > height) {\n targetH = height;\n targetW = targetH * ratio;\n }\n\n const x = Math.max(0, Math.round((width - targetW) / 2));\n const y = Math.max(0, Math.round((height - targetH) / 2));\n this.state.crop = {\n x,\n y,\n width: Math.round(targetW),\n height: Math.round(targetH),\n };\n }\n this.notify();\n }\n\n public resetCrop(): void {\n this.recordHistory();\n this.state.crop = null;\n this.notify();\n }\n\n // --- Transformations (Rotate / Flip) ---\n\n public rotate(degrees: number): void {\n this.recordHistory();\n const current = this.state.transform.rotate;\n this.state.transform = { ...this.state.transform, rotate: (current + degrees) % 360 };\n this.notify();\n }\n\n public setRotation(degrees: number): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, rotate: degrees % 360 };\n this.notify();\n }\n\n public flipHorizontal(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipH: !this.state.transform.flipH };\n this.notify();\n }\n\n public flipVertical(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipV: !this.state.transform.flipV };\n this.notify();\n }\n\n // --- Layers (read) ---\n\n /** The layer stack, bottom to top (immutable objects). */\n public getLayers(): ImageLayer[] {\n return this.state.layers;\n }\n\n public getLayer(id: string): ImageLayer | undefined {\n return findLayer(this.state.layers, id)?.layer;\n }\n\n /** The active layer, or `undefined`. */\n public getActiveLayer(): ImageLayer | undefined {\n return this.state.activeLayerId ? this.getLayer(this.state.activeLayerId) : undefined;\n }\n\n /**\n * Makes a layer the active one (or none) and chooses whether painting targets its pixels or\n * its mask. View state: no undo step. Returns false for an unknown id.\n */\n public setActiveLayer(id: string | null, target: LayerTarget = 'pixels'): boolean {\n if (id !== null && !this.getLayer(id)) return false;\n const layer = id ? this.getLayer(id) : undefined;\n const t: LayerTarget = target === 'mask' && layer?.mask ? 'mask' : target === 'filter-mask' && layer?.type === 'smart' && layer.smartFilters?.mask ? 'filter-mask' : 'pixels';\n if (this.state.activeLayerId === id && this.state.activeTarget === t) return true;\n this.state.activeLayerId = id;\n this.state.activeTarget = t;\n this.notify();\n return true;\n }\n\n /**\n * The pixels of a pixel layer, in document space (`x`, `y` is the top-left of the returned\n * buffer), or `null` for an unknown or empty layer. Resolves the Background's source pixels.\n */\n public getLayerPixels(id: string): (PixelBuffer & { x: number; y: number }) | null {\n const layer = this.getLayer(id);\n if (!layer || (layer.type !== 'pixel' && layer.type !== 'smart')) return null;\n const surface = this.surfaceOf(layer);\n const b = contentBounds(surface);\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height, data: readRegion(surface, b) };\n }\n\n private boundsCache = new WeakMap<object, CropBox | null>();\n\n /**\n * Document bounds of a layer's non-transparent pixels (pixel layers; the Background's source\n * included), or `null` for an empty or non-pixel layer.\n */\n public getLayerBounds(id: string): CropBox | null {\n const layer = this.getLayer(id);\n if (!layer || (layer.type !== 'pixel' && layer.type !== 'smart')) return null;\n const surface = this.surfaceOf(layer);\n let b = this.boundsCache.get(surface);\n if (b === undefined) {\n b = contentBounds(surface);\n this.boundsCache.set(surface, b);\n }\n return b ? { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height } : null;\n }\n\n /**\n * Composites the layer stack in document space without a canvas (annotation layers need one\n * and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and\n * output opacity are not applied. With `layerIds`, only those layers are composited.\n */\n public compositePixels(rect?: CropBox, options: { layerIds?: string[] } = {}): PixelBuffer {\n const { width, height } = this.naturalSize;\n const r = rect ?? { x: 0, y: 0, width, height };\n let layers = this.state.layers;\n if (options.layerIds) {\n const keep = new Set(options.layerIds);\n layers = flattenLayers(layers).filter((l) => keep.has(l.id) && l.type !== 'group');\n }\n const data = this.compositor.composite(layers, r, this.compositorSources());\n return { width: r.width, height: r.height, data };\n }\n\n private compositorSources(): CompositorSources {\n const { width, height } = this.naturalSize;\n return {\n width,\n height,\n surfaceOf: (l) => this.surfaceOf(l),\n version: `${this.sourceVersion()}:${this.patternsVersion()}`,\n globalLight: this.state.globalLight,\n pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null,\n };\n }\n\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 1;\n\n /** Changes whenever the Background's source pixels can have changed. */\n private sourceVersion(): string {\n const key = (this.samplePixelsOverride ? this.samplePixels : this.imageElement ?? this.samplePixels) as object | null;\n if (!key) return '0';\n let id = this.sourceIds.get(key);\n if (!id) {\n id = this.nextSourceId++;\n this.sourceIds.set(key, id);\n }\n return String(id);\n }\n\n /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */\n private surfaceOf(layer: PixelLayer | SmartObjectLayer): PixelSurface {\n if (layer.type === 'smart' || layer.source !== 'document') return layer.surface;\n return this.sourceSurface() ?? layer.surface;\n }\n\n /**\n * The source image as a surface (cached): host-supplied sample pixels first, then the decoded\n * element, then pixels read back for the selection tools.\n */\n private sourceSurface(): PixelSurface | null {\n const { width, height } = this.naturalSize;\n const fromPixels = (px: PixelBuffer) => {\n let s = this.sourceSurfaces.get(px as object);\n if (!s) {\n s = surfaceFromPixels(px, 4);\n this.sourceSurfaces.set(px as object, s);\n }\n return s;\n };\n const px = this.samplePixels;\n const fits = Boolean(px && px.width === width && px.height === height);\n if (this.samplePixelsOverride && fits) return fromPixels(px!);\n const el = this.imageElement as unknown as object | null;\n if (el) {\n const cached = this.sourceSurfaces.get(el);\n if (cached) return cached;\n const decoded = this.decodeSource();\n if (decoded) {\n if (!this.samplePixelsOverride) this.samplePixels = decoded;\n const s = surfaceFromPixels(decoded, 4);\n this.sourceSurfaces.set(el, s);\n this.sourceSurfaces.set(decoded as object, s);\n return s;\n }\n }\n return fits ? fromPixels(px!) : null;\n }\n\n private opaqueCache = new WeakMap<object, boolean>();\n\n /** Whether every source pixel is opaque (some legacy renders depend on it). */\n private sourceIsOpaque(): boolean {\n const s = this.sourceSurface();\n if (!s) return true;\n let v = this.opaqueCache.get(s);\n if (v === undefined) {\n const { width, height } = this.naturalSize;\n v = isOpaqueInside(s, width, height);\n this.opaqueCache.set(s, v);\n }\n return v;\n }\n\n /** Synchronously reads the source element's pixels through a canvas, or `null`. */\n private decodeSource(): PixelBuffer | null {\n const el = this.imageElement;\n if (!el) return null;\n const { width, height } = this.naturalSize;\n try {\n const canvas = this.renderer.createCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n ctx.drawImage(el, 0, 0, width, height);\n const data = ctx.getImageData(0, 0, width, height);\n if (!data?.data || data.data.length < width * height * 4) return null;\n return { width, height, data: data.data };\n } catch {\n return null;\n }\n }\n\n // --- Layer operations ---\n\n /** Records one undo step and applies a new stack (and any other state change in `extra`). */\n private commitLayers(next: ImageLayer[], extra?: () => void, commitToHistory = true): void {\n if (commitToHistory) this.recordHistory();\n this.state.layers = next;\n extra?.();\n if (this.state.activeLayerId && !findLayer(next, this.state.activeLayerId)) {\n this.state.activeLayerId = null;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n }\n\n /** The default position of a new layer: above the active layer, else the top of the stack. */\n private newLayerPlacement(placement: LayerPlacement): LayerPlacement {\n if (placement.aboveId || placement.belowId || placement.parentId || placement.index !== undefined) return placement;\n const active = this.state.activeLayerId ? findLayer(this.state.layers, this.state.activeLayerId) : null;\n return active ? { aboveId: active.layer.id } : {};\n }\n\n /**\n * Adds a pixel layer (one undo step) and makes it active: empty, filled with `color` over the\n * canvas, or holding `pixels` placed at `x`, `y`. Goes above the active layer unless placed.\n * Returns its id.\n */\n public addPixelLayer(init: PixelLayerInit = {}): string {\n const { width, height } = this.naturalSize;\n let surface = emptySurface(4);\n if (init.pixels) {\n const px = init.pixels;\n if (!(px.width > 0 && px.height > 0) || px.data.length < px.width * px.height * 4) {\n throw new LayerOperationError('pixels must be RGBA with width × height × 4 bytes.');\n }\n surface = surfaceFromPixels(px, 4);\n } else if (init.color !== undefined) {\n const c = parseColor(init.color);\n surface = solidSurface(4, { x: 0, y: 0, width, height }, packRgba(c[0], c[1], c[2], c[3]));\n }\n const blend = parseBlend(init.blend) as BlendMode | undefined;\n if (init.opacity !== undefined && !(init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer: PixelLayer = {\n type: 'pixel',\n id: init.id && !ids.has(init.id) ? init.id : newLayerId(),\n name: init.name?.trim() || this.nextLayerName(),\n visible: init.visible ?? true,\n opacity: init.opacity ?? 1,\n blend: blend ?? 'normal',\n surface,\n offset: { x: Math.round(init.x ?? 0), y: Math.round(init.y ?? 0) },\n };\n if (init.fillOpacity !== undefined && init.fillOpacity !== 1) layer.fillOpacity = init.fillOpacity;\n const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = layer.id;\n this.state.activeTarget = 'pixels';\n });\n return layer.id;\n }\n\n /**\n * `addPixelLayer` from an image: a PNG data URL or base64 PNG is decoded without a canvas;\n * other formats and URLs need a browser. Returns the new layer id.\n */\n public async addPixelLayerFromImage(image: string, init: Omit<PixelLayerInit, 'pixels' | 'color'> = {}): Promise<string> {\n const pixels = await this.decodeImage(image);\n return this.addPixelLayer({ ...init, pixels });\n }\n\n /** RGBA pixels of an image given as a data URL, base64 PNG or URL. */\n public async decodeImage(image: string): Promise<PixelBuffer> {\n const trimmed = image.trim();\n let png: Uint8Array | null = null;\n if (/^data:image\\/png/i.test(trimmed)) png = parseDataUrl(trimmed)?.bytes ?? null;\n else if (/^[A-Za-z0-9+/=\\s]+$/.test(trimmed) && trimmed.length > 16) {\n try {\n const bytes = base64ToBytes(trimmed);\n if (isPng(bytes)) png = bytes;\n } catch {\n png = null;\n }\n }\n if (png) {\n try {\n const d = decodePng(png);\n return { width: d.width, height: d.height, data: d.data };\n } catch {\n // Fall back to the browser decoder below (interlaced PNGs).\n }\n }\n const src = png ? `data:image/png;base64,${bytesToBase64(png)}` : trimmed;\n let img: HTMLImageElement;\n try {\n img = await this.renderer.imageCache.get(src);\n } catch {\n throw new LayerOperationError('The image could not be decoded here. Pass a PNG (data URL or base64), or run in a browser for other formats and URLs.');\n }\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const ctx = this.renderer.createCanvas(w, h).getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') throw new LayerOperationError('Decoding this image needs a canvas; pass a PNG instead.');\n ctx.drawImage(img, 0, 0, w, h);\n const data = ctx.getImageData(0, 0, w, h);\n return { width: w, height: h, data: data.data };\n }\n\n private nextLayerName(): string {\n const names = new Set(flattenLayers(this.state.layers).map((l) => l.name));\n let n = 1;\n while (names.has(`Layer ${n}`)) n++;\n return `Layer ${n}`;\n }\n\n /** Duplicates a layer (with its children) directly above it, as one undo step. Returns the copy's id. */\n public duplicateLayer(id: string, name?: string): string {\n const res = duplicateLayerOp(this.state.layers, id, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Deletes a layer (refused when locked), as one undo step. */\n public removeLayer(id: string): void {\n const next = removeLayerOp(this.state.layers, id);\n this.commitLayers(next, () => {\n if (this.state.selectedAnnotationId && !flattenLayers(next).some((l) => l.type === 'annotation' && l.annotation.id === this.state.selectedAnnotationId)) {\n this.state.selectedAnnotationId = null;\n }\n });\n }\n\n /** Moves a layer above / below another, or into a group, as one undo step. Returns its new parent and index. */\n public moveLayer(id: string, placement: LayerPlacement): { parentId: string | null; index: number } {\n const next = moveLayerOp(this.state.layers, id, placement);\n const loc = findLayer(next, id)!;\n if (next === this.state.layers) return { parentId: loc.parent?.id ?? null, index: loc.index };\n this.commitLayers(next);\n return { parentId: loc.parent?.id ?? null, index: loc.index };\n }\n\n /**\n * Changes name, visibility, opacity, fill, blend, clipping, locks or (groups) collapsed state, as\n * one undo step (`commitToHistory = false` for the moves of a slider drag). Returns the layer.\n */\n public setLayerProperties(id: string, props: LayerProperties, commitToHistory = true): ImageLayer {\n const next = setLayerPropertiesOp(this.state.layers, id, props);\n this.commitLayers(next, undefined, commitToHistory);\n return findLayer(next, id)!.layer;\n }\n\n /** Adds an empty pass-through group above the active layer (or at `placement`) and makes it active. Returns its id. */\n public addGroup(init: LayerPlacement & { name?: string; blend?: LayerBlendMode } = {}): string {\n const blend = init.blend === undefined ? 'pass-through' : (parseBlend(init.blend, true) as LayerBlendMode);\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name: init.name?.trim() || 'Group',\n visible: true,\n opacity: 1,\n blend,\n children: [],\n };\n const next = placeLayer(this.state.layers, group, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = group.id;\n this.state.activeTarget = 'pixels';\n });\n return group.id;\n }\n\n /** Puts sibling layers into a new group (one undo step). Returns the group id. */\n public groupLayers(ids: string[], name?: string): string {\n const res = groupLayersOp(this.state.layers, ids, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Replaces a group by its children (one undo step). Returns the child ids, bottom to top. */\n public ungroupLayer(id: string): string[] {\n const res = ungroupLayerOp(this.state.layers, id);\n this.commitLayers(res.layers);\n return res.ids;\n }\n\n /**\n * Moves a pixel layer to an absolute offset (`x`, `y`) or by `dx`, `dy`, as one undo step\n * (`commitToHistory = false` for the moves of a drag). Returns the offset and document bounds.\n */\n public offsetLayer(id: string, move: { x?: number; y?: number; dx?: number; dy?: number }, commitToHistory = true): { offset: { x: number; y: number }; bounds: CropBox | null } {\n const layer = requireLayer(this.state.layers, id).layer;\n if (layer.type === 'smart') {\n // A smart object moves through its placement: x, y is where its top-left corner goes.\n const tl = layer.placement.corners[0];\n const dx = move.x !== undefined ? move.x - tl.x : (move.dx ?? 0);\n const dy = move.y !== undefined ? move.y - tl.y : (move.dy ?? 0);\n if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError('x and y must be numbers.');\n this.commitLayers(offsetSmartObject(this.state.layers, id, dx, dy, this.canvasSize), undefined, commitToHistory);\n const moved = findLayer(this.state.layers, id)!.layer as SmartObjectLayer;\n return { offset: { x: Math.round(moved.placement.corners[0].x), y: Math.round(moved.placement.corners[0].y) }, bounds: this.getLayerBounds(id) };\n }\n const current = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const x = move.x ?? current.x + (move.dx ?? 0);\n const y = move.y ?? current.y + (move.dy ?? 0);\n const next = offsetLayerOp(this.state.layers, id, { x, y });\n this.commitLayers(next, undefined, commitToHistory);\n return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };\n }\n\n /**\n * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black) or\n * `selection` (the selection's coverage). Replaces an existing mask. Makes the mask the paint\n * target when the layer is active.\n */\n public addLayerMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' = 'reveal-all', props: LayerMaskProps = {}): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n let mask: RasterMask;\n if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n if (layer.type === 'pixel' && (layer.offset.x || layer.offset.y)) {\n // Masks live in the layer's space (they move with it).\n mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };\n }\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const next = setLayerMaskOp(this.state.layers, id, mask);\n this.commitLayers(next, () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'mask';\n });\n return mask;\n }\n\n /** Removes a layer's mask (one undo step). */\n public removeLayerMask(id: string): void {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, null), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */\n public setLayerMaskProps(id: string, props: LayerMaskProps, commitToHistory = true): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n const mask = withMaskProps(layer.mask, props);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, mask), undefined, commitToHistory);\n return mask;\n }\n\n /**\n * Bakes a pixel layer's mask into its pixels (alpha × coverage, including density, feather or\n * the depth map) and removes the mask, as one undo step.\n */\n public async applyLayerMask(id: string): Promise<void> {\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Only a pixel layer's mask can be applied to its pixels; \"${id}\" is a ${layer.type} layer. Remove the mask or keep it.`);\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n requireUnlocked(layer, 'pixels', 'apply its mask');\n const { width, height } = this.naturalSize;\n await this.renderer.prepareLayerAssets([layer], width, height);\n const sources = this.renderer.compositorSources(width, height);\n const surface = this.surfaceOf(layer);\n let out = surface;\n const b = surface.bounds;\n if (b) {\n const draft = new SurfaceDraft(surface);\n for (const t of tilesInRect(b)) {\n const data = draft.peek(t.tx, t.ty);\n if (!data) continue;\n const r = { x: t.tx * TILE + layer.offset.x, y: t.ty * TILE + layer.offset.y, width: TILE, height: TILE };\n const cov = this.compositor.maskCoverage(layer, r, sources);\n if (!cov) continue;\n const d = draft.tile(t.tx, t.ty);\n for (let p = 0, i = 3; p < cov.length; p++, i += 4) d[i] = Math.round(d[i] * cov[p]);\n }\n out = draft.commit();\n }\n const next: PixelLayer = { ...layer, surface: out };\n delete next.mask;\n delete next.source;\n this.commitLayers(mapLayer(this.state.layers, id, () => next), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /**\n * Layer via Copy (or via Cut): a new pixel layer with the selected pixels of a pixel layer (the\n * active one by default), directly above it, as one undo step. Returns the new id.\n */\n public layerViaCopy(options: { layerId?: string; cut?: boolean; name?: string } = {}): string {\n const id = options.layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError('No layer given and none is active; pass layerId (see list_layers).');\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer via copy needs a pixel layer; \"${id}\" is a ${layer.type} layer.`);\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select the pixels to copy first.');\n if (options.cut) requireUnlocked(layer, 'pixels', 'cut from it');\n const surface = this.surfaceOf(layer);\n const sb = sel.bounds;\n const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, src);\n const kept = options.cut ? new Uint8ClampedArray(px) : null;\n for (let y = 0; y < sb.height; y++) {\n for (let x = 0; x < sb.width; x++) {\n const c = sel.data[(sb.y + y) * sel.width + sb.x + x];\n const i = (y * sb.width + x) * 4 + 3;\n if (kept) kept[i] = Math.round(kept[i] * (1 - c / 255));\n px[i] = Math.round(px[i] * (c / 255));\n }\n }\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: options.name?.trim() || `${layer.name || 'Layer'} ${options.cut ? 'cut' : 'copy'}`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width: src.width, height: src.height, data: px }, 4, src.x, src.y),\n offset: { ...layer.offset },\n };\n let next = insertLayer(this.state.layers, copy, loc.parent?.id ?? null, loc.index + 1);\n if (kept) {\n const cutLayer: PixelLayer = { ...layer, surface: writeRegion(surface, src, kept) };\n delete cutLayer.source;\n next = mapLayer(next, id, () => cutLayer);\n }\n this.commitLayers(next, () => {\n this.state.activeLayerId = copy.id;\n this.state.activeTarget = 'pixels';\n });\n return copy.id;\n }\n\n // --- Free Transform ---\n\n private openTransform: FreeTransformSession | null = null;\n\n /** The Free Transform in progress (see `beginFreeTransform`), or `null`. */\n public getFreeTransform(): FreeTransformSession | null {\n if (this.openTransform && !this.openTransform.isOpen) this.openTransform = null;\n return this.openTransform;\n }\n\n private transformHost(): FreeTransformHost {\n return {\n layers: () => this.state.layers,\n activeLayerId: () => this.state.activeLayerId,\n activeTarget: () => this.state.activeTarget,\n selection: () => (this.state.selection?.bounds ? this.state.selection : null),\n canvas: () => this.naturalSize,\n pixelsOf: (layer) => this.surfaceOf(layer),\n capture: () => this.captureHistoryEntry(),\n show: (layers, selection, active) => {\n this.state.layers = layers;\n this.state.selection = selection;\n if (active !== this.state.activeLayerId) {\n this.state.activeLayerId = active;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n },\n record: (before) => {\n if (this.isApplyingHistory) return;\n this.undoStack.push(before as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n this.notify();\n },\n restore: (before) => {\n const e = before as HistoryEntry;\n this.state.layers = e.layers;\n this.state.selection = e.selection;\n this.state.activeLayerId = e.activeLayerId;\n this.state.activeTarget = e.activeTarget;\n this.notify();\n },\n };\n }\n\n /**\n * Starts a Free Transform (Edit › Free Transform, Transform Selection, or a transform of a Layer\n * via Copy): of the layer's pixels with its linked mask, an unlinked mask alone, the selected\n * pixels or just the selection outline (see `FreeTransformInit`). Change the geometry with\n * `session.update` (live preview, no history), then `commit()` (one undo step) or `cancel()`.\n * Refused while another transform is open.\n */\n public beginFreeTransform(init: FreeTransformInit = {}): FreeTransformSession {\n if (this.getFreeTransform()) throw new LayerOperationError('A Free Transform is already open; commit or cancel it first.');\n const session = new FreeTransformSession(this.transformHost(), init);\n this.openTransform = session;\n this.notify();\n return session;\n }\n\n /**\n * Transforms in one step (agents, scripts): begins a Free Transform for `spec`, sets its corners\n * from exactly one of `matrix` (document-space affine), `corners` (where the frame's corners go:\n * distort / perspective) or `params` (scale, rotate, skew and move about a reference point), and\n * commits it as one undo step. Returns what changed and the new bounds.\n */\n public transformLayer(spec: FreeTransformInit & { matrix?: AffineMatrix; corners?: Quad; params?: TransformParams; warp?: WarpMesh | WarpPreset | null }): FreeTransformResult {\n const given = [spec.matrix, spec.corners, spec.params].filter((v) => v !== undefined).length;\n if (given > 1) throw new LayerOperationError('Give only one of matrix, corners and params.');\n if (!given && spec.warp === undefined) throw new LayerOperationError('Give matrix, corners or params (or a warp).');\n if (spec.matrix && !(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) {\n throw new LayerOperationError('matrix must be six finite numbers [a, b, c, d, e, f] (x\\' = a·x + c·y + e, y\\' = b·x + d·y + f).');\n }\n if (spec.matrix) {\n const [a, b, c, d] = spec.matrix;\n if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError('matrix is singular (it collapses the layer to a line); its determinant a·d − b·c must not be 0.');\n }\n const session = this.beginFreeTransform(spec);\n try {\n // Relative to where the content is now (a smart object's current placement).\n const g = session.startGeometry;\n const corners = spec.matrix ? (g.corners.map((p) => applyMatrix(spec.matrix!, p)) as unknown as Quad) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;\n let warp: WarpMesh | null = spec.warp === undefined ? (g.warp ?? null) : null;\n if (spec.warp) {\n if ('style' in spec.warp) {\n if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style \"${String(spec.warp.style)}\". Styles: ${WARP_STYLES.join(', ')}.`);\n warp = presetWarp(session.box, spec.warp);\n } else {\n if (!isValidWarp(spec.warp)) throw new LayerOperationError('mesh must have us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points, row by row.');\n warp = spec.warp;\n }\n }\n session.update({ corners, warp }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */\n public flipLayer(axis: 'horizontal' | 'vertical', init: FreeTransformInit = {}): FreeTransformResult {\n if (axis !== 'horizontal' && axis !== 'vertical') throw new LayerOperationError(`axis must be horizontal or vertical, got \"${String(axis)}\".`);\n const session = this.beginFreeTransform({ interpolation: 'nearest', ...init });\n try {\n session.update({ corners: flipCorners(session.startGeometry, axis) }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /**\n * Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's\n * centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).\n */\n public rotateLayer90(turn: 90 | -90 | 180, init: FreeTransformInit = {}): FreeTransformResult {\n if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);\n const session = this.beginFreeTransform({ interpolation: 'nearest', ...init });\n try {\n // The frame in the document (a smart object's placed outline).\n const b = session.smartObject ? (transformedBox(session.startGeometry) ?? session.box) : session.box;\n // About the centre, rounded so an odd-by-even box still lands on the pixel grid.\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);\n const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);\n const m: AffineMatrix =\n turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];\n session.update({ corners: session.startGeometry.corners.map((p) => applyMatrix(m, p)) as unknown as Quad }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */\n public addLayerFilter(id: string, filter: Partial<LayerFilter> & { kind: LayerFilter['kind'] }): LayerFilter[] {\n const f = normalizeFilter(filter);\n const next = addLayerFilterOp(this.state.layers, id, f);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */\n public updateLayerFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): LayerFilter[] {\n const next = updateLayerFilterOp(this.state.layers, id, filterId, changes);\n this.commitLayers(next, undefined, commitToHistory);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Removes one filter (one undo step). Returns the remaining filters. */\n public removeLayerFilter(id: string, filterId: string): LayerFilter[] {\n const next = removeLayerFilterOp(this.state.layers, id, filterId);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n // --- Smart objects ---\n\n private get canvasSize(): { width: number; height: number } {\n return this.naturalSize;\n }\n\n /**\n * Layer › Smart Objects › Convert to Smart Object (one undo step): sibling layers (default the\n * active one) become one smart object at the topmost one's position. A single pixel layer keeps\n * its id and properties and its filters become smart filters (it renders exactly as before);\n * anything else nests as a layer document. Returns the smart object's id.\n */\n public convertToSmartObject(ids?: string[], options: { name?: string } = {}): string {\n const list = ids?.length ? ids : this.state.activeLayerId ? [this.state.activeLayerId] : [];\n if (!list.length) throw new LayerOperationError('No layers given and none is active; pass layerIds (see list_layers).');\n const res = convertToSmartObjectOp(this.state.layers, list, this.canvasSize, { surfaceOf: (l) => this.surfaceOf(l), name: options.name });\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.layer.id;\n this.state.activeTarget = 'pixels';\n });\n return res.layer.id;\n }\n\n /** Layer › Smart Objects › Rasterize (one undo step): the smart object becomes a pixel layer holding what it renders. */\n public rasterizeSmartObject(id: string): string {\n const res = rasterizeSmartObjectOp(this.state.layers, id);\n this.commitLayers(res.layers, () => {\n if (this.state.activeLayerId === id && this.state.activeTarget === 'filter-mask') this.state.activeTarget = 'pixels';\n });\n return id;\n }\n\n /** A smart object's contents, placement and smart filters as plain data (see `SmartObjectInfo`). */\n public getSmartObject(id: string): SmartObjectInfo {\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n const g = placementGeometry(layer);\n const readout = describeTransform(g);\n const stack = layer.smartFilters;\n return {\n layerId: layer.id,\n contents: {\n id: c.id,\n name: c.name,\n kind: c.kind,\n width: c.width,\n height: c.height,\n decoded: isContentsDecoded(c),\n fileType: c.file?.type ?? null,\n linkedPath: c.linked?.path ?? null,\n instances: instancesOf(this.state.layers, c.id),\n layerCount: c.document ? flattenLayers(c.document.layers).length : 0,\n },\n placement: {\n corners: g.corners.map((p) => ({ x: p.x, y: p.y })) as unknown as Quad,\n warp: layer.placement.warp ?? null,\n interpolation: layer.placement.interpolation ?? 'bicubic',\n affine: readout.affine,\n scaleX: readout.scaleX,\n scaleY: readout.scaleY,\n rotate: readout.rotate,\n skewX: readout.skewX,\n center: anchorPoint(g, 'center'),\n bounds: transformedBox(g),\n },\n smartFilters: {\n enabled: stack?.enabled ?? true,\n filters: (stack?.filters ?? []).map((f) => (f.kind === 'psd' ? { id: f.id, kind: 'psd', name: f.name, enabled: f.enabled, blend: f.blend, opacity: f.opacity } : { ...f })),\n mask: stack?.mask\n ? { enabled: stack.mask.enabled !== false, linked: stack.mask.linked !== false, density: stack.mask.density ?? 1, feather: stack.mask.feather ?? 0, bounds: contentBounds(stack.mask.surface), defaultValue: stack.mask.defaultValue }\n : null,\n },\n bounds: this.getLayerBounds(id),\n };\n }\n\n /**\n * Sets a smart object's placement (one undo step, or none with `commitToHistory = false` during a\n * drag): where the contents' corners land, an optional warp in contents pixels and the\n * interpolation. The contents render again from the source; a linked mask follows a warp-free\n * change. Returns the new placement summary.\n */\n public setSmartObjectPlacement(id: string, placement: SmartPlacement, commitToHistory = true): SmartObjectInfo['placement'] & { masksMoved: boolean } {\n const res = setSmartPlacement(this.state.layers, id, placement, this.canvasSize);\n this.commitLayers(res.layers, undefined, commitToHistory);\n return { ...this.getSmartObject(id).placement, masksMoved: res.masksMoved };\n }\n\n /**\n * Sets a smart object's transform absolutely (one undo step): exactly one of `corners` (where the\n * contents' corners land), `matrix` (contents pixels → document) or the numbers (`scaleX`,\n * `scaleY` in percent of the contents' own size, `rotate`, `skewX`, `skewY`, and `x`, `y` where\n * the contents' centre goes, default where it is now); plus, optionally, a warp (a preset, a mesh\n * in contents pixels, or `null` to remove it) and the interpolation. Unlike `transformLayer`,\n * nothing is relative to the current transform.\n */\n public setSmartObjectTransform(\n id: string,\n spec: {\n corners?: Quad;\n matrix?: AffineMatrix;\n scaleX?: number;\n scaleY?: number;\n rotate?: number;\n skewX?: number;\n skewY?: number;\n x?: number;\n y?: number;\n warp?: WarpMesh | WarpPreset | null;\n interpolation?: Interpolation;\n }\n ): SmartObjectInfo['placement'] & { masksMoved: boolean } {\n const layer = requireSmartObject(this.state.layers, id);\n const g = placementGeometry(layer);\n const numeric = (['scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'x', 'y'] as const).filter((k) => spec[k] !== undefined);\n const ways = [spec.corners !== undefined, spec.matrix !== undefined, numeric.length > 0].filter(Boolean).length;\n if (ways > 1) throw new LayerOperationError('Give the transform only one way: corners, matrix, or the numbers (scaleX, scaleY, rotate, skewX, skewY, x, y).');\n let corners: Quad = g.corners;\n if (spec.corners) corners = spec.corners;\n else if (spec.matrix) {\n if (!(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) throw new LayerOperationError(\"matrix must be six finite numbers [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f).\");\n const [a, b, c, d] = spec.matrix;\n if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError('matrix is singular (it collapses the contents to a line); a·d − b·c must not be 0.');\n corners = cornersFromMatrix(g.box, spec.matrix);\n } else if (numeric.length) {\n if (spec.scaleX === 0 || spec.scaleY === 0) throw new LayerOperationError('scaleX and scaleY must not be 0 (that collapses the contents to a line).');\n for (const k of numeric) if (!Number.isFinite(spec[k] as number)) throw new LayerOperationError(`${k} must be a finite number.`);\n const now = describeTransform(g);\n const centre = anchorPoint(g, 'center');\n const params = {\n scaleX: spec.scaleX ?? (now.affine ? now.scaleX : 100),\n scaleY: spec.scaleY ?? (now.affine ? now.scaleY : 100),\n rotate: spec.rotate ?? (now.affine ? now.rotate : 0),\n skewX: spec.skewX ?? (now.affine ? now.skewX : 0),\n skewY: spec.skewY ?? 0,\n };\n const m = matrixFromParams(g.box, { ...params, reference: 'center' });\n const cx = g.box.width / 2;\n const cy = g.box.height / 2;\n const to = { x: spec.x ?? centre.x, y: spec.y ?? centre.y };\n corners = cornersFromMatrix(g.box, multiplyMatrices(m, translateMatrix(to.x - cx, to.y - cy)));\n }\n let warp: WarpMesh | null | undefined = layer.placement.warp ?? null;\n if (spec.warp === null) warp = null;\n else if (spec.warp) {\n if ('style' in spec.warp) {\n if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style \"${String(spec.warp.style)}\". Styles: ${WARP_STYLES.join(', ')}.`);\n warp = presetWarp(g.box, spec.warp);\n } else {\n if (!isValidWarp(spec.warp)) throw new LayerOperationError('mesh must have us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points in contents pixels, row by row.');\n warp = spec.warp;\n }\n }\n const next: SmartPlacement = { corners, warp, interpolation: spec.interpolation ?? layer.placement.interpolation };\n return this.setSmartObjectPlacement(id, next);\n }\n\n /**\n * Layer › Smart Objects › Replace Contents (one undo step): new contents for the smart object and\n * every instance linked to it, from RGBA pixels, PSD bytes (a layer document) or contents. Each\n * instance keeps its scale and rotation about the contents' centre. Returns the contents id and\n * the layers that changed.\n */\n public replaceSmartObjectContents(\n id: string,\n source: { pixels: PixelBuffer; name?: string } | { psd: Uint8Array; name?: string } | { contents: SmartContents }\n ): { contentsId: string; width: number; height: number; layerIds: string[] } {\n const layer = requireSmartObject(this.state.layers, id);\n let contents: SmartContents;\n if ('contents' in source) contents = source.contents;\n else if ('pixels' in source) contents = rasterContents(source.pixels, source.name ?? layer.contents.name.replace(/\\.\\w+$/, '.png'));\n else {\n const psd = importPsd(source.psd, { newId: () => newLayerId() });\n contents = documentContents({ layers: psd.layers, globalLight: psd.globalLight, patterns: psd.patterns.length ? psd.patterns : undefined }, psd.width, psd.height, source.name ?? `${layer.name || 'Contents'}.psb`);\n contents.file = { bytes: source.psd.slice(), type: psd.info.psb ? 'psb' : 'psd' };\n }\n const res = replaceContentsOp(this.state.layers, layer.contents.id, contents, this.canvasSize);\n this.commitLayers(res.layers);\n return { contentsId: layer.contents.id, width: contents.width, height: contents.height, layerIds: res.ids };\n }\n\n /** `replaceSmartObjectContents` from an image (PNG data URL or base64 decodes anywhere; other formats need a browser). */\n public async replaceSmartObjectContentsFromImage(id: string, image: string, name?: string): Promise<{ contentsId: string; width: number; height: number; layerIds: string[] }> {\n const pixels = await this.decodeImage(image);\n return this.replaceSmartObjectContents(id, { pixels, name });\n }\n\n /**\n * Layer › Smart Objects › Export Contents: the file the contents came from while unedited, else a\n * PNG (raster contents) or a PSD / PSB of the nested document. Returns the file name, MIME type\n * and bytes.\n */\n public exportSmartObjectContents(id: string): { name: string; mimeType: string; bytes: Uint8Array } {\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n const base = c.name.replace(/\\.\\w+$/, '') || 'Contents';\n if (c.file) {\n const mime: Record<string, string> = { psd: 'image/vnd.adobe.photoshop', psb: 'image/vnd.adobe.photoshop', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', tif: 'image/tiff', tiff: 'image/tiff', gif: 'image/gif' };\n const ext = c.file.type.toLowerCase();\n return { name: /\\.\\w+$/.test(c.name) ? c.name : `${base}.${ext}`, mimeType: mime[ext] ?? 'application/octet-stream', bytes: c.file.bytes };\n }\n if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are not available here.`);\n if (c.kind === 'raster') {\n const data = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });\n return { name: `${base}.png`, mimeType: 'image/png', bytes: encodePng(c.width, c.height, data) };\n }\n const doc = c.document!;\n const merged = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });\n const prepared = prepareNestedForPsd(doc.layers);\n const res = exportPsd({ width: c.width, height: c.height, layers: prepared, merged, patterns: doc.patterns, globalLight: doc.globalLight });\n return { name: `${base}.${res.psb ? 'psb' : 'psd'}`, mimeType: 'image/vnd.adobe.photoshop', bytes: res.bytes };\n }\n\n /**\n * Layer › Smart Objects › New Smart Object via Copy (`linked: false`: independent contents) or a\n * linked duplicate (`linked: true`: edits to the contents show in both), directly above, as one\n * undo step. Returns the copy's id.\n */\n public copySmartObject(id: string, linked = false, name?: string): string {\n const res = copySmartObjectOp(this.state.layers, id, linked, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.layer.id;\n this.state.activeTarget = 'pixels';\n });\n return res.layer.id;\n }\n\n /** Adds a smart filter (`develop` or `blur`, with `blend` and `opacity`) at `index` (default the top), as one undo step. */\n public addSmartFilter(id: string, filter: Record<string, unknown> & { kind: string }, index?: number): SmartFilter {\n const res = addSmartFilterOp(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);\n this.commitLayers(res.layers);\n return res.filter;\n }\n\n /** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */\n public updateSmartFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): SmartFilter {\n const res = updateSmartFilterOp(this.state.layers, id, filterId, changes, this.canvasSize);\n this.commitLayers(res.layers, undefined, commitToHistory);\n return res.filter;\n }\n\n /** Removes one smart filter (one undo step). Returns the remaining filters. */\n public removeSmartFilter(id: string, filterId: string): SmartFilter[] {\n const res = removeSmartFilterOp(this.state.layers, id, filterId, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.filters ?? [];\n }\n\n /** Moves one smart filter to `index` (0 = bottom, applied first), as one undo step. Returns the filters. */\n public moveSmartFilter(id: string, filterId: string, index: number): SmartFilter[] {\n const res = moveSmartFilterOp(this.state.layers, id, filterId, index, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.filters ?? [];\n }\n\n /** Turns a smart object's whole filter stack on or off (one undo step). */\n public setSmartFiltersEnabled(id: string, enabled: boolean): boolean {\n const res = setSmartFiltersEnabledOp(this.state.layers, id, enabled, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.enabled ?? true;\n }\n\n /**\n * Sets the smart filter mask (one undo step): `reveal-all`, `hide-all`, `selection` (the\n * selection's coverage), or `none` to remove it; `props` sets density, feather, enabled and linked.\n * Painting it targets channel `filter-mask`. Returns the mask, or `null` when removed.\n */\n public setSmartFilterMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' | 'none' | 'keep', props: LayerMaskProps = {}, commitToHistory = true): RasterMask | null {\n const layer = requireSmartObject(this.state.layers, id);\n let mask: RasterMask | null;\n if (from === 'none') mask = null;\n else if (from === 'keep') {\n if (!layer.smartFilters?.mask) throw new LayerOperationError(`Smart object \"${layer.name || id}\" has no filter mask; add one with from reveal-all, hide-all or selection.`);\n mask = withMaskProps(layer.smartFilters.mask, props);\n } else if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const res = setSmartFilterMaskOp(this.state.layers, id, mask, this.canvasSize);\n this.commitLayers(res.layers, () => {\n if (this.state.activeLayerId !== id) return;\n if (mask && from !== 'keep') this.state.activeTarget = 'filter-mask';\n else if (!mask && this.state.activeTarget === 'filter-mask') this.state.activeTarget = 'pixels';\n }, commitToHistory);\n return res.layer.smartFilters?.mask ?? null;\n }\n\n /**\n * Decodes smart object contents that came as files the synchronous decoders cannot read (a JPEG\n * or TIFF embedded in a PSD), through the browser's image decoder. No undo step: the pixels on\n * screen do not change. Returns the contents ids still undecoded.\n */\n public async prepareSmartObjects(): Promise<string[]> {\n const pending = new Map<string, SmartContents>();\n for (const l of flattenLayers(this.state.layers)) if (l.type === 'smart' && !isContentsDecoded(l.contents) && l.contents.file) pending.set(l.contents.id, l.contents);\n const left: string[] = [];\n for (const [cid, c] of pending) {\n try {\n const url = `data:${c.file!.type === 'png' ? 'image/png' : c.file!.type === 'gif' ? 'image/gif' : c.file!.type === 'tif' || c.file!.type === 'tiff' ? 'image/tiff' : 'image/jpeg'};base64,${bytesToBase64(c.file!.bytes)}`;\n const px = await this.decodeImage(url);\n const decoded: SmartContents = { ...c, kind: 'raster', raster: rasterContents(px, c.name, cid).raster, width: px.width, height: px.height };\n const swap = (layers: ImageLayer[]) => {\n let out = layers;\n for (const l of flattenLayers(layers)) if (l.type === 'smart' && l.contents.id === cid) out = mapLayer(out, l.id, (x) => ({ ...(x as SmartObjectLayer), contents: decoded }));\n return out;\n };\n this.state.layers = swap(this.state.layers);\n for (const e of [...this.undoStack, ...this.redoStack]) e.layers = swap(e.layers);\n } catch {\n left.push(cid);\n }\n }\n if (pending.size) this.notify();\n return left;\n }\n\n // --- Smart object contents (Edit Contents) ---\n\n private contentsStack: Array<{ layerId: string; contentsId: string; name: string; saved: SavedDocument }> = [];\n\n /** The smart objects whose contents are open (outermost first); empty when the document itself is shown. */\n public getEditingContents(): Array<{ layerId: string; name: string }> {\n return this.contentsStack.map((e) => ({ layerId: e.layerId, name: e.name }));\n }\n\n /**\n * Layer › Smart Objects › Edit Contents: shows the smart object's contents as a document of their\n * own (a layer document, or raster contents as one pixel layer), with their own undo history.\n * Every editor operation and tool then works on the contents until `closeSmartObjectContents`.\n * Contents can nest. Returns the contents' size and layer ids.\n */\n public editSmartObjectContents(id: string): { layerId: string; depth: number; width: number; height: number; layerIds: string[] } {\n if (this.getFreeTransform()) throw new LayerOperationError('A Free Transform is open; commit or cancel it first.');\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are a ${c.file?.type ?? 'file'} that could not be decoded here; run prepareSmartObjects() in a browser first.`);\n const saved: SavedDocument = {\n state: { ...this.state },\n undo: this.undoStack,\n redo: this.redoStack,\n initial: this.initialSnapshot,\n element: this.imageElement,\n samplePixels: this.samplePixels,\n samplePixelsOverride: this.samplePixelsOverride,\n psdResolution: this.psdResolution,\n };\n const layers: ImageLayer[] =\n c.kind === 'document'\n ? c.document!.layers\n : [{ type: 'pixel', id: newLayerId(), name: 'Layer 0', visible: true, opacity: 1, blend: 'normal', surface: c.raster!, offset: { x: 0, y: 0 } }];\n this.contentsStack.push({ layerId: layer.id, contentsId: c.id, name: layer.name || c.name, saved });\n const s = this.state;\n this.state = {\n ...s,\n sourceUrl: null,\n imageDimensions: { width: c.width, height: c.height },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM_VALUES },\n layers,\n activeLayerId: layers.length ? layers[layers.length - 1].id : null,\n activeTarget: 'pixels',\n selection: null,\n selectedAnnotationId: null,\n outputOpacity: 1,\n zoom: 1,\n pan: { x: 0, y: 0 },\n editingContents: this.getEditingContents(),\n };\n delete this.state.legacyOutputBlur;\n if (c.document?.globalLight) this.state.globalLight = c.document.globalLight;\n else delete this.state.globalLight;\n if (c.document?.patterns?.length) this.state.patterns = c.document.patterns;\n else delete this.state.patterns;\n this.imageElement = null;\n this.samplePixels = null;\n this.samplePixelsOverride = false;\n this.undoStack = [];\n this.redoStack = [];\n this.viewKey = [];\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n return { layerId: layer.id, depth: this.contentsStack.length, width: c.width, height: c.height, layerIds: flattenLayers(layers).map((l) => l.id) };\n }\n\n /**\n * Closes the innermost open contents: with `save` (default) the edited layers become the smart\n * object's contents, and it and every linked instance render again (one undo step in the\n * document); without, the edits are dropped. Returns the smart object and the layers that changed.\n */\n public closeSmartObjectContents(save = true): { layerId: string; changed: boolean; layerIds: string[] } {\n const top = this.contentsStack.pop();\n if (!top) throw new LayerOperationError('No smart object contents are open; open them with edit_smart_object_contents.');\n if (this.openTransform?.isOpen) this.openTransform.cancel();\n this.openTransform = null;\n const edited = this.state;\n const changed = save && this.undoStack.length > 0;\n let next: SmartContents | null = null;\n if (changed) {\n const layer = findLayer(top.saved.state.layers, top.layerId)?.layer as SmartObjectLayer | undefined;\n const old = layer?.contents;\n const { width, height } = edited.imageDimensions;\n const only = edited.layers.length === 1 ? edited.layers[0] : null;\n const plain =\n only?.type === 'pixel' && only.visible && only.opacity === 1 && only.blend === 'normal' && !only.mask && !only.effects && !only.filters?.length && !only.clipped && (only.fillOpacity ?? 1) === 1 && only.source !== 'document';\n if (old?.kind === 'raster' && plain) {\n const px = readRegion(only.surface, { x: -only.offset.x, y: -only.offset.y, width, height });\n next = rasterContents({ width, height, data: px }, old.name, old.id);\n } else {\n const doc = { layers: edited.layers, globalLight: edited.globalLight, patterns: edited.patterns };\n next = documentContents(doc, width, height, old?.kind === 'document' ? old.name : `${(old?.name ?? 'Contents').replace(/\\.\\w+$/, '')}.psb`, top.contentsId);\n }\n }\n const saved = top.saved;\n this.state = saved.state;\n this.undoStack = saved.undo;\n this.redoStack = saved.redo;\n this.initialSnapshot = saved.initial;\n this.imageElement = saved.element;\n this.samplePixels = saved.samplePixels;\n this.samplePixelsOverride = saved.samplePixelsOverride;\n this.psdResolution = saved.psdResolution;\n this.viewKey = [];\n if (this.contentsStack.length) this.state.editingContents = this.getEditingContents();\n else delete this.state.editingContents;\n if (!next) {\n this.notify();\n return { layerId: top.layerId, changed: false, layerIds: [] };\n }\n const res = replaceContentsOp(this.state.layers, top.contentsId, next, this.canvasSize);\n this.commitLayers(res.layers);\n return { layerId: top.layerId, changed: true, layerIds: res.ids };\n }\n\n /** The document's own layers (not the contents open for editing). */\n private rootLayers(): ImageLayer[] {\n return this.contentsStack[0]?.saved.state.layers ?? this.state.layers;\n }\n\n /** Drops every open contents edit (loading another document). */\n private abandonContents(): void {\n while (this.contentsStack.length) this.closeSmartObjectContents(false);\n }\n\n // --- Layer effects ---\n\n /** A layer's effects (an empty, enabled list when it has none). */\n public getLayerEffects(id: string): LayerEffects {\n return layerEffectsOf(requireLayer(this.state.layers, id).layer);\n }\n\n /**\n * Adds an effect of `kind` with Photoshop's defaults and `params` applied (one undo step), at\n * `index` in the layer's effect list (default the end). Returns the new effect and the effects.\n */\n public addLayerEffect(id: string, kind: LayerEffectKind, params: Record<string, unknown> = {}, index?: number): { effect: LayerEffect; effects: LayerEffects } {\n const res = addLayerEffectOp(this.state.layers, id, kind, params, index);\n this.commitLayers(res.layers);\n return { effect: res.effect, effects: this.getLayerEffects(id) };\n }\n\n /** Changes one effect (one undo step, or none with `commitToHistory = false` during a drag). Returns the effects. */\n public updateLayerEffect(id: string, effectId: string, changes: Record<string, unknown>, commitToHistory = true): LayerEffects {\n this.commitLayers(updateLayerEffectOp(this.state.layers, id, effectId, changes), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n /** Removes one effect (one undo step). Returns the remaining effects. */\n public removeLayerEffect(id: string, effectId: string): LayerEffects {\n this.commitLayers(removeLayerEffectOp(this.state.layers, id, effectId));\n return this.getLayerEffects(id);\n }\n\n /** Turns a layer's effects on or off as a whole and, when given, sets Scale Effects (one undo step). */\n public setLayerEffectsEnabled(id: string, enabled: boolean, scale?: number, commitToHistory = true): LayerEffects {\n this.commitLayers(setLayerEffectsEnabledOp(this.state.layers, id, enabled, scale), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n /** Replaces a layer's effects (one undo step; `null` removes them). Returns the effects. */\n public setLayerEffects(id: string, effects: LayerEffects | null, commitToHistory = true): LayerEffects {\n this.commitLayers(setLayerEffectsOp(this.state.layers, id, effects), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n private patternIds = new WeakMap<object, number>();\n\n /** Changes whenever the document's pattern list is replaced (the compositor then drops its caches). */\n private patternsVersion(): number {\n const list = this.state.patterns;\n if (!list) return 0;\n let id = this.patternIds.get(list);\n if (id === undefined) {\n id = this.nextSourceId++;\n this.patternIds.set(list, id);\n }\n return id;\n }\n\n /** The document's patterns (from an opened PSD), besides the built-in ones. */\n public getDocumentPatterns(): EffectPattern[] {\n return this.state.patterns ?? [];\n }\n\n /** The light effects with \"use global light\" follow. */\n public getGlobalLight(): GlobalLight {\n return { ...(this.state.globalLight ?? DEFAULT_GLOBAL_LIGHT) };\n }\n\n /** Sets the global light (one undo step). `angle` -360..360°, `altitude` 0..90°. Returns it. */\n public setGlobalLight(light: Partial<GlobalLight>, commitToHistory = true): GlobalLight {\n const next = { ...this.getGlobalLight(), ...light };\n 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}.`);\n 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}.`);\n if (commitToHistory) this.recordHistory();\n if (next.angle === DEFAULT_GLOBAL_LIGHT.angle && next.altitude === DEFAULT_GLOBAL_LIGHT.altitude) delete this.state.globalLight;\n else this.state.globalLight = next;\n this.notify();\n return this.getGlobalLight();\n }\n\n /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */\n private async compositeToSurface(layers: ImageLayer[], rect: CropBox): Promise<PixelSurface> {\n const { width, height } = this.naturalSize;\n if (flattenLayers(layers).some((l) => l.type === 'annotation' && l.visible) && !this.renderer.canRasterize()) {\n throw new LayerOperationError('Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.');\n }\n await this.renderer.prepareLayerAssets(layers, width, height);\n const data = new LayerCompositor().composite(layers, rect, this.renderer.compositorSources(width, height));\n return surfaceFromPixels({ width: rect.width, height: rect.height, data }, 4, rect.x, rect.y);\n }\n\n /** Document bounds a layer can paint into (pixel content, annotations, or the canvas for the rest). */\n private paintBounds(layer: ImageLayer): CropBox {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n if (layer.type === 'pixel') return this.getLayerBounds(layer.id) ?? { x: 0, y: 0, width: 0, height: 0 };\n if (layer.type === 'annotation') {\n const b = getAnnotationBounds(layer.annotation);\n const pad = 4 + ('strokeWidth' in layer.annotation ? (layer.annotation as { strokeWidth: number }).strokeWidth : 0);\n return coveringRect({ x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad });\n }\n if (layer.type === 'group') {\n let r: CropBox | null = null;\n for (const c of layer.children) r = unionRect(r, this.paintBounds(c));\n return r ?? canvas;\n }\n return canvas;\n }\n\n /**\n * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,\n * opacity and blend; masks, fill, filters and both layers' effects are baked), as one undo step.\n * Returns the merged id.\n */\n public async mergeDown(id: string): Promise<string> {\n const loc = requireLayer(this.state.layers, id);\n const upper = loc.layer;\n const siblings = loc.parent ? loc.parent.children : this.state.layers;\n const lower = siblings[loc.index - 1];\n if (!lower) throw new LayerOperationError(`Layer \"${upper.name || id}\" is the bottom of its group; there is nothing below to merge into.`);\n if (lower.type !== 'pixel') throw new LayerOperationError(`Merge Down needs a pixel layer below; \"${lower.name || lower.id}\" is a ${lower.type} layer. Move the layer or merge into another one.`);\n if (!upper.visible) throw new LayerOperationError(`Layer \"${upper.name || id}\" is hidden; show it or delete it instead of merging.`);\n requireUnlocked(upper, 'all', 'merge it');\n requireUnlocked(lower, 'pixels', 'merge into it');\n let rect = unionRect(this.paintBounds(lower), upper.type === 'adjustment' ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };\n // Effects paint beyond the layers' pixels: merge as far as they reach.\n const reach = Math.max(effectsMargin(lower.effects), upper.type === 'adjustment' ? 0 : effectsMargin(upper.effects));\n if (reach) rect = { x: rect.x - reach, y: rect.y - reach, width: rect.width + 2 * reach, height: rect.height + 2 * reach };\n const base: PixelLayer = { ...lower, blend: 'normal', opacity: 1 };\n delete base.clipped;\n const surface = await this.compositeToSurface([base, upper], rect);\n const merged: PixelLayer = { ...lower, surface, offset: { x: 0, y: 0 } };\n delete merged.mask;\n delete merged.fillOpacity;\n delete merged.filters;\n delete merged.source;\n delete merged.effects;\n let next = mapLayer(this.state.layers, id, () => null);\n next = mapLayer(next, lower.id, () => merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n });\n return merged.id;\n }\n\n /**\n * Merge Visible: the visible top-level layers become one normal pixel layer (at the position of\n * the lowest of them); hidden layers stay. One undo step. Returns the merged id.\n */\n public async mergeVisible(): Promise<string> {\n const visible = this.state.layers.filter((l) => l.visible);\n if (!visible.length) throw new LayerOperationError('No layer is visible.');\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(visible, { x: 0, y: 0, width, height });\n const merged: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: 'Merged',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n const at = this.state.layers.indexOf(visible[0]);\n const next = this.state.layers.filter((l) => !l.visible);\n next.splice(this.state.layers.slice(0, at).filter((l) => !l.visible).length, 0, merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return merged.id;\n }\n\n /** Flatten Image: every visible layer composited into one Background layer; hidden layers are discarded. One undo step. */\n public async flattenImage(): Promise<string> {\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(this.state.layers, { x: 0, y: 0, width, height });\n const bg: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n this.commitLayers([bg], () => {\n this.state.activeLayerId = bg.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return bg.id;\n }\n\n private thumbCache = new WeakMap<object, Map<string, PixelBuffer>>();\n\n /**\n * A small RGBA preview of a pixel layer over the canvas (or of its mask with `mask: true`),\n * longest side `maxSide` px, point-sampled; `null` for layers without pixels.\n */\n public getLayerThumbnail(id: string, options: { maxSide?: number; mask?: boolean } = {}): PixelBuffer | null {\n const layer = this.getLayer(id);\n if (!layer) return null;\n const { width, height } = this.naturalSize;\n const side = Math.max(4, Math.min(256, Math.round(options.maxSide ?? 48)));\n const k = Math.min(1, side / Math.max(width, height));\n const tw = Math.max(1, Math.round(width * k));\n const th = Math.max(1, Math.round(height * k));\n const surface = options.mask ? (layer.mask && !layer.mask.source ? layer.mask.surface : null) : layer.type === 'pixel' || layer.type === 'smart' ? this.surfaceOf(layer) : null;\n if (!surface) return null;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const key = `${tw}x${th}:${ox},${oy}`;\n let byKey = this.thumbCache.get(surface);\n const hit = byKey?.get(key);\n if (hit) return hit;\n const out = new Uint8ClampedArray(tw * th * 4);\n for (let y = 0; y < th; y++) {\n const dy = Math.min(height - 1, Math.floor(((y + 0.5) * height) / th)) - oy;\n for (let x = 0; x < tw; x++) {\n const dx = Math.min(width - 1, Math.floor(((x + 0.5) * width) / tw)) - ox;\n const v = readPixel(surface, dx, dy);\n const i = (y * tw + x) * 4;\n if (surface.channels === 1) {\n out[i] = out[i + 1] = out[i + 2] = v[0];\n out[i + 3] = 255;\n } else out.set(v, i);\n }\n }\n const thumb = { width: tw, height: th, data: out };\n if (!byKey) {\n byKey = new Map();\n this.thumbCache.set(surface, byKey);\n }\n byKey.set(key, thumb);\n return thumb;\n }\n\n // --- Painting ---\n\n /** Brush settings, colours and tool options of the paint tools (view state, no undo). */\n public setPaintOptions(\n options: Partial<Omit<PaintOptions, 'brush' | 'mixer' | 'mixerLoad'>> & { brush?: Partial<BrushSettings>; mixer?: Partial<MixerSettings> }\n ): PaintOptions {\n const cur = this.state.paint;\n const next: PaintOptions = {\n brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,\n foreground: options.foreground !== undefined ? toHex(parseColor(options.foreground)) : cur.foreground,\n background: options.background !== undefined ? toHex(parseColor(options.background)) : cur.background,\n mixer: options.mixer ? normalizeMixer({ ...cur.mixer, ...options.mixer }) : cur.mixer,\n mixerLoad: cur.mixerLoad,\n };\n this.state.paint = next;\n this.notify();\n return next;\n }\n\n /**\n * Loads the Mixer Brush's reservoir full with `color` (default the foreground); the picked-up\n * colour is kept. View state, no undo. Returns the brush load.\n */\n public loadMixerBrush(color?: string | number[]): MixerLoad {\n const c = parseColor(color ?? this.state.paint.foreground);\n const load = loadMixer(this.state.paint.mixerLoad, [c[0] / 255, c[1] / 255, c[2] / 255]);\n this.state.paint = { ...this.state.paint, mixerLoad: load };\n this.notify();\n return load;\n }\n\n /** Cleans the Mixer Brush: empties the pickup well (the reservoir is kept). Returns the brush load. */\n public cleanMixerBrush(): MixerLoad {\n const load = cleanMixer(this.state.paint.mixerLoad);\n this.state.paint = { ...this.state.paint, mixerLoad: load };\n this.notify();\n return load;\n }\n\n /** Empties the Mixer Brush entirely (no reservoir, nothing picked up). Returns the brush load. */\n public emptyMixerBrush(): MixerLoad {\n this.state.paint = { ...this.state.paint, mixerLoad: EMPTY_MIXER_LOAD };\n this.notify();\n return EMPTY_MIXER_LOAD;\n }\n\n /** D: black foreground, white background. */\n public resetPaintColors(): void {\n this.setPaintOptions({ foreground: '#000000', background: '#ffffff' });\n }\n\n /** X: swaps the foreground and background colours. */\n public swapPaintColors(): void {\n const { foreground, background } = this.state.paint;\n this.setPaintOptions({ foreground: background, background: foreground });\n }\n\n /** Resolves and validates what a stroke, fill or clear paints into. */\n private paintTarget(layerId: string | undefined, channel: LayerTarget | undefined, action: string) {\n const id = layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`);\n const layer = requireLayer(this.state.layers, id).layer;\n const ch: LayerTarget = channel ?? (id === this.state.activeLayerId ? this.state.activeTarget : 'pixels');\n if (ch === 'mask') {\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask to ${action}; add one with add_layer_mask, or target pixels.`);\n if (layer.mask.source) throw new LayerOperationError(`Layer \"${layer.name || id}\" has a depth mask, which is generated, not painted.`);\n requireUnlocked(layer, 'all', action);\n } else if (ch === 'filter-mask') {\n if (layer.type !== 'smart' || !layer.smartFilters?.mask) {\n throw new LayerOperationError(`Layer \"${layer.name || id}\" has no smart filter mask to ${action}; add one with set_smart_filter_mask, or target pixels or mask.`);\n }\n if (!isContentsDecoded(layer.contents)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are not decoded here, so its filter mask cannot be painted; run prepareSmartObjects() first.`);\n requireUnlocked(layer, 'all', action);\n } else {\n if (layer.type === 'smart') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);\n requireUnlocked(layer, 'pixels', action);\n }\n if (!layer.visible) throw new LayerOperationError(`Layer \"${layer.name || id}\" is hidden; show it before you ${action}.`);\n const offset = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const surface = ch === 'mask' ? layer.mask!.surface : ch === 'filter-mask' ? (layer as SmartObjectLayer).smartFilters!.mask!.surface : this.surfaceOf(layer as PixelLayer);\n const lockTransparency = ch === 'pixels' && Boolean(layer.locks?.transparency);\n return { id, layer, channel: ch, offset, surface, lockTransparency };\n }\n\n /** The layer with its pixels (or mask) replaced by `surface`. */\n private withSurface(layer: ImageLayer, channel: LayerTarget, surface: TiledSurface): ImageLayer {\n if (channel === 'mask') return { ...layer, mask: { ...layer.mask!, surface } } as ImageLayer;\n if (channel === 'filter-mask') return withFilterMaskSurface(layer as SmartObjectLayer, surface, this.canvasSize);\n const next = { ...(layer as PixelLayer), surface };\n delete next.source;\n return next;\n }\n\n /**\n * Starts a brush or eraser stroke on a layer's pixels or mask (default: the active layer and\n * target, the paint options' brush and colours). Feed points with `push`, finish with `end()`\n * (one undo step for the whole stroke) or `cancel()`. While it runs, the stack shows the stroke\n * without recording history.\n */\n public beginStroke(init: StrokeInit = {}): PaintStrokeSession {\n const tool = init.tool ?? 'brush';\n if (tool !== 'brush' && tool !== 'eraser' && tool !== 'mixer') throw new LayerOperationError(`Unknown paint tool \"${String(tool)}\"; use brush, eraser or mixer.`);\n const target = this.paintTarget(init.layerId, init.channel, tool === 'eraser' ? 'erase' : 'paint');\n if (tool === 'mixer' && target.channel !== 'pixels') {\n throw new LayerOperationError('The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.');\n }\n const brush = normalizeBrush({ ...this.state.paint.brush, ...(init.brush ?? {}) });\n const color = parseColor(init.color ?? this.state.paint.foreground);\n const background = parseColor(init.background ?? this.state.paint.background);\n const toMaskGrey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode: tool === 'eraser' ? 'erase' : 'paint',\n color: target.channel !== 'pixels' ? toMaskGrey(color) : color,\n background: target.channel !== 'pixels' ? toMaskGrey(background) : background,\n opacity: brush.opacity,\n blend: target.channel !== 'pixels' ? 'normal' : brush.blend,\n lockTransparency: target.lockTransparency,\n selection: this.state.selection?.bounds ? this.state.selection : null,\n offset: target.offset,\n };\n if (tool === 'mixer') return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply);\n return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);\n }\n\n /** A Mixer Brush stroke's start: the settings, and the load (refilled when asked or set to). */\n private mixerStroke(init: StrokeInit, brush: BrushSettings): MixerStrokeInit {\n const settings = normalizeMixer({ ...this.state.paint.mixer, ...(init.mixer ?? {}) });\n let load = this.state.paint.mixerLoad;\n if (init.color !== undefined || settings.loadEachStroke) {\n const c = parseColor(init.color ?? this.state.paint.foreground);\n load = loadMixer(load, [c[0] / 255, c[1] / 255, c[2] / 255]);\n }\n return {\n create: (pixels) => new MixerStroke(brush, settings, load, pixels),\n end: (after) => {\n const next = settings.cleanEachStroke ? cleanMixer(after) : after;\n this.state.paint = { ...this.state.paint, mixerLoad: next };\n return next;\n },\n };\n }\n\n /** @internal Shows a stroke's live surface (no history). */\n public _strokeUpdate(layerId: string, channel: LayerTarget, surface: TiledSurface, rects: CropBox[]): void {\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n // A filter mask re-renders the smart object (and feathering spreads the change): redraw it all.\n this.notify(channel === 'filter-mask' ? undefined : rects);\n }\n\n /** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */\n public _strokeCommit(before: HistoryEntryHandle, layerId: string, channel: LayerTarget, surface: TiledSurface | null): void {\n if (surface) this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n else this.state.layers = (before as unknown as HistoryEntry).layers;\n if (surface && !this.isApplyingHistory) {\n this.undoStack.push(before as unknown as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n this.notify();\n }\n\n /** @internal The pre-stroke snapshot. */\n public _captureForStroke(): HistoryEntryHandle {\n return this.captureHistoryEntry() as unknown as HistoryEntryHandle;\n }\n\n /**\n * Paints a whole stroke in one call (agents, replays): `points` in document pixels with optional\n * pressure, through the brush, eraser or Mixer Brush. One undo step. Returns the layer, channel, the changed\n * document bounds and the number of dabs.\n */\n public paintStroke(init: StrokeInit & { points: StrokePoint[] }): PaintStrokeResult {\n if (!Array.isArray(init.points) || !init.points.length) throw new LayerOperationError('points must hold at least one { x, y } point.');\n for (const p of init.points) {\n if (!Number.isFinite(p?.x) || !Number.isFinite(p?.y)) throw new LayerOperationError('Every point needs finite x and y (document pixels).');\n }\n const session = this.beginStroke(init);\n session.push(init.points);\n return session.end();\n }\n\n /**\n * Edit › Fill: fills the selection (or, without one or with `useSelection: false`, the whole\n * canvas) on a layer's pixels or mask with a colour (default the foreground), at `opacity` with\n * `blend`. One undo step. Returns the changed bounds.\n */\n public fillPixels(options: FillOptions = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'paint');\n }\n\n /**\n * Edit › Clear: removes the selected pixels (alpha to 0; on a transparency-locked layer or a mask\n * it fills with the background colour). Without a selection (or with `useSelection: false`) the\n * whole canvas. One undo step. Returns the changed bounds.\n */\n public clearPixels(options: Omit<FillOptions, 'color' | 'blend'> = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'erase');\n }\n\n private fillOrClear(options: FillOptions, mode: 'paint' | 'erase'): PaintStrokeResult {\n const target = this.paintTarget(options.layerId, options.channel, mode === 'paint' ? 'fill' : 'clear');\n const { width, height } = this.naturalSize;\n const opacity = options.opacity ?? 1;\n if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const blend = (parseBlend(options.blend) as BlendMode | undefined) ?? 'normal';\n const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;\n const region = sel?.bounds ?? { x: 0, y: 0, width, height };\n const coverage = new CoverageMap();\n const ones = new Float32Array(region.width * region.height);\n if (sel) {\n for (let y = 0; y < region.height; y++) {\n for (let x = 0; x < region.width; x++) ones[y * region.width + x] = sel.data[(region.y + y) * sel.width + region.x + x] / 255;\n }\n } else ones.fill(1);\n coverage.accumulate(region, ones, 1);\n const color = parseColor(options.color ?? this.state.paint.foreground);\n const background = parseColor(this.state.paint.background);\n const grey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode,\n color: target.channel !== 'pixels' ? grey(color) : color,\n background: target.channel !== 'pixels' ? grey(background) : background,\n opacity,\n blend: target.channel !== 'pixels' ? 'normal' : blend,\n lockTransparency: target.lockTransparency,\n selection: null,\n offset: target.offset,\n };\n const surface = paintCoverageInto(target.surface, coverage, apply, [region]);\n const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));\n this.commitLayers(next);\n return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };\n }\n\n // --- Adjustments ---\n\n /** The pixel layer the base sliders edit: the active pixel layer, else the Background. */\n private developLayer(): PixelLayer | null {\n return developTarget(this.state.layers, this.state.activeLayerId);\n }\n\n /**\n * Sets base adjustment values as one undo step (unless `commitToHistory` is false). The base\n * sliders edit the `develop` filter of the active pixel layer (or the Background), `blur` its\n * blur filter (or `legacyOutputBlur` in migrated documents), `opacity` the output opacity, and\n * `layers` replaces the adjustment layers.\n */\n public setAdjustments(partial: Partial<ImageAdjustments>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const target = this.developLayer();\n let layers = this.state.layers;\n if (target && (DEVELOP_KEYS.some((k) => k in partial) || 'process' in partial)) {\n layers = mapLayer(layers, target.id, (l) => withDevelop(l as PixelLayer, partial));\n }\n if ('blur' in partial && typeof partial.blur === 'number') {\n const blur = Math.max(0, partial.blur);\n if (this.state.legacyOutputBlur !== undefined) this.state.legacyOutputBlur = blur;\n else if (target) layers = mapLayer(layers, target.id, (l) => withBlur(l as PixelLayer, blur));\n }\n if ('opacity' in partial && typeof partial.opacity === 'number') {\n this.state.outputOpacity = Math.min(1, Math.max(0, partial.opacity));\n }\n if ('layers' in partial) {\n layers = this.replaceAdjustmentNodes(layers, normalizeAdjustmentLayers(partial.layers));\n }\n this.state.layers = layers;\n this.notify();\n }\n\n /** True when the adjustments use an older rendering process than {@link LATEST_PROCESS_VERSION}. */\n public get isProcessOutdated(): boolean {\n this.syncViews();\n return normalizeProcessVersion(this.state.adjustments.process) < LATEST_PROCESS_VERSION;\n }\n\n /**\n * Update to Current Process: re-interprets the current adjustment values with the latest\n * rendering process (one undo step). Returns false when already current.\n */\n public upgradeProcess(): boolean {\n if (!this.isProcessOutdated) return false;\n this.setAdjustments({ process: LATEST_PROCESS_VERSION });\n return true;\n }\n\n /** Resets the base sliders (latest process). Adjustment layers are kept; see `clearAdjustmentLayers`. */\n public resetAdjustments(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) =>\n withBlur(withDevelop(l as PixelLayer, { ...DEFAULT_ADJUSTMENTS, process: LATEST_PROCESS_VERSION }), 0)\n );\n }\n delete this.state.legacyOutputBlur;\n this.state.outputOpacity = 1;\n this.notify();\n }\n\n // --- Adjustment layers ---\n\n /** The adjustment layers in stack order, bottom (first) to top (last). */\n public getAdjustmentLayers(): AdjustmentLayer[] {\n this.syncViews();\n return this.state.adjustments.layers ?? [];\n }\n\n public getAdjustmentLayer(id: string): AdjustmentLayer | undefined {\n return this.getAdjustmentLayers().find((l) => l.id === id);\n }\n\n private adjustmentNodes(layers: readonly ImageLayer[] = this.state.layers): AdjustmentLayerNode[] {\n return flattenLayers(layers).filter((l): l is AdjustmentLayerNode => l.type === 'adjustment');\n }\n\n /**\n * Where a new layer goes when no position is given: directly above the topmost layer that is\n * not an annotation, so it adjusts what adjustment layers always adjusted (annotations stay on top).\n */\n private defaultInsertIndex(layers: readonly ImageLayer[] = this.state.layers): number {\n let i = layers.length;\n while (i > 0 && layers[i - 1].type === 'annotation') i--;\n return i;\n }\n\n /** Inserts `node` by `aboveId` / `parentId` / adjustment index / default position. */\n private placeLayer(\n layers: ImageLayer[],\n node: ImageLayer,\n where: { aboveId?: string; parentId?: string; adjustmentIndex?: number }\n ): ImageLayer[] {\n if (where.aboveId) {\n const loc = findLayer(layers, where.aboveId);\n if (loc) return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n if (where.parentId) {\n const loc = findLayer(layers, where.parentId);\n if (loc && loc.layer.type === 'group') return insertLayer(layers, node, loc.layer.id);\n }\n if (where.adjustmentIndex !== undefined) {\n const nodes = this.adjustmentNodes(layers);\n const at = Math.max(0, Math.min(nodes.length, Math.round(where.adjustmentIndex)));\n if (at < nodes.length) {\n const loc = findLayer(layers, nodes[at].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index);\n }\n if (nodes.length) {\n const loc = findLayer(layers, nodes[nodes.length - 1].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n }\n return insertLayer(layers, node, null, this.defaultInsertIndex(layers));\n }\n\n /** `setAdjustments({ layers })`: updates nodes by id in place, removes missing ones, adds new ones. */\n private replaceAdjustmentNodes(layers: ImageLayer[], next: AdjustmentLayer[]): ImageLayer[] {\n const { width, height } = this.naturalSize;\n const byId = new Map(next.map((l) => [l.id, l]));\n let out = layers;\n for (const node of this.adjustmentNodes(layers)) {\n const update = byId.get(node.id);\n out = mapLayer(out, node.id, () => (update ? mergeAdjustmentNode(node, update, width, height) : null));\n }\n const existing = new Set(this.adjustmentNodes(out).map((n) => n.id));\n for (const l of next) {\n if (existing.has(l.id)) continue;\n out = this.placeLayer(out, adjustmentNodeFromLegacy(l, width, height), { adjustmentIndex: Infinity });\n }\n // Keep the given order among the adjustment layers.\n const order = next.map((l) => l.id);\n for (let to = 0; to < order.length; to++) out = this.moveAdjustmentNode(out, order[to], to);\n return out;\n }\n\n /**\n * Adds an adjustment layer and returns its id, as one undo step. With no position it goes\n * above the other adjustment layers (or, without any, above the topmost non-annotation layer,\n * so annotations stay on top); `index` counts adjustment layers (0 = below the others);\n * `aboveId` / `parentId` place it anywhere in the stack. `kind` alone gives neutral parameters.\n */\n public addAdjustmentLayer<K extends AdjustmentKind>(init: K | AdjustmentLayerInit<K>, index?: number): string {\n const spec: AdjustmentLayerInit<K> = typeof init === 'string' ? { kind: init } : init;\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer = createAdjustmentLayer(spec.kind, {\n id: spec.id && !ids.has(spec.id) ? spec.id : undefined,\n name: spec.name,\n params: spec.params,\n enabled: spec.enabled,\n opacity: spec.opacity,\n blend: spec.blend,\n mask: spec.mask ?? undefined,\n });\n while (ids.has(layer.id)) layer.id = newLayerId('adj');\n const { width, height } = this.naturalSize;\n this.recordHistory();\n const node = adjustmentNodeFromLegacy(layer, width, height);\n const positioned = Boolean(spec.aboveId || spec.parentId);\n this.state.layers = this.placeLayer(this.state.layers, node, {\n aboveId: spec.aboveId,\n parentId: spec.parentId,\n adjustmentIndex: index !== undefined ? index : positioned ? undefined : Infinity,\n });\n this.notify();\n return layer.id;\n }\n\n /**\n * Updates a layer (`params` merged into the current ones, then normalised). Pass\n * `commitToHistory = false` for the moves of a drag after recording its start once.\n * Returns false for an unknown id.\n */\n public updateAdjustmentLayer(id: string, update: AdjustmentLayerUpdate, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n const { width, height } = this.naturalSize;\n const node = loc.layer;\n const merged = normalizeAdjustmentLayer({\n id: node.id,\n kind: node.kind,\n enabled: update.enabled ?? node.visible,\n opacity: update.opacity ?? node.opacity,\n blend: update.blend ?? node.blend,\n name: update.name ?? node.name,\n params: { ...node.params, ...(update.params ?? {}) },\n });\n if (!merged) return false;\n if (commitToHistory) this.recordHistory();\n const next = {\n ...node,\n visible: merged.enabled,\n opacity: merged.opacity,\n blend: merged.blend,\n name: merged.name ?? '',\n params: merged.params,\n } as AdjustmentLayerNode;\n if (update.mask !== undefined) {\n const mask = update.mask ? rasterMaskFromLegacy(update.mask, width, height) : null;\n if (mask) next.mask = mask;\n else delete next.mask;\n }\n this.state.layers = mapLayer(this.state.layers, id, () => next);\n this.notify();\n return true;\n }\n\n /** Removes a layer as one undo step. Returns false for an unknown id. */\n public removeAdjustmentLayer(id: string): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, id, () => null);\n if (this.state.activeLayerId === id) this.state.activeLayerId = null;\n this.notify();\n return true;\n }\n\n private moveAdjustmentNode(layers: ImageLayer[], id: string, toIndex: number): ImageLayer[] {\n const nodes = this.adjustmentNodes(layers);\n const from = nodes.findIndex((n) => n.id === id);\n if (from === -1) return layers;\n const to = Math.max(0, Math.min(nodes.length - 1, Math.round(toIndex)));\n if (to === from) return layers;\n const node = nodes[from];\n const anchor = nodes[to];\n const without = mapLayer(layers, id, () => null);\n const loc = findLayer(without, anchor.id)!;\n return insertLayer(without, node, loc.parent?.id ?? null, to > from ? loc.index + 1 : loc.index);\n }\n\n /** Moves a layer to `toIndex` among the adjustment layers (0 = bottom) as one undo step. Returns false if nothing moved. */\n public moveAdjustmentLayer(id: string, toIndex: number): boolean {\n const next = this.moveAdjustmentNode(this.state.layers, id, toIndex);\n if (next === this.state.layers) return false;\n this.recordHistory();\n this.state.layers = next;\n this.notify();\n return true;\n }\n\n /** Shows or hides a layer (flips it without `enabled`) as one undo step. */\n public toggleAdjustmentLayer(id: string, enabled?: boolean): boolean {\n const layer = this.getAdjustmentLayer(id);\n if (!layer) return false;\n const next = enabled ?? !layer.enabled;\n if (next === layer.enabled) return false;\n return this.updateAdjustmentLayer(id, { enabled: next });\n }\n\n /** Removes every adjustment layer as one undo step. */\n public clearAdjustmentLayers(): void {\n const nodes = this.adjustmentNodes();\n if (!nodes.length) return;\n this.recordHistory();\n let layers = this.state.layers;\n for (const n of nodes) layers = mapLayer(layers, n.id, () => null);\n this.state.layers = layers;\n if (nodes.some((n) => n.id === this.state.activeLayerId)) this.state.activeLayerId = null;\n this.notify();\n }\n\n // --- Selection ---\n\n /** The active selection, or `null`. */\n public getSelection(): SelectionMask | null {\n return this.state.selection ?? null;\n }\n\n public get hasSelection(): boolean {\n return Boolean(this.state.selection?.bounds);\n }\n\n private get naturalSize(): { width: number; height: number } {\n return {\n width: Math.max(1, Math.round(this.state.imageDimensions.width)),\n height: Math.max(1, Math.round(this.state.imageDimensions.height)),\n };\n }\n\n private dropMismatchedSelection(): void {\n const sel = this.state.selection;\n if (!sel) return;\n const { width, height } = this.naturalSize;\n if (sel.width !== width || sel.height !== height) this.state.selection = null;\n }\n\n /**\n * Replaces the selection, or combines `mask` with it by `mode`, as one undo step. A mask of a\n * different size (e.g. an AI segmentation at another resolution) is resampled to the image.\n * Returns false when nothing changed.\n */\n public setSelection(mask: SelectionMask | null, mode: SelectionMode = 'replace'): boolean {\n const { width, height } = this.naturalSize;\n let next: SelectionMask | null = null;\n if (mask) {\n const sized = mask.width === width && mask.height === height ? mask : resizeSelection(mask, width, height);\n next = combineSelections(this.state.selection, sized, mode);\n } else if (mode !== 'replace') {\n return false;\n }\n const prev = normalizeSelection(this.state.selection ?? null);\n if (prev === next || (prev && next && sameSelection(prev, next))) return false;\n this.recordHistory();\n this.state.selection = next;\n this.notify();\n return true;\n }\n\n /** Select › All. */\n public selectAll(): boolean {\n const { width, height } = this.naturalSize;\n return this.setSelection(fullSelection(width, height));\n }\n\n /** Select › Deselect. */\n public deselect(): boolean {\n return this.setSelection(null);\n }\n\n /** Select › Inverse (with nothing selected, selects everything). */\n public invertSelection(): boolean {\n const { width, height } = this.naturalSize;\n const current = this.state.selection ?? createSelectionMask(width, height);\n return this.setSelection(invertSelectionMask(current));\n }\n\n private shapeCommand(mask: SelectionMask, options: SelectionCommandOptions): boolean {\n const shaped = options.feather && options.feather > 0 ? modifySelectionMask(mask, 'feather', options.feather) : mask;\n return this.setSelection(shaped, options.mode ?? 'replace');\n }\n\n /** Rectangular marquee in natural image pixels. */\n public selectRect(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(rectSelection(width, height, rect, options), options);\n }\n\n /** Elliptical marquee inscribed in `rect`. */\n public selectEllipse(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(ellipseSelection(width, height, rect, options), options);\n }\n\n /** Lasso / polygonal lasso. */\n public selectPolygon(points: ImagePoint[], options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(polygonSelection(width, height, points, options), options);\n }\n\n /**\n * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker\n * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)\n * these are also the Background layer's pixels.\n */\n public setSamplePixels(pixels: PixelBuffer | null): void {\n this.samplePixels = pixels;\n this.samplePixelsOverride = pixels !== null;\n }\n\n /**\n * The pixels the selection tools sample: the source image at natural size, or with\n * `sampleMerged` the composite of the layers (no crop, rotation or annotations). `null`\n * without pixel access (no DOM, tainted canvas).\n */\n public async getSamplePixels(options: SampleOptions = {}): Promise<PixelBuffer | null> {\n const { width, height } = this.naturalSize;\n let base = options.pixels ?? this.samplePixels;\n if (!base) {\n try {\n const canvas = await this.renderSource();\n const ctx = canvas.getContext('2d');\n const data = ctx?.getImageData?.(0, 0, width, height);\n if (data && data.data && data.data.length >= width * height * 4) {\n base = { width, height, data: data.data };\n this.samplePixels = base;\n this.samplePixelsOverride = false;\n }\n } catch {\n base = null;\n }\n }\n if (!base) return null;\n if (base.width !== width || base.height !== height) {\n if (!this.samplePixelsOverride && !options.pixels) this.samplePixels = null;\n return null;\n }\n const legacy = legacyView(this.stack(), width, height, null).exact;\n if (options.sample === 'active-layer') {\n const layer = this.getActiveLayer();\n if (!layer || layer.type !== 'pixel') {\n throw new LayerOperationError('Sampling the active layer needs an active pixel layer; select one, or sample the composite.');\n }\n const surface = layer.source === 'document' && options.pixels ? surfaceFromPixels(options.pixels, 4) : this.surfaceOf(layer);\n return { width, height, data: readRegion(surface, { x: -layer.offset.x, y: -layer.offset.y, width, height }) };\n }\n if (legacy && !options.sampleMerged) return base;\n this.syncViews();\n if (legacy && !options.pixels) {\n // The old shape: the source through the base adjustments and adjustment layers.\n const copy = { width: base.width, height: base.height, data: new Uint8ClampedArray(base.data as ArrayLike<number>) };\n applyAdjustments(copy as unknown as ImageData, legacyView(this.stack(), width, height, null).adjustments);\n return copy;\n }\n const pixels = options.pixels;\n const sources: CompositorSources = {\n width,\n height,\n surfaceOf: (l) => (l.type === 'pixel' && l.source === 'document' && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l)),\n version: pixels ? 'pixels' : this.sourceVersion(),\n };\n // Without `sampleMerged`, the pixels as painted: adjustment layers and filters left out.\n const stack = withoutAnnotations(this.state.layers);\n const layers = options.sampleMerged ? stack : pixelsOnly(stack);\n const data = new LayerCompositor().composite(layers, { x: 0, y: 0, width, height }, sources);\n return { width, height, data };\n }\n\n private async requirePixels(options: SampleOptions): Promise<PixelBuffer> {\n const pixels = await this.getSamplePixels(options);\n if (!pixels) throw new Error('The image pixels are not available (no canvas access); pass `pixels`.');\n return pixels;\n }\n\n /**\n * Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when\n * nothing is selected).\n */\n public async magicWand(\n point: ImagePoint,\n options: MagicWandOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(magicWand(pixels, point, options), options);\n return this.getSelection();\n }\n\n /** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */\n public async selectColorRange(\n samples: ColorRangeSample[],\n options: ColorRangeOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(colorRange(pixels, samples, options), options);\n return this.getSelection();\n }\n\n /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */\n public modifySelection(op: SelectionModifyOp, radius: number): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n return this.setSelection(modifySelectionMask(sel, op, radius));\n }\n\n /**\n * Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)\n * to the image's edges with a guided filter, then smooth / feather / contrast / shift edge.\n */\n public async refineSelection(params: RefineEdgeParams, options: SampleOptions = {}): Promise<boolean> {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;\n return this.setSelection(refineSelectionMask(pixels, sel, params));\n }\n\n /**\n * Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or\n * RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.\n */\n public async loadSelectionFromImage(\n source: string | PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number; mode?: SelectionMode } = {}\n ): Promise<SelectionMask | null> {\n let pixels: PixelBuffer;\n if (typeof source === 'string') {\n const img = await this.renderer.imageCache.get(source);\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const canvas = this.renderer.createCanvas(w, h);\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.drawImage(img, 0, 0, w, h);\n const data = ctx?.getImageData?.(0, 0, w, h);\n if (!data) throw new Error('The mask image pixels are not available (no canvas access); pass pixels.');\n pixels = { width: w, height: h, data: data.data };\n } else {\n pixels = source;\n }\n this.setSelection(selectionFromPixels(pixels, options), options.mode ?? 'replace');\n return this.getSelection();\n }\n\n /** Quick Mask view (tints the unselected area). View state: no undo step. */\n public setQuickMask(on: boolean): void {\n if (Boolean(this.state.quickMask) === on) return;\n this.state.quickMask = on;\n this.notify();\n }\n\n // --- Layer masks ---\n\n private maskFromSelection(sel: SelectionMask, props: LayerMaskProps = {}): RasterMask {\n const { width, height } = this.naturalSize;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const mask: RasterMask = { surface: surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0), defaultValue: 0 };\n return withMaskProps(mask, props);\n }\n\n /**\n * Adds the selection as the layer's mask (Reveal Selection), replacing any mask, as one undo\n * step. Without a selection, returns false.\n */\n public applySelectionAsLayerMask(layerId: string, props: LayerMaskProps = {}): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds || !this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, this.maskFromSelection(sel, props));\n }\n\n private setLayerMask(layerId: string, mask: RasterMask | null, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, layerId);\n if (!loc) return false;\n if (commitToHistory) this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n if (!mask && this.state.activeLayerId === layerId) this.state.activeTarget = 'pixels';\n this.notify();\n return true;\n }\n\n /** Sets (from a selection-shaped mask) or removes (`null`) a layer's mask as one undo step. */\n public setAdjustmentLayerMask(layerId: string, mask: SelectionMask | null, props: LayerMaskProps = {}): boolean {\n if (!this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, mask ? this.maskFromSelection(mask, props) : null);\n }\n\n /** Changes a layer mask's density (0..1), feather (px) or enabled flag; the mask pixels are kept. */\n public setAdjustmentLayerMaskProps(layerId: string, props: LayerMaskProps, commitToHistory = true): boolean {\n const layer = this.getLayer(layerId);\n if (!layer || layer.type !== 'adjustment' || !layer.mask) return false;\n return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);\n }\n\n /** Load Selection from a layer mask (its pixels; density and feather are not applied). */\n public selectionFromLayerMask(layerId: string, mode: SelectionMode = 'replace'): boolean {\n const layer = this.getLayer(layerId);\n if (!layer?.mask || layer.mask.source) return false;\n const { width, height } = this.naturalSize;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });\n const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));\n return this.setSelection(sel, mode);\n }\n\n // --- Histogram ---\n\n /**\n * Histogram of the last rendered preview (after adjustments and layers, before annotations),\n * or `null` before the first render or without canvas pixel access.\n */\n public getHistogram(): ImageHistogram | null {\n return this.renderer.previewHistogram;\n }\n\n /** Calls `listener` now and whenever a render produces a new preview histogram. */\n public subscribeHistogram(listener: (histogram: ImageHistogram | null) => void): () => void {\n this.histogramListeners.add(listener);\n listener(this.renderer.previewHistogram);\n return () => {\n this.histogramListeners.delete(listener);\n };\n }\n\n // --- Depth Masking (Gleamforge) ---\n\n /** Sets the depth mask (a parametric mask on the active pixel layer, or the Background) as one undo step. */\n public setDepthMask(partial: Partial<DepthMaskOptions>): void {\n this.recordHistory();\n this.syncViews();\n const target = this.developLayer();\n if (target) {\n const merged: DepthMaskOptions = { ...this.state.depthMask, ...partial };\n merged.depthRange = [...merged.depthRange] as [number, number];\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n if (isDefaultDepthMask(merged)) {\n if (next.mask?.source) delete next.mask;\n } else next.mask = depthRasterMask(merged);\n return next;\n });\n }\n this.notify();\n }\n\n public resetDepthMask(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target?.mask?.source) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n delete next.mask;\n return next;\n });\n }\n this.notify();\n }\n\n // --- Annotations & Layers ---\n\n private annotationLayer(id: string): AnnotationLayer | undefined {\n return flattenLayers(this.state.layers).find(\n (l): l is AnnotationLayer => l.type === 'annotation' && l.annotation.id === id\n );\n }\n\n /** Adds an annotation as its own layer (top of the stack unless `aboveId` is given). */\n public addAnnotation(annotation: CreateAnnotationPayload, placement: AnnotationPlacement = {}): string {\n this.recordHistory();\n const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n const fullAnnotation = { ...annotation, id } as Annotation;\n const layer = annotationLayerFor(fullAnnotation);\n const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;\n this.state.layers = loc\n ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1)\n : insertLayer(this.state.layers, layer, null);\n this.state.selectedAnnotationId = id;\n this.notify();\n return id;\n }\n\n public updateAnnotation(id: string, partial: Partial<Annotation>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const layer = this.annotationLayer(id);\n if (layer) {\n const annotation = { ...layer.annotation, ...partial, id } as Annotation;\n const name = layer.name === annotationLayerName(layer.annotation) ? annotationLayerName(annotation) : layer.name;\n this.state.layers = mapLayer(this.state.layers, layer.id, () => ({ ...layer, annotation, name }));\n }\n this.notify();\n }\n\n public removeAnnotation(id: string): void {\n this.recordHistory();\n const layer = this.annotationLayer(id);\n if (layer) this.state.layers = mapLayer(this.state.layers, layer.id, () => null);\n if (this.state.selectedAnnotationId === id) {\n this.state.selectedAnnotationId = null;\n }\n this.notify();\n }\n\n public clearAnnotations(): void {\n this.recordHistory();\n let layers = this.state.layers;\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.layers = layers;\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n public selectAnnotation(id: string | null): void {\n this.state.selectedAnnotationId = id;\n this.notify();\n }\n\n // --- Zoom & Pan ---\n\n public setZoom(zoom: number): void {\n this.state.zoom = Math.max(0.1, Math.min(10, zoom));\n this.notify();\n }\n\n public zoomIn(factor = 1.2): void {\n this.setZoom(this.state.zoom * factor);\n }\n\n public zoomOut(factor = 1.2): void {\n this.setZoom(this.state.zoom / factor);\n }\n\n public resetZoom(): void {\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.notify();\n }\n\n public setPan(pan: ImagePoint): void {\n this.state.pan = pan;\n this.notify();\n }\n\n public setComparing(comparing: boolean): void {\n if (this.state.isOriginalCompared === comparing) return;\n this.state.isOriginalCompared = comparing;\n this.notify();\n }\n\n public setFit(fit: ImageEditorFit): void {\n if (this.state.fit === fit) return;\n this.state.fit = fit;\n this.notify();\n }\n\n // --- Rendering & Export ---\n\n private blankSource: HTMLCanvasElement | null = null;\n\n /**\n * What the renderer draws from: the source image, or, for a document without one (an opened\n * PSD, a v3 document loaded on its own), a transparent canvas of the document size, since every\n * pixel then comes from the layers. `null` when there is nothing to draw.\n */\n private drawSource(): CanvasImageSource | string | null {\n const source = this.imageElement || this.state.sourceUrl;\n if (source) return source;\n if (!this.isSourceless()) return null;\n const { width, height } = this.naturalSize;\n if (!this.blankSource || this.blankSource.width !== width || this.blankSource.height !== height) {\n this.blankSource = this.renderer.createCanvas(width, height);\n }\n return this.blankSource;\n }\n\n public async render(targetCanvas: HTMLCanvasElement, options?: RenderOptions): Promise<void> {\n const source = this.drawSource();\n if (!source) return;\n this.syncViews();\n const dirty = this.pendingDirty;\n this.pendingDirty = [];\n try {\n await this.renderer.renderToCanvas(targetCanvas, source, this.state, {\n ...options,\n dirty: dirty === 'all' ? undefined : dirty,\n checkpointLayerId: this.state.activeLayerId,\n });\n } catch {\n // In non-DOM or test mock environments without image loading, safely ignore\n }\n }\n\n public async renderExport(options?: ExportOptions): Promise<HTMLCanvasElement> {\n const source = this.drawSource();\n if (!source) {\n return this.renderer.createCanvas(1, 1);\n }\n this.syncViews();\n if (options?.layerIds) {\n const keep = new Set(options.layerIds);\n const layers = flattenLayers(this.state.layers).filter((l) => keep.has(l.id));\n return this.renderer.renderExport(source, { ...this.state, layers }, options);\n }\n return this.renderer.renderExport(source, this.state, options);\n }\n\n /**\n * Renders the edited image (adjustments, layers and, by default, annotations) in the\n * source image's own orientation and frame, ignoring crop, rotation and flips, so it lines\n * up with natural-pixel coordinates (selections, `select_region`). `scale` ≤ 1 shrinks it.\n * Returns `null` when no image is loaded.\n */\n public async renderSourceView(options: { scale?: number; includeAnnotations?: boolean } = {}): Promise<HTMLCanvasElement | null> {\n const source = this.drawSource();\n if (!source) return null;\n this.syncViews();\n const { width, height } = this.state.imageDimensions;\n const k = Math.min(1, Math.max(0, options.scale ?? 1));\n return this.renderer.renderExport(\n source,\n { ...this.state, crop: null, transform: { rotate: 0, flipH: false, flipV: false } },\n {\n width: Math.max(1, Math.round(width * k)),\n height: Math.max(1, Math.round(height * k)),\n includeAnnotations: options.includeAnnotations,\n }\n );\n }\n\n public async toDataURL(options: ExportOptions = {}): Promise<string> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n return canvas.toDataURL(format, quality);\n }\n\n public async toBlob(options: ExportOptions = {}): Promise<Blob> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n\n return new Promise((resolve, reject) => {\n if (canvas.toBlob) {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob);\n else reject(new Error('Failed to create Blob from canvas'));\n }, format, quality);\n } else {\n // Fallback for environments lacking canvas.toBlob\n try {\n const dataUrl = canvas.toDataURL(format, quality);\n const parts = dataUrl.split(',');\n const byteStr = atob(parts[1]);\n const ab = new ArrayBuffer(byteStr.length);\n const ia = new Uint8Array(ab);\n for (let i = 0; i < byteStr.length; i++) {\n ia[i] = byteStr.charCodeAt(i);\n }\n resolve(new Blob([ab], { type: format }));\n } catch (err) {\n reject(err);\n }\n }\n });\n }\n\n // --- AI Masking & Transformation Helpers ---\n\n public async renderMask(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<HTMLCanvasElement> {\n this.syncViews();\n return this.renderer.renderMask(this.state, options);\n }\n\n public async toMaskDataURL(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<string> {\n const canvas = await this.renderMask(options);\n return canvas.toDataURL('image/png');\n }\n\n /**\n * Renders the current source image at natural size with no crop, rotation, flip,\n * adjustments or annotations. This is the image that `toMaskDataURL()` is aligned with\n * and the space that `applyInpaintedImage` / `applyBackgroundRemovedImage` expect back.\n */\n public async renderSource(): Promise<HTMLCanvasElement> {\n const { width, height } = this.state.imageDimensions;\n const canvas = this.renderer.createCanvas(width, height);\n const source = this.imageElement || this.state.sourceUrl;\n const ctx = canvas.getContext('2d');\n if (!ctx || !source) return canvas;\n const img = typeof source === 'string' ? await this.renderer.imageCache.get(source) : source;\n ctx.drawImage(img, 0, 0, width, height);\n return canvas;\n }\n\n /**\n * The image an AI hook should edit: the source (`toSourceDataURL`) when results replace the\n * Background, or the composite of the layers without annotations, in the source frame, when\n * they go to a new layer or mask (see `applyInpaintedImage`).\n */\n public async toAIInputDataURL(options: AIResultOptions = {}): Promise<string> {\n const target = options.target ?? this.defaultAITarget();\n if (target === 'replace') return this.toSourceDataURL();\n const canvas = await this.renderSourceView({ includeAnnotations: false });\n return canvas ? canvas.toDataURL('image/png') : this.toSourceDataURL();\n }\n\n public async toSourceDataURL(options: Pick<ExportOptions, 'format' | 'quality'> = {}): Promise<string> {\n const canvas = await this.renderSource();\n return canvas.toDataURL(options.format || 'image/png', options.quality ?? 0.92);\n }\n\n /**\n * Whether AI results go to layers: the document has more than one pixel layer or group (adjustment\n * and annotation layers are edits of the image, not images of their own).\n */\n /** `new-layer` in multi-layer documents and documents without a source image, else `replace`. */\n private defaultAITarget(): 'new-layer' | 'replace' {\n return this.isMultiLayer || this.isSourceless() ? 'new-layer' : 'replace';\n }\n\n /** No source image, and no layer reading one (an opened PSD): the layers are the whole document. */\n private isSourceless(): boolean {\n if (this.imageElement || this.state.sourceUrl || !this.state.layers.length) return false;\n return !flattenLayers(this.state.layers).some((l) => l.type === 'pixel' && l.source === 'document');\n }\n\n public get isMultiLayer(): boolean {\n return flattenLayers(this.state.layers).filter((l) => l.type === 'pixel' || l.type === 'group').length > 1;\n }\n\n /**\n * Applies an inpainted result (natural image space, see `toSourceDataURL`) as ONE undoable step.\n * In a document with several layers (see `isMultiLayer`), or with `target: 'new-layer'`, the\n * result goes to a new pixel layer above the active layer, limited to the selection. Otherwise\n * (`target: 'replace'`, the single-layer default) it replaces the Background's pixels and clears\n * the mask annotations, as before. Resolves to the new layer's id in new-layer mode.\n */\n public async applyInpaintedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, true);\n return;\n }\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const data = new Uint8ClampedArray(px.data as ArrayLike<number>);\n const sel = this.state.selection?.bounds ? this.state.selection : null;\n if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round((data[i] * sel.data[p]) / 255);\n return this.addPixelLayer({ name: 'Inpaint', pixels: { width, height, data } });\n }\n\n /**\n * Applies a background-removed result as ONE undoable step. In a document with several layers\n * (or with `target: 'new-layer'`) the result's alpha becomes the mask of the active pixel layer\n * (or the Background), replacing its mask. Otherwise (`target: 'replace'`, the single-layer\n * default) it replaces the Background's pixels; crop and annotations are kept (rescaled if the\n * new image has different dimensions). Resolves to the masked layer's id in mask mode.\n */\n public async applyBackgroundRemovedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, false);\n return;\n }\n const target = this.developLayer();\n if (!target) throw new LayerOperationError('There is no pixel layer to mask.');\n requireUnlocked(target, 'all', 'mask it');\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const alpha = new Uint8ClampedArray(width * height);\n for (let p = 0; p < alpha.length; p++) alpha[p] = (px.data as ArrayLike<number>)[p * 4 + 3];\n let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);\n if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);\n this.commitLayers(setLayerMaskOp(this.state.layers, target.id, { surface, defaultValue: 0 }));\n return target.id;\n }\n\n /**\n * Rasterize Layer: an annotation layer becomes a pixel layer with the same id, name and\n * properties (needs a canvas), and a pixel layer's filters are baked into its pixels. One undo\n * step. Returns the layer id.\n */\n public async rasterizeLayer(id: string): Promise<string> {\n const layer = requireLayer(this.state.layers, id).layer;\n requireUnlocked(layer, 'all', 'rasterize it');\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n let next: PixelLayer;\n if (layer.type === 'annotation') {\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n const surface = await this.compositeToSurface([content], this.paintBounds(layer));\n next = { type: 'pixel', id: layer.id, name: layer.name, visible: layer.visible, opacity: layer.opacity, blend, surface, offset: { x: 0, y: 0 } };\n if (layer.clipped) next.clipped = true;\n if (layer.mask) next.mask = layer.mask;\n if (layer.locks) next.locks = layer.locks;\n if (this.state.selectedAnnotationId === layer.annotation.id) this.state.selectedAnnotationId = null;\n } else if (layer.type === 'pixel') {\n if (!layer.filters?.length) throw new LayerOperationError(`Layer \"${layer.name || id}\" is already pixels with no filters to bake.`);\n requireUnlocked(layer, 'pixels', 'rasterize it');\n const spatial = layer.filters.some((f) => f.enabled && f.kind === 'blur');\n const content: PixelLayer = { ...layer, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n const rect = spatial ? canvas : this.getLayerBounds(id) ?? canvas;\n const surface = await this.compositeToSurface([content], rect);\n next = { ...layer, surface, offset: { x: 0, y: 0 } };\n delete next.filters;\n delete next.source;\n if (layer.mask && (layer.offset.x || layer.offset.y)) {\n // The mask lives in layer space: keep it where it was in the document.\n next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };\n }\n } else if (layer.type === 'smart') {\n return this.rasterizeSmartObject(id);\n } else {\n throw new LayerOperationError(`Only annotation layers, smart objects and pixel layers with filters rasterize; \"${layer.name || id}\" is a ${layer.type} layer. Merge it down or flatten instead.`);\n }\n this.commitLayers(mapLayer(this.state.layers, id, () => next));\n return id;\n }\n\n // --- PSD ---\n\n /** Pixels per inch of the last opened PSD (written back by `toPsd`). */\n private psdResolution: number | undefined;\n\n /**\n * Opens PSD / PSB bytes as the document (one undo step): the layers replace the stack and the\n * document has no source image afterwards (every pixel comes from the layers). What the model\n * cannot hold is approximated or left out; `warnings` says what, naming the layers.\n * Throws `PsdError` for files that cannot be read at all.\n */\n public loadPsd(bytes: Uint8Array): { layerIds: string[]; warnings: string[]; width: number; height: number; summary: PsdImportResult['summary'] } {\n const result = importPsd(bytes, { newId: () => newLayerId() });\n this.abandonContents();\n this.recordHistory();\n const s = this.state;\n this.imageElement = null;\n s.sourceUrl = null;\n s.imageDimensions = { width: result.width, height: result.height };\n if (!this.samplePixelsOverride) this.samplePixels = null;\n s.layers = result.layers;\n s.crop = null;\n s.transform = { ...DEFAULT_TRANSFORM_VALUES };\n s.outputOpacity = 1;\n delete s.legacyOutputBlur;\n s.selection = null;\n s.selectedAnnotationId = null;\n const pixels = flattenLayers(result.layers).filter((l) => l.type === 'pixel');\n s.activeLayerId = pixels.length ? pixels[pixels.length - 1].id : null;\n s.activeTarget = 'pixels';\n this.psdResolution = result.resolution;\n if (result.patterns.length) s.patterns = result.patterns;\n else delete s.patterns;\n if (result.globalLight) s.globalLight = result.globalLight;\n else delete s.globalLight;\n this.lastLoadProblems = result.warnings;\n this.notify();\n return {\n layerIds: flattenLayers(result.layers).map((l) => l.id),\n warnings: result.warnings,\n width: result.width,\n height: result.height,\n summary: result.summary,\n };\n }\n\n /**\n * The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked\n * into pixels and masks, annotation layers are rasterized (in a browser; elsewhere they are left\n * out), and the merged image is our composite. `warnings` lists every such change.\n */\n public async toPsd(): Promise<{ bytes: Uint8Array; warnings: string[]; psb: boolean; width: number; height: number }> {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n const warnings: string[] = [];\n const note = new Map<string, string[]>();\n const add = (msg: string, name: string) => note.set(msg, [...(note.get(msg) ?? []), name]);\n const canRasterize = this.renderer.canRasterize();\n await this.renderer.prepareLayerAssets(this.state.layers, width, height);\n const sources = this.renderer.compositorSources(width, height);\n const compositor = new LayerCompositor();\n\n const bakeMask = (layer: ImageLayer): RasterMask | undefined => {\n const mask = layer.mask;\n if (!mask?.source) return mask;\n add('Parametric (depth) masks were baked into pixel masks', layer.name);\n const plain = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } } as ImageLayer;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const coverage = compositor.maskCoverage(plain, canvas, sources);\n const bytes = new Uint8ClampedArray(width * height);\n if (coverage) for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(coverage[i] * 255);\n else bytes.fill(255);\n const baked: RasterMask = { surface: surfaceFromPixels({ width, height, data: bytes }, 1, -ox, -oy), defaultValue: 0 };\n if (mask.density !== undefined && mask.density < 1) baked.density = mask.density;\n if (mask.feather) baked.feather = mask.feather;\n if (mask.enabled === false) baked.enabled = false;\n return baked;\n };\n\n const prepare = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n const p = await prepareLayer(layer);\n // `exportPsd` writes it as Normal; the merged image keeps the paint mix.\n if (p?.blend === 'pigment') add('The Pigment blend mode has no PSD equivalent and was written as Normal', layer.name);\n return p;\n };\n\n const prepareLayer = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n if (layer.type === 'group') {\n const children: ImageLayer[] = [];\n for (const c of layer.children) {\n const p = await prepare(c);\n if (p) children.push(p);\n }\n return { ...layer, children, mask: bakeMask(layer) } as ImageLayer;\n }\n if (layer.type === 'adjustment') return { ...layer, mask: bakeMask(layer) } as ImageLayer;\n if (layer.type === 'annotation') {\n if (!canRasterize) {\n add('Annotation layers need a canvas (a browser) to rasterize and were left out', layer.name);\n return null;\n }\n add('Annotation layers were written as pixel layers', layer.name);\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n delete content.effects;\n const data = compositor.composite([content], canvas, sources);\n const out: PixelLayer = {\n type: 'pixel',\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend,\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n if (layer.clipped) out.clipped = true;\n if (layer.locks) out.locks = layer.locks;\n if (layer.effects) out.effects = layer.effects;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n return out;\n }\n if (layer.type === 'smart') {\n add('Smart objects were written as pixel layers (their rendered pixels)', layer.name);\n const px = rasterizeSmartObjectOp([layer], layer.id).layer;\n const mask = bakeMask(px);\n if (mask) px.mask = mask;\n return px;\n }\n let out: PixelLayer = { ...layer };\n if (layer.source === 'document') {\n out.surface = this.surfaceOf(layer);\n delete out.source;\n }\n const active = layer.filters?.filter(isActiveFilter) ?? [];\n if (active.length) {\n add('Layer filters (develop, blur) were baked into the pixels', layer.name);\n const content: PixelLayer = { ...out, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n delete content.effects;\n const data = compositor.composite([content], canvas, sources);\n out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };\n }\n if (layer.filters?.some((f) => !f.enabled)) add('Disabled layer filters were left out', layer.name);\n delete out.filters;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\n };\n\n const prepared: ImageLayer[] = [];\n for (const l of this.state.layers) {\n const p = await prepare(l);\n if (p) prepared.push(p);\n }\n const s = this.state;\n if (s.crop) warnings.push('The crop is not applied; the PSD is the uncropped document.');\n if (s.transform.rotate || s.transform.flipH || s.transform.flipV) warnings.push('Rotation and flips are not applied; the PSD is in the source orientation.');\n if (s.outputOpacity < 1) warnings.push('The document opacity is not written; layers keep their own opacity.');\n if (s.legacyOutputBlur) warnings.push('The document-level blur of an old document is not written.');\n for (const [msg, names] of note) warnings.push(`${msg} (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`);\n\n const merged = compositor.composite(prepared, canvas, { ...sources, version: `psd:${Date.now()}` });\n const result = exportPsd({ width, height, layers: prepared, merged, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight });\n warnings.push(...result.warnings);\n return { bytes: result.bytes, warnings, psb: result.psb, width, height };\n }\n\n private async replaceSource(src: string, clearAnnotations: boolean): Promise<void> {\n const next = await this.resolveSource(src);\n this.recordHistory();\n\n const prev = this.state.imageDimensions;\n const sx = next.dimensions.width / (prev.width || next.dimensions.width);\n const sy = next.dimensions.height / (prev.height || next.dimensions.height);\n\n this.imageElement = next.element;\n this.state.sourceUrl = next.sourceUrl;\n this.state.imageDimensions = next.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (this.state.selection && (sx !== 1 || sy !== 1)) {\n this.state.selection = normalizeSelection(\n resizeSelection(this.state.selection, Math.round(next.dimensions.width), Math.round(next.dimensions.height))\n );\n }\n\n let layers = this.state.layers;\n if (sx !== 1 || sy !== 1) {\n if (this.state.crop) {\n this.state.crop = clampCropToImage(\n {\n x: Math.round(this.state.crop.x * sx),\n y: Math.round(this.state.crop.y * sy),\n width: Math.round(this.state.crop.width * sx),\n height: Math.round(this.state.crop.height * sy),\n },\n next.dimensions.width,\n next.dimensions.height\n );\n }\n layers = resizeLegacyMasks(layers, prev, next.dimensions);\n for (const l of flattenLayers(layers)) {\n if (l.type === 'annotation') {\n layers = mapLayer(layers, l.id, () => ({ ...l, annotation: scaleAnnotation(l.annotation, sx, sy) }));\n }\n }\n }\n // The Background now shows the new source.\n const bg = developTarget(layers, null);\n if (bg && bg.source !== 'document') layers = mapLayer(layers, bg.id, () => ({ ...bg, source: 'document' }));\n\n if (clearAnnotations) {\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.selectedAnnotationId = null;\n }\n this.state.layers = layers;\n this.notify();\n }\n\n // --- Serialization ---\n\n /**\n * The serializable state. A document whose layers still have the v1 / v2 shape (the\n * Background with its sliders and depth mask, adjustment layers, annotations) is written as\n * v1 / v2 exactly as before; anything else is written as v3 with the layer pixels as PNG\n * assets. `selection` is included (run-length encoded) only when something is selected.\n */\n public toJSON(): SerializedImageEditorState;\n /**\n * With `assets: 'external'`, layer pixels go to `store` (each PNG uploaded once per store) and\n * the v3 document holds the store's references. Old-shape documents have no assets either way.\n */\n public toJSON(options: { assets: 'external'; store: ImageEditorAssetStore }): Promise<SerializedImageEditorState>;\n public toJSON(options?: { assets: 'external'; store: ImageEditorAssetStore }): SerializedImageEditorState | Promise<SerializedImageEditorState> {\n // While contents are open, the document is the one set aside (without the open edits).\n const root = this.contentsStack[0]?.saved;\n const current = this.state;\n const element = this.imageElement;\n if (root) {\n this.state = root.state;\n this.imageElement = root.element;\n this.viewKey = [];\n }\n let out: SerializedImageEditorState;\n try {\n out = this.serializeState();\n if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);\n } finally {\n if (root) {\n this.state = current;\n this.imageElement = element;\n this.viewKey = [];\n }\n }\n if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;\n return serializeLayersExternal(this.rootLayers(), options.store).then(({ layers, assets, smartContents }) => (smartContents ? { ...out, layers, assets, smartContents } : { ...out, layers, assets }));\n }\n\n /**\n * Loads a stored state whose v3 assets may live in an asset store (references other than data\n * URLs are fetched with `store.get`), as one undo step. Assets that cannot be fetched are left\n * out and listed in `lastLoadProblems`.\n */\n public async loadJSONAsync(data: SerializedImageEditorState, options: { store?: ImageEditorAssetStore } = {}): Promise<void> {\n if (data.version !== LAYERED_SCHEMA_VERSION || !data.assets) {\n this.loadJSON(data);\n return;\n }\n const problems: string[] = [];\n const assets = await resolveExternalAssets(data.assets, options.store, problems);\n this.loadJSON({ ...data, assets });\n this.lastLoadProblems = [...problems, ...this.lastLoadProblems];\n }\n\n /** Loads a stored state of any version (one undo step). */\n public loadJSON(data: SerializedImageEditorState): void {\n this.abandonContents();\n this.recordHistory();\n this.applyStored(data);\n let selection: SelectionMask | null = null;\n if (data.selection) {\n try {\n selection = normalizeSelection(deserializeSelection(data.selection));\n } catch {\n selection = null;\n }\n }\n this.state.selection = selection;\n this.dropMismatchedSelection();\n this.notify();\n }\n\n /** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */\n public lastLoadProblems: string[] = [];\n\n private applyStored(raw: Partial<SerializedImageEditorState>): void {\n const s = this.state;\n const data = migrateSerializedImageEditorState(raw as SerializedImageEditorState);\n this.lastLoadProblems = [];\n if (isLayered(data)) {\n const problems: string[] = [];\n const canvas = typeof data.width === 'number' && data.width > 0 && typeof data.height === 'number' && data.height > 0 ? { width: data.width, height: data.height } : this.naturalSize;\n s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems, smartContents: Array.isArray(data.smartContents) ? data.smartContents : [], canvas });\n this.lastLoadProblems = problems;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.outputOpacity = data.opacity;\n if (typeof data.legacyOutputBlur === 'number' && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;\n const light = data.globalLight;\n if (light && Number.isFinite(light.angle) && Number.isFinite(light.altitude)) s.globalLight = { angle: light.angle, altitude: Math.min(90, Math.max(0, light.altitude)) };\n else delete s.globalLight;\n const patterns = deserializePatterns(data.patterns, problems);\n if (patterns.length) s.patterns = patterns;\n else delete s.patterns;\n if (!this.imageElement && typeof data.width === 'number' && typeof data.height === 'number' && data.width > 0 && data.height > 0) {\n s.imageDimensions = { width: data.width, height: data.height };\n }\n } else {\n const { width, height } = this.naturalSize;\n const stack = layersFromLegacy(data, width, height);\n s.layers = stack.layers;\n s.outputOpacity = stack.outputOpacity;\n if (stack.legacyOutputBlur !== undefined) s.legacyOutputBlur = stack.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.activeLayerId = null;\n delete s.globalLight;\n delete s.patterns;\n }\n s.activeTarget = 'pixels';\n this.viewKey = [];\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) s.selectedAnnotationId = null;\n }\n\n private serializeState(): SerializedImageEditorState {\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, null);\n // A global light is only stored in the layered schema.\n if (view.exact && !this.state.globalLight && !this.state.patterns?.length) {\n return {\n // States without adjustment layers keep schema 1, so they round-trip unchanged.\n version: view.adjustments.layers?.length ? 2 : 1,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n adjustments: serializeAdjustments(view.adjustments),\n depthMask: {\n ...view.depthMask,\n depthRange: [...view.depthMask.depthRange] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(view.annotations)),\n };\n }\n const { layers, assets, smartContents } = serializeLayers(this.state.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width,\n height,\n layers,\n assets,\n activeLayerId: this.state.activeLayerId,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n opacity: this.state.outputOpacity,\n };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };\n if (this.state.patterns?.length) out.patterns = serializePatterns(this.state.patterns);\n if (smartContents) out.smartContents = smartContents;\n return out;\n }\n}\n\n/**\n * Legacy-process adjustments are saved without `process` (and without an unused zero `tint`), so\n * they serialize exactly as they did before process versions existed.\n */\nfunction serializeAdjustments(adjustments: ImageAdjustments): ImageAdjustments {\n const out: ImageAdjustments = { ...adjustments };\n if (out.layers?.length) out.layers = JSON.parse(JSON.stringify(out.layers));\n else delete out.layers;\n if (normalizeProcessVersion(out.process) === LEGACY_PROCESS_VERSION) {\n delete out.process;\n if (!out.tint) delete out.tint;\n }\n return out;\n}\n\n/** A pixel layer with its develop filter updated from base-slider values (created if missing). */\nfunction withDevelop(layer: PixelLayer, partial: Partial<ImageAdjustments>): PixelLayer {\n const filters = [...(layer.filters ?? [])];\n let i = filters.findIndex((f) => f.kind === 'develop');\n if (i === -1) {\n filters.unshift({\n id: layer.source === 'document' ? DEVELOP_FILTER_ID : newLayerId('flt'),\n kind: 'develop',\n enabled: true,\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n process: LATEST_PROCESS_VERSION,\n });\n i = 0;\n }\n const next = { ...(filters[i] as DevelopFilter) };\n for (const k of DEVELOP_KEYS) {\n const v = partial[k];\n if (typeof v === 'number' && Number.isFinite(v)) next[k] = v;\n }\n if ('process' in partial) next.process = normalizeProcessVersion(partial.process);\n filters[i] = next;\n return { ...layer, filters };\n}\n\n/** A pixel layer with its blur filter set to `radius` (removed at 0). */\nfunction withBlur(layer: PixelLayer, radius: number): PixelLayer {\n const existing = layer.filters?.find((f) => f.kind === 'blur');\n const filters: LayerFilter[] = (layer.filters ?? []).filter((f) => f.kind !== 'blur');\n if (radius > 0) {\n filters.push({\n id: existing?.id ?? (layer.source === 'document' ? BLUR_FILTER_ID : newLayerId('flt')),\n kind: 'blur',\n enabled: true,\n radius,\n });\n }\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return next;\n}\n\n/** Updates a node from an old-shape layer (`setAdjustments({ layers })`), keeping a raster mask whose pixels did not change. */\nfunction mergeAdjustmentNode(node: AdjustmentLayerNode, update: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const next = {\n ...node,\n kind: update.kind,\n params: JSON.parse(JSON.stringify(update.params)),\n visible: update.enabled !== false,\n opacity: update.opacity,\n blend: update.blend,\n name: update.name ?? '',\n } as AdjustmentLayerNode;\n if (!update.mask) delete next.mask;\n else {\n const current = node.mask && !node.mask.source ? node.mask : null;\n const memo = current ? legacyMaskOf(current, width, height) : null;\n if (current && memo && memo.width === update.mask.width && memo.height === update.mask.height && memo.rle === update.mask.rle) {\n next.mask = withMaskProps(current, {\n density: update.mask.density ?? 1,\n feather: update.mask.feather ?? 0,\n enabled: update.mask.enabled !== false,\n });\n } else {\n const m = rasterMaskFromLegacy(update.mask, width, height);\n if (m) next.mask = m;\n else delete next.mask;\n }\n }\n return next;\n}\n\n/** Applies mask property changes; `undefined` fields keep their value, defaults are left out. */\nfunction withMaskProps(mask: RasterMask, props: LayerMaskProps): RasterMask {\n const next: RasterMask = { ...mask };\n if (props.density !== undefined) {\n const d = Math.min(1, Math.max(0, props.density));\n if (d !== 1) next.density = d;\n else delete next.density;\n }\n if (props.feather !== undefined) {\n const f = Math.min(1000, Math.max(0, props.feather));\n if (f) next.feather = f;\n else delete next.feather;\n }\n if (props.enabled !== undefined) {\n if (props.enabled) delete next.enabled;\n else next.enabled = false;\n }\n if (props.linked !== undefined) {\n if (props.linked) delete next.linked;\n else next.linked = false;\n }\n return next;\n}\n\n/** Rescales adjustment-layer masks (canvas-sized) when the canvas size changes. */\nfunction resizeLegacyMasks(layers: ImageLayer[], from: ImageDimensions, to: ImageDimensions): ImageLayer[] {\n const fw = Math.max(1, Math.round(from.width));\n const fh = Math.max(1, Math.round(from.height));\n const tw = Math.max(1, Math.round(to.width));\n const th = Math.max(1, Math.round(to.height));\n if (fw === tw && fh === th) return layers;\n let out = layers;\n for (const l of flattenLayers(layers)) {\n if (l.type !== 'adjustment' || !l.mask || l.mask.source) continue;\n const old = l.mask;\n const mask = rasterMaskFromLegacy(legacyMaskOf(old, fw, fh), tw, th);\n if (!mask) continue;\n out = mapLayer(out, l.id, (n) => ({ ...n, mask: { ...mask, density: old.density, feather: old.feather, enabled: old.enabled } }));\n }\n return out;\n}\n\n/** Whether every pixel of `surface` inside the canvas is opaque. */\nfunction isOpaqueInside(surface: PixelSurface, width: number, height: number): boolean {\n const row = new Uint8ClampedArray(width * 4);\n for (let y = 0; y < height; y++) {\n readRegion(surface, { x: 0, y, width, height: 1 }, row);\n for (let i = 3; i < row.length; i += 4) if (row[i] !== 255) return false;\n }\n return true;\n}\n\nfunction collectStateTiles(layers: readonly ImageLayer[], into: Set<Tile>): void {\n for (const l of flattenLayers(layers)) {\n if (l.type === 'pixel' && l.source !== 'document') collectTiles(l.surface, into);\n if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);\n if (l.type === 'smart') {\n collectTiles(l.surface, into);\n if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);\n if (l.contents.raster) collectTiles(l.contents.raster, into);\n if (l.contents.document) collectStateTiles(l.contents.document.layers, into);\n }\n }\n}\n\n/** The stack without adjustment layers and layer filters (the pixels as painted). */\nfunction pixelsOnly(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'adjustment') continue;\n if (l.type === 'group') out.push({ ...l, children: pixelsOnly(l.children) });\n else if (l.type === 'pixel' && l.filters?.length) {\n const p = { ...l };\n delete p.filters;\n out.push(p);\n } else out.push(l);\n }\n return out;\n}\n\n/** Bilinear resample of RGBA pixels to `width × height`. */\nexport function resizePixels(px: PixelBuffer, width: number, height: number): PixelBuffer {\n const src = px.data as ArrayLike<number>;\n const out = new Uint8ClampedArray(width * height * 4);\n const kx = px.width / width;\n const ky = px.height / height;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(px.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(px.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(px.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(px.width - 1, x0 + 1);\n const tx = fx - x0;\n for (let c = 0; c < 4; c++) {\n const a = src[(y0 * px.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px.width + x1) * 4 + c] * tx;\n const b = src[(y1 * px.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px.width + x1) * 4 + c] * tx;\n out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;\n }\n }\n }\n return { width, height, data: out };\n}\n\nfunction withoutAnnotations(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: withoutAnnotations(l.children) } : l);\n }\n return out;\n}\n\n/** A colour as RGBA bytes: `#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` (0..255). */\nexport function parseColor(value: string | number[]): [number, number, number, number] {\n if (Array.isArray(value)) {\n if (value.length < 3 || value.length > 4 || !value.every((v) => typeof v === 'number' && v >= 0 && v <= 255)) {\n throw new LayerOperationError('A colour array is [r, g, b] or [r, g, b, a] with values 0..255.');\n }\n return [Math.round(value[0]), Math.round(value[1]), Math.round(value[2]), Math.round(value[3] ?? 255)];\n }\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(String(value).trim());\n if (!m) throw new LayerOperationError(`Invalid colour \"${value}\"; use #rrggbb or #rrggbbaa.`);\n let hex = m[1];\n if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');\n const n = (i: number) => parseInt(hex.slice(i, i + 2), 16);\n return [n(0), n(2), n(4), hex.length === 8 ? n(6) : 255];\n}\n\n/** `#rrggbb` of RGBA bytes (alpha dropped). */\nexport function toHex(c: readonly number[]): string {\n return '#' + [c[0], c[1], c[2]].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');\n}\n\n/** `null` for an empty selection, so \"nothing selected\" has one representation. */\nfunction normalizeSelection(mask: SelectionMask | null): SelectionMask | null {\n return mask?.bounds ? mask : null;\n}\n\nfunction sameSelection(a: SelectionMask, b: SelectionMask): boolean {\n if (a.width !== b.width || a.height !== b.height) return false;\n const x = a.data;\n const y = b.data;\n for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;\n return true;\n}\n\n/** Scales an annotation's geometry from one image size to another. */\nfunction scaleAnnotation(ann: Annotation, sx: number, sy: number): Annotation {\n const k = Math.sqrt(Math.abs(sx * sy));\n const base = { ...ann, x: ann.x * sx, y: ann.y * sy } as Annotation;\n switch (base.type) {\n case 'pen':\n return { ...base, points: base.points.map((p) => ({ x: p.x * sx, y: p.y * sy })), strokeWidth: base.strokeWidth * k };\n case 'rect':\n return { ...base, width: base.width * sx, height: base.height * sy, strokeWidth: base.strokeWidth * k };\n case 'image':\n return { ...base, width: base.width * sx, height: base.height * sy };\n case 'circle':\n return { ...base, radiusX: base.radiusX * sx, radiusY: base.radiusY * sy, strokeWidth: base.strokeWidth * k };\n case 'arrow':\n case 'line':\n return { ...base, endX: base.endX * sx, endY: base.endY * sy, strokeWidth: base.strokeWidth * k };\n case 'text':\n return { ...base, fontSize: base.fontSize * k };\n }\n}\n","import {\n loadDocument,\n type JsonObject,\n type LoadReport,\n type MigrationStep,\n} from '../persistence/migrate';\nimport { DEFAULT_DEPTH_MASK, DEFAULT_TRANSFORM, normalizeStoredAdjustments } from './controller';\nimport type {\n Annotation,\n CropBox,\n DepthMaskOptions,\n ImageAdjustments,\n ImageTransform,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { deserializeSelection } from './selection/mask';\nimport type { SerializedLayer, SerializedLayeredImageEditorState } from './layers/serialize';\n\n/**\n * The newest `version` that `ImageEditorController.toJSON()` writes (3: layers). Documents whose\n * layers still have the old shape are written as version 1 or 2.\n */\nexport const IMAGE_EDITOR_STATE_VERSION = 3;\n\n/**\n * Migration steps for the document-level schema (versions 1 and 2), keyed by the version they\n * start from. Empty: version 2 only adds `adjustments.layers`, which version 1 readers keep.\n */\nexport const imageEditorStateMigrations: Record<number, MigrationStep> = {};\n\nexport interface ImageEditorDocumentLoad {\n /** State to pass to `loadJSON` / `initialState`. */\n state: SerializedImageEditorState;\n report: LoadReport;\n /** Top-level keys this build does not know; spread them into the saved JSON to keep them. */\n unknown: JsonObject;\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value);\n\nfunction expectObject(value: unknown, what: string): Record<string, unknown> {\n if (!isObject(value)) throw new Error(`${what} must be an object`);\n return value;\n}\n\nfunction checkFields(\n value: Record<string, unknown>,\n types: Record<string, (v: unknown) => boolean>,\n what: string,\n) {\n for (const [field, ok] of Object.entries(types)) {\n if (field in value && value[field] !== undefined && !ok(value[field])) {\n throw new Error(`${what}.${field} has an invalid value`);\n }\n }\n}\n\nconst isBoolean = (v: unknown) => typeof v === 'boolean';\n\nfunction parseCrop(value: unknown): CropBox | null {\n if (value === null) return null;\n const crop = expectObject(value, 'crop');\n for (const field of ['x', 'y', 'width', 'height'] as const) {\n if (!isNumber(crop[field])) throw new Error(`crop.${field} must be a number`);\n }\n return { ...(crop as unknown as CropBox) };\n}\n\nfunction parseTransform(value: unknown): ImageTransform {\n const transform = expectObject(value, 'transform');\n checkFields(transform, { rotate: isNumber, flipH: isBoolean, flipV: isBoolean }, 'transform');\n return { ...DEFAULT_TRANSFORM, ...(transform as Partial<ImageTransform>) };\n}\n\nconst ADJUSTMENT_NUMBERS = [\n 'brightness',\n 'contrast',\n 'saturation',\n 'exposure',\n 'temperature',\n 'tint',\n 'blur',\n 'opacity',\n] as const;\n\nfunction parseAdjustments(value: unknown): ImageAdjustments {\n const adjustments = expectObject(value, 'adjustments');\n checkFields(\n adjustments,\n {\n ...Object.fromEntries(ADJUSTMENT_NUMBERS.map((k) => [k, isNumber])),\n process: (v) => v === 1 || v === 2,\n },\n 'adjustments',\n );\n // Kept as stored: `loadJSON` fills defaults and keeps a missing `process` legacy.\n return { ...(adjustments as unknown as ImageAdjustments) };\n}\n\nfunction parseDepthMask(value: unknown): DepthMaskOptions {\n const mask = expectObject(value, 'depthMask');\n checkFields(\n mask,\n {\n enabled: isBoolean,\n maskSource: (v) => v === null || typeof v === 'string',\n mode: (v) => v === 'threshold' || v === 'brightness',\n depthRange: (v) => Array.isArray(v) && v.length === 2 && v.every(isNumber),\n softness: isNumber,\n invert: isBoolean,\n },\n 'depthMask',\n );\n const merged = { ...DEFAULT_DEPTH_MASK, ...(mask as Partial<DepthMaskOptions>) };\n return { ...merged, depthRange: [...merged.depthRange] as [number, number] };\n}\n\nconst ANNOTATION_TYPES = new Set(['pen', 'rect', 'circle', 'arrow', 'line', 'text', 'image']);\n\nconst isAnnotation = (value: unknown): value is Annotation =>\n isObject(value) &&\n typeof value.id === 'string' &&\n typeof value.type === 'string' &&\n ANNOTATION_TYPES.has(value.type) &&\n isNumber(value.x) &&\n isNumber(value.y);\n\n/**\n * Loads stored image editor state (a JSON string or object) with a `LoadReport`:\n *\n * - Runs `imageEditorStateMigrations` up to `IMAGE_EDITOR_STATE_VERSION`; a missing\n * `version` is taken as current.\n * - A newer `version` is still loaded but reported `readOnly`: do not save over it.\n * - Each section (`crop`, `transform`, `adjustments`, `depthMask`, `annotations`, and the\n * optional run-length encoded `selection`)\n * falls back to its default on its own; invalid annotations are dropped one by one.\n * - Unknown top-level keys are returned in `unknown`.\n *\n * Valid state from `toJSON()` comes back unchanged, so `loadJSON(state)` behaves\n * exactly as with the raw object.\n */\nexport function loadImageEditorDocument(input: unknown): ImageEditorDocumentLoad {\n let parsed: unknown = input;\n if (typeof input === 'string') {\n try {\n parsed = JSON.parse(input);\n } catch {\n parsed = input;\n }\n }\n if (isObject(parsed) && parsed.version === 3) return loadLayeredDocument(parsed);\n // Version 2 only adds `adjustments.layers`, which the version 1 sections keep as stored.\n const isV2 = isObject(parsed) && parsed.version === 2;\n if (isV2) parsed = { ...(parsed as Record<string, unknown>), version: 1 };\n const newer = isObject(parsed) && typeof parsed.version === 'number' && parsed.version > IMAGE_EDITOR_STATE_VERSION;\n let dropped = 0;\n const result = loadDocument<Omit<LegacySerializedImageEditorState, 'version'>>(\n parsed,\n {\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n adjustments: {\n parse: parseAdjustments,\n // Same as what `loadJSON` derives from missing adjustments.\n defaults: () => normalizeStoredAdjustments(undefined),\n },\n depthMask: {\n parse: parseDepthMask,\n defaults: () => ({ ...DEFAULT_DEPTH_MASK, depthRange: [...DEFAULT_DEPTH_MASK.depthRange] as [number, number] }),\n },\n annotations: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('annotations must be an array');\n const kept = value.filter(isAnnotation);\n dropped = value.length - kept.length;\n return JSON.parse(JSON.stringify(kept)) as Annotation[];\n },\n defaults: () => [],\n },\n // Optional: only present when something was selected when saving.\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n },\n {\n // Versions 1 and 2 load as one schema; anything newer than 3 is reported read-only.\n currentVersion: newer ? IMAGE_EDITOR_STATE_VERSION : 1,\n steps: imageEditorStateMigrations,\n },\n );\n if (dropped > 0) {\n result.report.problems.push({\n section: 'annotations',\n message: `${dropped} invalid annotation${dropped === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 1, ...result.value } as LegacySerializedImageEditorState;\n if (isV2) {\n if (state.adjustments.layers?.length) state.version = 2;\n result.report.version = 2;\n }\n if (state.selection === undefined) delete state.selection;\n return { state, report: result.report, unknown: result.unknown };\n}\n\nconst LAYER_TYPES = new Set(['pixel', 'adjustment', 'group', 'annotation', 'smart']);\n\n/** Keeps layers that have an id-like shape and a known type (children checked too); counts the rest. */\nfunction cleanLayers(value: unknown[], dropped: { count: number }): SerializedLayer[] {\n const out: SerializedLayer[] = [];\n for (const raw of value) {\n if (!isObject(raw) || typeof raw.type !== 'string' || !LAYER_TYPES.has(raw.type)) {\n dropped.count++;\n continue;\n }\n if (raw.type === 'group') {\n const children = Array.isArray(raw.children) ? cleanLayers(raw.children, dropped) : [];\n out.push({ ...(raw as unknown as SerializedLayer), children } as SerializedLayer);\n } else out.push(JSON.parse(JSON.stringify(raw)) as SerializedLayer);\n }\n return out;\n}\n\n/** Version 3 (layers): each section falls back on its own; unreadable layers are dropped one by one. */\nfunction loadLayeredDocument(doc: Record<string, unknown>): ImageEditorDocumentLoad {\n const dropped = { count: 0 };\n const result = loadDocument<Omit<SerializedLayeredImageEditorState, 'version'>>(\n doc,\n {\n width: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('width must be a positive number'); })()), defaults: 0 },\n height: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('height must be a positive number'); })()), defaults: 0 },\n layers: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('layers must be an array');\n return cleanLayers(value, dropped);\n },\n defaults: () => [],\n },\n assets: {\n parse: (value) => {\n const assets = expectObject(value, 'assets');\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(assets)) if (typeof v === 'string') out[k] = v;\n return out;\n },\n defaults: () => ({}),\n },\n activeLayerId: { parse: (v) => (v === null || typeof v === 'string' ? v : null), defaults: null },\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n opacity: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('opacity must be a number');\n return Math.min(1, Math.max(0, v));\n },\n defaults: 1,\n },\n legacyOutputBlur: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('legacyOutputBlur must be a number');\n return Math.max(0, v);\n },\n defaults: undefined,\n },\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n smartContents: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('smartContents must be an array');\n // Entries are checked one by one when the layers load; keep the ones with an id.\n return value.filter((c) => isObject(c) && typeof c.id === 'string').map((c) => JSON.parse(JSON.stringify(c)));\n },\n defaults: undefined,\n },\n },\n { currentVersion: 3, steps: {} },\n );\n if (dropped.count > 0) {\n result.report.problems.push({\n section: 'layers',\n message: `${dropped.count} invalid layer${dropped.count === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 3, ...result.value } as SerializedLayeredImageEditorState;\n if (state.selection === undefined) delete state.selection;\n if (state.legacyOutputBlur === undefined) delete state.legacyOutputBlur;\n if (state.smartContents === undefined) delete state.smartContents;\n return { state, report: result.report, unknown: result.unknown };\n}\n","// Portions ported from lightcraft-main/crates/develop/src/controls.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentKind, ImageAdjustments } from './types';\n\n/**\n * Control specs: one table describes every numeric adjustment slider (id, label, section, range,\n * default, step, display precision and track style). The React and Svelte panels render from it,\n * the agent tools expose it as a schema, and values are addressed by id.\n */\n\nexport type ControlSection = 'light' | 'color' | 'effects' | 'layer';\n\n/** How a slider track is drawn. */\nexport type ControlTrack =\n /** Plain track, fill from the minimum. */\n | { kind: 'plain' }\n /** Zero in the middle. */\n | { kind: 'centered' }\n /** Blue → yellow (white-balance temperature). */\n | { kind: 'temp' }\n /** Green → magenta (tint). */\n | { kind: 'tint' }\n /** A two-colour gradient. */\n | { kind: 'gradient'; from: string; to: string }\n /** Full hue rainbow. */\n | { kind: 'rainbow' };\n\nexport interface ControlSpec {\n /** Dotted path of the value (`exposure`; inside a layer's params: `midtones.0`, `ranges.4.hue`). */\n id: string;\n label: string;\n section: ControlSection;\n min: number;\n max: number;\n step: number;\n default: number;\n /** Decimal places shown (after `displayScale`). */\n decimals: number;\n track: ControlTrack;\n /** Multiplier for display (opacity 0..1 shows as 0..100 %). */\n displayScale?: number;\n /** Unit suffix for display (`px`, `%`, `°`). */\n unit?: string;\n /** The rendering process the control needs (tint: process 2). */\n requiresProcess?: 1 | 2;\n}\n\nconst plain: ControlTrack = { kind: 'plain' };\nconst centered: ControlTrack = { kind: 'centered' };\n\n/** Every numeric base adjustment, in panel order. */\nexport const IMAGE_ADJUSTMENT_CONTROLS: readonly ControlSpec[] = [\n { id: 'brightness', label: 'Brightness', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'contrast', label: 'Contrast', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n {\n id: 'saturation', label: 'Saturation', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' },\n },\n { id: 'exposure', label: 'Exposure', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'temperature', label: 'Warmth / Temperature', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'temp' } },\n { id: 'tint', label: 'Tint', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'tint' }, requiresProcess: 2 },\n { id: 'blur', label: 'Blur', section: 'effects', min: 0, max: 30, step: 1, default: 0, decimals: 0, track: plain, unit: 'px' },\n { id: 'opacity', label: 'Opacity', section: 'effects', min: 0, max: 1, step: 0.05, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\nconst tone = (prefix: string, label: string): ControlSpec[] =>\n [\n ['0', 'Cyan – Red', '#00b7c7', '#e0343a'],\n ['1', 'Magenta – Green', '#d23ad2', '#2fb34a'],\n ['2', 'Yellow – Blue', '#e0c63a', '#3a5be0'],\n ].map(([i, l, from, to]) => ({\n id: `${prefix}.${i}`, label: `${label} ${l}`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from, to },\n }));\n\nconst levelsRecord = (ch: string, label: string): ControlSpec[] => [\n { id: `${ch}.inBlack`, label: `${label} input black`, section: 'layer', min: 0, max: 253, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.gamma`, label: `${label} gamma`, section: 'layer', min: 0.1, max: 9.99, step: 0.01, default: 1, decimals: 2, track: plain },\n { id: `${ch}.inWhite`, label: `${label} input white`, section: 'layer', min: 2, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n { id: `${ch}.outBlack`, label: `${label} output black`, section: 'layer', min: 0, max: 255, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.outWhite`, label: `${label} output white`, section: 'layer', min: 0, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n];\n\nconst RANGE_NAMES = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'];\n\n/**\n * Numeric parameters of each adjustment-layer kind, addressed inside `layer.params`. Curves points\n * and booleans / colours (colorize, preserve luminosity, tint, filter colour) are edited by the\n * dedicated controls, not sliders.\n */\nexport const ADJUSTMENT_LAYER_CONTROLS: Readonly<Record<AdjustmentKind, readonly ControlSpec[]>> = {\n curves: [],\n levels: [\n ...levelsRecord('master', 'RGB'),\n ...levelsRecord('red', 'Red'),\n ...levelsRecord('green', 'Green'),\n ...levelsRecord('blue', 'Blue'),\n ],\n hueSaturation: [\n { id: 'hue', label: 'Hue', section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n { id: 'lightness', label: 'Lightness', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } },\n ...RANGE_NAMES.flatMap((name, i): ControlSpec[] => [\n { id: `ranges.${i}.hue`, label: `${name} hue`, section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: `ranges.${i}.saturation`, label: `${name} saturation`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: `ranges.${i}.lightness`, label: `${name} lightness`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n ]),\n ],\n vibrance: [\n { id: 'vibrance', label: 'Vibrance', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#d8406a' } },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n ],\n colorBalance: [...tone('shadows', 'Shadows'), ...tone('midtones', 'Midtones'), ...tone('highlights', 'Highlights')],\n blackWhite: [\n ['reds', 'Reds', 40, '#e0343a'],\n ['yellows', 'Yellows', 60, '#e0c63a'],\n ['greens', 'Greens', 40, '#2fb34a'],\n ['cyans', 'Cyans', 60, '#00b7c7'],\n ['blues', 'Blues', 20, '#3a5be0'],\n ['magentas', 'Magentas', 80, '#d23ad2'],\n ].map(([id, label, def, to]) => ({\n id: id as string, label: label as string, section: 'layer' as const, min: -200, max: 300, step: 1, default: def as number, decimals: 0,\n track: { kind: 'gradient' as const, from: '#000000', to: to as string }, unit: '%',\n })),\n invert: [],\n posterize: [{ id: 'levels', label: 'Levels', section: 'layer', min: 2, max: 255, step: 1, default: 4, decimals: 0, track: plain }],\n threshold: [{ id: 'level', label: 'Threshold level', section: 'layer', min: 1, max: 255, step: 1, default: 128, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } }],\n photoFilter: [{ id: 'density', label: 'Density', section: 'layer', min: 0, max: 100, step: 1, default: 25, decimals: 0, track: plain, unit: '%' }],\n};\n\n/** Opacity of any adjustment layer (0..1, shown as %). */\nexport const ADJUSTMENT_LAYER_OPACITY_CONTROL: ControlSpec = {\n id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%',\n};\n\nconst BASE_BY_ID = new Map(IMAGE_ADJUSTMENT_CONTROLS.map((c) => [c.id, c]));\n\n/** The base-adjustment control with this id. */\nexport function findAdjustmentControl(id: string): ControlSpec | undefined {\n return BASE_BY_ID.get(id);\n}\n\n/** The control of a layer parameter (`opacity` included). */\nexport function findLayerControl(kind: AdjustmentKind, id: string): ControlSpec | undefined {\n if (id === 'opacity') return ADJUSTMENT_LAYER_OPACITY_CONTROL;\n return ADJUSTMENT_LAYER_CONTROLS[kind]?.find((c) => c.id === id);\n}\n\n/** `v` clamped to the control's range and snapped to its step; non-finite values give the default. */\nexport function clampControlValue(spec: ControlSpec, v: unknown): number {\n if (typeof v !== 'number' || !Number.isFinite(v)) return spec.default;\n const clamped = Math.min(spec.max, Math.max(spec.min, v));\n const steps = Math.round((clamped - spec.min) / spec.step);\n const snapped = spec.min + steps * spec.step;\n // Trim float noise from the step arithmetic.\n return Math.min(spec.max, Number(snapped.toFixed(6)));\n}\n\n/** A value as the slider shows it (`+12`, `35%`, `4px`). */\nexport function formatControlValue(spec: ControlSpec, v: number): string {\n const shown = (Number.isFinite(v) ? v : spec.default) * (spec.displayScale ?? 1);\n const s = shown.toFixed(spec.decimals);\n return `${s}${spec.unit ?? ''}`;\n}\n\n/** Where `v` sits on the track, 0 (left) to 1 (right). */\nexport function controlPosition(spec: ControlSpec, v: number): number {\n const p = (clampControlValue(spec, v) - spec.min) / (spec.max - spec.min || 1);\n return Math.min(1, Math.max(0, p));\n}\n\n/** CSS background for a control's track. */\nexport function controlTrackBackground(spec: ControlSpec): string | undefined {\n const t = spec.track;\n switch (t.kind) {\n case 'temp':\n return 'linear-gradient(to right, #3a7bd5, #d8d8d8, #f2c94c)';\n case 'tint':\n return 'linear-gradient(to right, #3fbf4f, #d8d8d8, #d23ad2)';\n case 'gradient':\n return `linear-gradient(to right, ${t.from}, ${t.to})`;\n case 'rainbow':\n return 'linear-gradient(to right, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff)';\n default:\n return undefined;\n }\n}\n\n/** Reads a dotted path (`ranges.2.hue`) from an object. */\nexport function getControlPath(obj: unknown, path: string): unknown {\n let cur: any = obj;\n for (const part of path.split('.')) {\n if (cur === null || cur === undefined) return undefined;\n cur = cur[part];\n }\n return cur;\n}\n\n/** A copy of `obj` with the dotted path set (arrays and objects along the path are cloned). */\nexport function setControlPath<T>(obj: T, path: string, value: unknown): T {\n const parts = path.split('.');\n const root: any = Array.isArray(obj) ? [...(obj as unknown[])] : { ...(obj as object) };\n let cur = root;\n for (let i = 0; i < parts.length - 1; i++) {\n const next = cur[parts[i]];\n cur[parts[i]] = Array.isArray(next) ? [...next] : next && typeof next === 'object' ? { ...next } : {};\n cur = cur[parts[i]];\n }\n cur[parts[parts.length - 1]] = value;\n return root as T;\n}\n\n/** The base adjustment values the control table describes. */\nexport function readAdjustmentControls(adjustments: ImageAdjustments): Record<string, number> {\n const out: Record<string, number> = {};\n for (const c of IMAGE_ADJUSTMENT_CONTROLS) {\n const v = (adjustments as unknown as Record<string, unknown>)[c.id];\n out[c.id] = typeof v === 'number' && Number.isFinite(v) ? v : c.default;\n }\n return out;\n}\n","import type { SelectionMask, SelectionMode } from './mask';\nimport type { ColorDistance } from './wand';\nimport type { RefineEdgeParams } from './refine';\n\n/** Selection tools of the image editor UI. */\nexport type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'lasso';\n\nexport const SELECTION_TOOLS: readonly { id: SelectionToolKind; label: string }[] = [\n { id: 'wand', label: 'Magic Wand' },\n { id: 'rect', label: 'Rectangle' },\n { id: 'ellipse', label: 'Ellipse' },\n { id: 'lasso', label: 'Lasso' },\n];\n\nexport const SELECTION_MODE_LABELS: Record<SelectionMode, string> = {\n replace: 'New',\n add: 'Add',\n subtract: 'Subtract',\n intersect: 'Intersect',\n};\n\n/** The options bar of the selection tools (shared by the React and Svelte UIs). */\nexport interface SelectionToolOptions {\n tool: SelectionToolKind;\n mode: SelectionMode;\n /** Magic wand tolerance, 0..255. */\n tolerance: number;\n contiguous: boolean;\n antiAlias: boolean;\n /** Sample the adjusted image instead of the source. */\n sampleMerged: boolean;\n /** Sample the whole image (`composite`, default) or only the active pixel layer. */\n sample?: 'composite' | 'active-layer';\n distance: ColorDistance;\n}\n\nexport const DEFAULT_SELECTION_TOOL_OPTIONS: SelectionToolOptions = {\n tool: 'wand',\n mode: 'replace',\n tolerance: 32,\n contiguous: true,\n antiAlias: true,\n sampleMerged: false,\n distance: 'rgb',\n};\n\n/**\n * Photoshop's modifier keys: Shift adds, Alt/Option subtracts, both intersect; otherwise the\n * options bar mode.\n */\nexport function selectionModeFromModifiers(\n modifiers: { shiftKey?: boolean; altKey?: boolean },\n fallback: SelectionMode\n): SelectionMode {\n if (modifiers.shiftKey && modifiers.altKey) return 'intersect';\n if (modifiers.shiftKey) return 'add';\n if (modifiers.altKey) return 'subtract';\n return fallback;\n}\n\nexport interface RefineEdgeControl {\n id: keyof Omit<RefineEdgeParams, 'smartRadius'>;\n label: string;\n min: number;\n max: number;\n step: number;\n unit: string;\n}\n\n/** Sliders of the Refine Edge panel. */\nexport const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[] = [\n { id: 'radius', label: 'Radius', min: 0, max: 64, step: 1, unit: 'px' },\n { id: 'smooth', label: 'Smooth', min: 0, max: 100, step: 1, unit: '' },\n { id: 'feather', label: 'Feather', min: 0, max: 50, step: 0.5, unit: 'px' },\n { id: 'contrast', label: 'Contrast', min: 0, max: 100, step: 1, unit: '%' },\n { id: 'shiftEdge', label: 'Shift Edge', min: -100, max: 100, step: 1, unit: '%' },\n];\n\n/** A one-line status: \"No selection\" or \"W × H px selected (n % of the image)\". */\nexport function describeSelection(mask: SelectionMask | null | undefined): string {\n if (!mask || !mask.bounds) return 'No selection';\n let sum = 0;\n for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];\n const pct = (sum / 255 / mask.data.length) * 100;\n const shown = pct >= 10 ? Math.round(pct) : Math.round(pct * 10) / 10;\n return `${mask.bounds.width} × ${mask.bounds.height} px selected (${shown}% of the image)`;\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { JsonSchema } from '../tools/schema.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind } from './types';\nimport { ADJUSTMENT_KINDS, isAdjustmentKind } from './adjustment-layers';\nimport { describeSelection } from './selection/tool-options';\nimport { SELECTION_MODES, parseSelectionMode, selectionArea } from './selection/mask';\nimport { SELECTION_MODIFY_OPS, type SelectionModifyOp } from './selection/modify';\nimport { layerMaskFromSelection } from './selection/layer-mask';\n\nconst modeSchema: JsonSchema = {\n type: 'string',\n enum: [...SELECTION_MODES],\n description: 'How the result combines with the current selection (default replace).',\n};\n\nconst num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\nfunction summary(controller: ImageEditorController) {\n const sel = controller.getSelection();\n const { width, height } = controller.getState().imageDimensions;\n const area = selectionArea(sel);\n return {\n empty: !sel,\n bounds: sel?.bounds ?? null,\n /** Σ coverage in pixels. */\n area: Math.round(area),\n percent: Math.round((area / Math.max(1, width * height)) * 10000) / 100,\n description: describeSelection(sel),\n };\n}\n\nconst writeAnnotations = (title: string, idempotent = false) => ({\n title,\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: idempotent,\n openWorldHint: false,\n});\n\n/**\n * Agent tools for selections and masks (part of `createImageEditorToolProvider`):\n * `get_selection`, `select_region`, `select_by_color`, `modify_selection`, `refine_selection`\n * and `apply_selection_as_mask`. Coordinates are natural pixels of the source image.\n */\nexport function createImageSelectionTools(controller: ImageEditorController): AgentTool[] {\n const getSelection: AgentTool = {\n name: 'get_selection',\n title: 'Read the selection',\n description:\n 'Returns the active selection: whether it is empty, its bounds (natural image pixels), its area in pixels and percent of the image.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read the selection', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n handler: () => summary(controller),\n };\n\n const selectRegion: AgentTool = {\n name: 'select_region',\n title: 'Select a region',\n description:\n 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), everything (\"all\"), nothing (\"none\") or the inverse of the current selection (\"invert\"). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n shape: { type: 'string', enum: ['rect', 'ellipse', 'all', 'none', 'invert'], description: 'What to select.' },\n x: { type: 'number', description: 'rect / ellipse: left edge in natural image pixels.' },\n y: { type: 'number', description: 'rect / ellipse: top edge in natural image pixels.' },\n width: { type: 'number', description: 'rect / ellipse: width in natural image pixels.' },\n height: { type: 'number', description: 'rect / ellipse: height in natural image pixels.' },\n mode: modeSchema,\n feather: { type: 'number', minimum: 0, description: 'Feather radius in pixels (default 0).' },\n },\n required: ['shape'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select a region', true),\n handler: ({ shape, x, y, width, height, mode, feather }) => {\n const opts = { mode: parseSelectionMode(mode), feather: num(feather, 0) };\n switch (shape) {\n case 'all':\n controller.selectAll();\n break;\n case 'none':\n controller.deselect();\n break;\n case 'invert':\n controller.invertSelection();\n break;\n case 'rect':\n case 'ellipse': {\n const rect = { x: num(x, NaN), y: num(y, NaN), width: num(width, NaN), height: num(height, NaN) };\n if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) {\n throw new Error('rect and ellipse need numeric x, y, width and height.');\n }\n if (shape === 'rect') controller.selectRect(rect, opts);\n else controller.selectEllipse(rect, opts);\n break;\n }\n default:\n throw new Error(`Unknown shape \"${String(shape)}\". Use rect, ellipse, all, none or invert.`);\n }\n return summary(controller);\n },\n };\n\n const selectByColor: AgentTool = {\n name: 'select_by_color',\n title: 'Select by colour',\n description:\n 'Magic wand at (x, y) with tolerance (0-255) and contiguous, or with \"color\" (\"#rrggbb\") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Seed point x in natural image pixels.' },\n y: { type: 'number', description: 'Seed point y in natural image pixels.' },\n color: { type: 'string', description: '#rrggbb: select this colour everywhere instead of a seed point.' },\n tolerance: { type: 'number', minimum: 0, maximum: 255, description: 'Magic wand tolerance (default 32).' },\n contiguous: { type: 'boolean', description: 'Only the connected region around (x, y) (default true).' },\n fuzziness: { type: 'number', minimum: 0, maximum: 200, description: 'Color Range fuzziness (default 40).' },\n sampleMerged: { type: 'boolean', description: 'Sample the adjusted image rather than the source.' },\n sample: {\n type: 'string',\n enum: ['composite', 'active-layer'],\n description: 'composite (default): the whole image; active-layer: only the active pixel layer (see set_active_layer).',\n },\n mode: modeSchema,\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select by colour', true),\n handler: async ({ x, y, color, tolerance, contiguous, fuzziness, sampleMerged, sample, mode }) => {\n const common = {\n mode: parseSelectionMode(mode),\n sampleMerged: sampleMerged === true,\n sample: (sample as 'composite' | 'active-layer' | undefined) ?? 'composite',\n };\n if (typeof color === 'string') {\n await controller.selectColorRange([{ color }], { ...common, fuzziness: num(fuzziness, 40) });\n } else if (typeof x === 'number' && typeof y === 'number') {\n await controller.magicWand({ x, y }, { ...common, tolerance: num(tolerance, 32), contiguous: contiguous !== false });\n } else {\n throw new Error('Pass x and y (magic wand) or color (#rrggbb).');\n }\n return summary(controller);\n },\n };\n\n const modify: AgentTool = {\n name: 'modify_selection',\n title: 'Modify the selection',\n description: 'Select › Modify: grow, shrink, feather, border or smooth the selection by radius pixels. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n op: { type: 'string', enum: [...SELECTION_MODIFY_OPS], description: 'How to modify the selection.' },\n radius: { type: 'number', minimum: 0, maximum: 500, description: 'Radius in pixels.' },\n },\n required: ['op', 'radius'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Modify the selection'),\n handler: ({ op, radius }) => {\n if (!(SELECTION_MODIFY_OPS as readonly string[]).includes(op as string)) throw new Error(`Unknown op \"${String(op)}\".`);\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n controller.modifySelection(op as SelectionModifyOp, Math.min(500, Math.max(0, num(radius, 0))));\n return summary(controller);\n },\n };\n\n const refine: AgentTool = {\n name: 'refine_selection',\n title: 'Refine the selection edge',\n description:\n 'Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %). Use it after loading a coarse AI mask. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n radius: { type: 'number', minimum: 0, maximum: 64, description: 'Edge detection radius in pixels.' },\n smartRadius: { type: 'boolean', description: 'Adapt the radius to hard and soft edges.' },\n smooth: { type: 'number', minimum: 0, maximum: 100, description: 'Smooth the edge outline (0-100).' },\n feather: { type: 'number', minimum: 0, maximum: 250, description: 'Feather the edge (pixels).' },\n contrast: { type: 'number', minimum: 0, maximum: 100, description: 'Sharpen the edge transition (0-100).' },\n shiftEdge: { type: 'number', minimum: -100, maximum: 100, description: 'Move the edge out (+) or in (-), in percent.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Refine the selection edge'),\n handler: async (params) => {\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n await controller.refineSelection({\n radius: num(params.radius, 8),\n smartRadius: params.smartRadius === true,\n smooth: num(params.smooth, 0),\n feather: num(params.feather, 0),\n contrast: num(params.contrast, 0),\n shiftEdge: num(params.shiftEdge, 0),\n });\n return summary(controller);\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_selection_as_mask',\n title: 'Mask a layer with the selection',\n description:\n 'Uses the selection as an adjustment layer mask: on layerId, or on a new layer of kind (with optional params), so the adjustment only affects the selected area. density (0-1) and feather (px) soften the mask non-destructively. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Existing adjustment layer to mask.' },\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Kind of a new layer to add and mask (instead of layerId).' },\n params: { type: 'object', description: 'Parameters for the new layer (see list_controls).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0-1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Mask feather in pixels.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Mask a layer with the selection'),\n handler: ({ layerId, kind, params, density, feather }) => {\n const sel = controller.getSelection();\n if (!sel) throw new Error('Nothing is selected.');\n const props = {\n ...(typeof density === 'number' ? { density } : {}),\n ...(typeof feather === 'number' ? { feather } : {}),\n };\n let id: string;\n if (typeof layerId === 'string') {\n if (!controller.getAdjustmentLayer(layerId)) throw new Error(`No adjustment layer \"${layerId}\".`);\n controller.applySelectionAsLayerMask(layerId, props);\n id = layerId;\n } else if (isAdjustmentKind(kind)) {\n id = controller.addAdjustmentLayer({\n kind: kind as AdjustmentKind,\n params: params as never,\n mask: layerMaskFromSelection(sel, props),\n });\n } else {\n throw new Error('Pass layerId (an existing layer) or kind (a new adjustment layer).');\n }\n const layer = controller.getAdjustmentLayer(id)!;\n const { rle: _rle, ...mask } = layer.mask!;\n return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };\n },\n };\n\n return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];\n}\n","import type { CaptureRenderRequest, CaptureSurface } from '../tools/capture.js';\nimport type { ImageEditorController } from './controller';\nimport type { SelectionMask } from './selection/mask';\n\nconst NO_CANVAS = 'Rendering is not available in this environment.';\n\n/** Darkens what is outside the selection (natural pixels), so its coverage reads at a glance. */\nfunction selectionShade(selection: SelectionMask, doc: Document): HTMLCanvasElement | null {\n const canvas = doc.createElement('canvas');\n canvas.width = selection.width;\n canvas.height = selection.height;\n const context = canvas.getContext('2d');\n if (!context) return null;\n const pixels = context.createImageData(selection.width, selection.height);\n for (let i = 0; i < selection.data.length; i++) {\n pixels.data[i * 4 + 3] = Math.round(150 * (1 - selection.data[i] / 255));\n }\n context.putImageData(pixels, 0, 0);\n return canvas;\n}\n\n/** Draws a region of the image editor's source view for `capture_view`. */\nasync function renderImageRegion(controller: ImageEditorController, request: CaptureRenderRequest): Promise<string> {\n const state = controller.getState();\n // Never render above natural size; small regions are enlarged when drawn.\n const k = Math.min(1, request.scale);\n const view = await controller.renderSourceView({ scale: k });\n if (!view) throw new Error('No image is loaded, so there is nothing to capture.');\n if (typeof document === 'undefined') throw new Error(NO_CANVAS);\n const out = document.createElement('canvas');\n out.width = request.width;\n out.height = request.height;\n const context = out.getContext('2d');\n if (!context) throw new Error(NO_CANVAS);\n const { rect } = request;\n // Upscaled regions keep hard pixel edges, so the agent sees real pixels.\n context.imageSmoothingEnabled = request.scale < 1;\n if (request.format === 'jpeg') {\n context.fillStyle = '#ffffff';\n context.fillRect(0, 0, out.width, out.height);\n }\n const kx = view.width / state.imageDimensions.width;\n const ky = view.height / state.imageDimensions.height;\n context.drawImage(view, rect.x * kx, rect.y * ky, rect.width * kx, rect.height * ky, 0, 0, out.width, out.height);\n if (request.includeOverlays) {\n const s = request.scale;\n const selection = state.selection;\n if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {\n const shade = selectionShade(selection, document);\n if (shade) context.drawImage(shade, rect.x, rect.y, rect.width, rect.height, 0, 0, out.width, out.height);\n }\n if (state.crop) {\n context.save();\n context.strokeStyle = '#ffd400';\n context.lineWidth = 2;\n context.setLineDash([6, 4]);\n context.strokeRect((state.crop.x - rect.x) * s, (state.crop.y - rect.y) * s, state.crop.width * s, state.crop.height * s);\n context.restore();\n }\n }\n const url = out.toDataURL(request.format === 'jpeg' ? 'image/jpeg' : 'image/png', request.quality);\n if (!url.startsWith('data:image/') || url.indexOf(',') === url.length - 1) throw new Error(NO_CANVAS);\n return url;\n}\n\n/**\n * The image editor as a capture surface. Units are natural pixels of the source image,\n * the same space as `get_selection` and `select_region`: crop, rotation and flips are not\n * applied (use `render_preview` for the final framing), adjustments, layers and annotations\n * are. Regions: the whole image, the selection's bounds, or a rect. Small regions are\n * enlarged up to 8× with hard pixel edges for close inspection.\n */\nexport function createImageCaptureSurface(controller: ImageEditorController): CaptureSurface {\n const whole = () => {\n const { width, height } = controller.getState().imageDimensions;\n return width > 0 && height > 0 ? { x: 0, y: 0, width, height } : null;\n };\n return {\n units: 'image pixels',\n content: whole,\n selection: () => ({ rect: controller.getState().selection?.bounds ?? null }),\n overlays: 'a shade over unselected pixels and the crop box outline',\n maxScale: 8,\n render: (request) => renderImageRegion(controller, request),\n };\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { BlendMode } from './blend';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport { getRotatedSize, normalizeAngle } from './geometry';\nimport type { ImageHistogram } from './histogram';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Adjustment layer id from get_adjustments (e.g. \"adj_1\").' } as const;\n\nconst round = (n: number, digits = 2) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** Mean and the 1st / 99th percentiles of a 256-bin histogram. */\nfunction binStats(bins: ArrayLike<number>, samples: number) {\n if (!samples) return { mean: null, p1: null, p99: null };\n let sum = 0;\n let seen = 0;\n let p1: number | null = null;\n let p99: number | null = null;\n for (let i = 0; i < bins.length; i++) {\n sum += i * bins[i];\n seen += bins[i];\n if (p1 === null && seen >= samples * 0.01) p1 = i;\n if (p99 === null && seen >= samples * 0.99) p99 = i;\n }\n return { mean: round(sum / samples, 1), p1, p99 };\n}\n\n/** Sums 256 bins into `count` equal buckets. */\nfunction rebin(bins: ArrayLike<number>, count: number): number[] {\n const out = new Array<number>(count).fill(0);\n const width = bins.length / count;\n for (let i = 0; i < bins.length; i++) out[Math.floor(i / width)] += bins[i];\n return out;\n}\n\nfunction histogramSummary(h: ImageHistogram, bins: number | undefined) {\n const pct = (n: number) => (h.samples ? round((n / h.samples) * 100) : 0);\n const channels = { red: h.red, green: h.green, blue: h.blue, luminance: h.luminance };\n return {\n samples: h.samples,\n transparent: h.transparent,\n clipping: {\n shadowsPercent: { red: pct(h.shadows[0]), green: pct(h.shadows[1]), blue: pct(h.shadows[2]) },\n highlightsPercent: { red: pct(h.highlights[0]), green: pct(h.highlights[1]), blue: pct(h.highlights[2]) }\n },\n stats: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, binStats(v, h.samples)])),\n ...(bins ? { bins: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, rebin(v, bins)])) } : {})\n };\n}\n\n/**\n * Agent tools for the image editor's layers, geometry and history: `get_image_info`,\n * `get_histogram`, `update_adjustment_layer`, `remove_adjustment_layer`,\n * `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`, `flip_image`,\n * `undo_image` and `redo_image`. Each write is one undo step and returns what changed.\n */\nexport function createImageEditTools(controller: ImageEditorController): AgentTool[] {\n const layerOrThrow = (id: string) => {\n const layers = controller.getAdjustmentLayers();\n const index = layers.findIndex((l) => l.id === id);\n if (index === -1) {\n const ids = layers.map((l) => l.id);\n throw new Error(`Unknown adjustment layer \"${id}\". ${ids.length ? `Layers: ${ids.join(', ')}.` : 'There are no adjustment layers.'}`);\n }\n return { layer: layers[index], index };\n };\n const layerJson = (id: string) => JSON.parse(JSON.stringify(controller.getAdjustmentLayer(id)));\n const geometry = () => {\n const s = controller.getState();\n const region = s.crop ?? { x: 0, y: 0, ...s.imageDimensions };\n const out = getRotatedSize(region.width, region.height, s.transform);\n return {\n image: { ...s.imageDimensions },\n crop: s.crop ? { ...s.crop } : null,\n rotation: normalizeAngle(s.transform.rotate),\n flipH: s.transform.flipH,\n flipV: s.transform.flipV,\n output: { width: Math.round(out.width), height: Math.round(out.height) }\n };\n };\n const history = () => ({ canUndo: controller.canUndo, canRedo: controller.canRedo });\n const requireImage = () => {\n const { width, height } = controller.getState().imageDimensions;\n if (!width || !height) throw new Error('No image is loaded. Ask the user to open one first.');\n return { width, height };\n };\n\n const getImageInfo: AgentTool = {\n name: 'get_image_info',\n title: 'Read image geometry',\n description:\n 'Returns the source image size in pixels, the crop box (natural image pixels, or null for the whole image), rotation in degrees, flips, the output size after crop and rotation, the number of adjustment layers and whether undo / redo are available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read image geometry', ...READ },\n handler: () => ({ ...geometry(), layers: controller.getAdjustmentLayers().length, ...history() })\n };\n\n const getHistogram: AgentTool = {\n name: 'get_histogram',\n title: 'Read the histogram',\n description:\n 'Returns the histogram of the last rendered preview (after adjustments and layers): per channel and luminance mean and 1st / 99th percentiles (0-255), and the percentage of pixels clipped to pure black or white per channel. ' +\n 'Use it to judge exposure and clipping numerically. Pass bins for the counts themselves. Fails before the first render: call render_preview first.',\n inputSchema: {\n type: 'object',\n properties: {\n bins: { type: 'integer', enum: [16, 32, 64, 256], description: 'Also return the counts in this many buckets per channel (default: no counts).' }\n },\n additionalProperties: false\n },\n annotations: { title: 'Read the histogram', ...READ },\n handler: ({ bins }) => {\n const h = controller.getHistogram();\n if (!h) throw new Error('No histogram yet: it comes from the rendered preview. Call render_preview, then try again.');\n return histogramSummary(h, bins);\n }\n };\n\n const updateLayer: AgentTool = {\n name: 'update_adjustment_layer',\n title: 'Change an adjustment layer',\n description:\n 'Shows or hides a layer, renames it, or changes its blend mode or opacity, as one undo step. Change its parameters with set_adjustments (\"layers.<id>.<control>\"). Returns the updated layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n enabled: { type: 'boolean', description: 'false hides the layer without deleting it.' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (see list_controls).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1.' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' }\n },\n required: ['layer'],\n additionalProperties: false\n },\n annotations: { title: 'Change an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, enabled, blend, opacity, name }) => {\n layerOrThrow(layer);\n if (enabled === undefined && blend === undefined && opacity === undefined && name === undefined) {\n throw new Error('Nothing to change. Pass enabled, blend, opacity or name.');\n }\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n controller.updateAdjustmentLayer(layer, { enabled, blend: blend as BlendMode | undefined, opacity, name });\n return { layer: layerJson(layer) };\n }\n };\n\n const removeLayer: AgentTool = {\n name: 'remove_adjustment_layer',\n title: 'Delete an adjustment layer',\n description: 'Deletes an adjustment layer and its mask as one undo step. To hide it instead, use update_adjustment_layer with enabled false. Returns the remaining layer ids, bottom to top.',\n inputSchema: { type: 'object', properties: { layer: LAYER_ID }, required: ['layer'], additionalProperties: false },\n annotations: { title: 'Delete an adjustment layer', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n handler: ({ layer }) => {\n layerOrThrow(layer);\n controller.removeAdjustmentLayer(layer);\n return { removed: layer, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const moveLayer: AgentTool = {\n name: 'move_adjustment_layer',\n title: 'Reorder an adjustment layer',\n description:\n 'Moves an adjustment layer to a position in the stack (0 = bottom, applied first) as one undo step. Layers apply bottom to top, so order changes the result. Returns the layer ids, bottom to top.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n to: { type: 'integer', minimum: 0, description: 'New index, 0 = bottom; past the top means the top.' }\n },\n required: ['layer', 'to'],\n additionalProperties: false\n },\n annotations: { title: 'Reorder an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, to }) => {\n const { index } = layerOrThrow(layer);\n const top = controller.getAdjustmentLayers().length - 1;\n const target = Math.min(to, top);\n if (target === index) throw new Error(`Layer \"${layer}\" is already at index ${index} (0 = bottom, ${top} = top).`);\n controller.moveAdjustmentLayer(layer, target);\n return { layer, index: target, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const setCrop: AgentTool = {\n name: 'set_crop',\n title: 'Crop the image',\n description:\n 'Sets the crop box in natural image pixels of the un-rotated source (origin top-left; see get_image_info for the size) as one undo step. Non-destructive: reset_crop restores the whole image. Returns the new geometry and output size.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', minimum: 0, description: 'Left edge in source pixels.' },\n y: { type: 'number', minimum: 0, description: 'Top edge in source pixels.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width in source pixels.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height in source pixels.' }\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false\n },\n annotations: { title: 'Crop the image', ...WRITE_IDEMPOTENT },\n handler: ({ x, y, width, height }) => {\n const image = requireImage();\n if (x + width > image.width + 0.5 || y + height > image.height + 0.5) {\n throw new Error(`The crop box (${x}, ${y}, ${width} × ${height}) goes outside the ${image.width} × ${image.height} image.`);\n }\n controller.setCrop({ x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) });\n return geometry();\n }\n };\n\n const resetCrop: AgentTool = {\n name: 'reset_crop',\n title: 'Remove the crop',\n description: 'Removes the crop so the whole image shows again, as one undo step. Returns the new geometry.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Remove the crop', ...WRITE_IDEMPOTENT },\n handler: () => {\n if (!controller.getState().crop) throw new Error('The image is not cropped.');\n controller.resetCrop();\n return geometry();\n }\n };\n\n const setRotation: AgentTool = {\n name: 'set_rotation',\n title: 'Rotate the image',\n description:\n 'Sets the image rotation in degrees clockwise, as one undo step. Quarter turns (90, 180, 270) keep every pixel; other angles straighten the image inside a larger frame with transparent corners, so crop afterwards. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { degrees: { type: 'number', minimum: -360, maximum: 360, description: 'Absolute rotation in degrees clockwise (0 = upright).' } },\n required: ['degrees'],\n additionalProperties: false\n },\n annotations: { title: 'Rotate the image', ...WRITE_IDEMPOTENT },\n handler: ({ degrees }) => {\n requireImage();\n controller.setRotation(degrees);\n return geometry();\n }\n };\n\n const flipImage: AgentTool = {\n name: 'flip_image',\n title: 'Flip the image',\n description: 'Mirrors the image horizontally (left-right) or vertically (top-bottom), as one undo step. Flipping twice restores it. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left-right; vertical mirrors top-bottom.' } },\n required: ['axis'],\n additionalProperties: false\n },\n annotations: { title: 'Flip the image', ...WRITE },\n handler: ({ axis }) => {\n requireImage();\n if (axis === 'horizontal') controller.flipHorizontal();\n else controller.flipVertical();\n return geometry();\n }\n };\n\n const undo: AgentTool = {\n name: 'undo_image',\n title: 'Undo an image edit',\n description: 'Undoes the last image edit (one tool call is one step). Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Undo an image edit', ...WRITE },\n handler: () => {\n if (!controller.undo()) throw new Error('Nothing to undo.');\n return history();\n }\n };\n\n const redo: AgentTool = {\n name: 'redo_image',\n title: 'Redo an image edit',\n description: 'Redoes the last undone image edit. Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Redo an image edit', ...WRITE },\n handler: () => {\n if (!controller.redo()) throw new Error('Nothing to redo.');\n return history();\n }\n };\n\n return [getImageInfo, getHistogram, updateLayer, removeLayer, moveLayer, setCrop, resetCrop, setRotation, flipImage, undo, redo];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { contentBounds } from '../raster/surface';\nimport { findLayer, type ImageLayer, type LayerFilter, type RasterMask } from './layers/model';\nimport { LayerOperationError, requireLayer, type LayerPlacement } from './layers/ops';\nimport { adjustmentKindLabel } from './adjustment-layers';\nimport { BLEND_MODES } from './blend';\nimport type { BlendMode } from './blend';\nimport type { CropBox } from './types';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** A short description of a mask for agents (no pixels). */\nexport function maskSummary(mask: RasterMask | undefined): Record<string, unknown> | null {\n if (!mask) return null;\n const out: Record<string, unknown> = {\n kind: mask.source ? 'depth' : 'raster',\n enabled: mask.enabled !== false,\n linked: mask.linked !== false,\n density: mask.density ?? 1,\n feather: mask.feather ?? 0,\n };\n if (!mask.source) {\n out.defaultValue = mask.defaultValue;\n out.paintedBounds = contentBounds(mask.surface);\n }\n return out;\n}\n\n/**\n * A plain description of a layer (and its children) for agents: ids, types, names, document\n * bounds, visibility, opacity, fill, blend, clipping, locks, mask and effects summaries and\n * whether it is active. `boundsOf` gives a pixel layer's document bounds.\n */\nexport function describeLayer(\n layer: ImageLayer,\n activeId: string | null,\n boundsOf: (layer: ImageLayer) => CropBox | null\n): Record<string, unknown> {\n const out: Record<string, unknown> = {\n id: layer.id,\n type: layer.type,\n name: layer.name || (layer.type === 'adjustment' ? adjustmentKindLabel(layer.kind) : ''),\n visible: layer.visible,\n opacity: round(layer.opacity),\n blend: layer.blend,\n clipped: Boolean(layer.clipped),\n locks: layer.locks ? { ...layer.locks } : {},\n mask: maskSummary(layer.mask),\n effects: layer.effects\n ? { enabled: layer.effects.enabled, scale: layer.effects.scale ?? 1, items: layer.effects.items.map((e) => ({ id: e.id, kind: e.kind, enabled: e.enabled })) }\n : null,\n active: layer.id === activeId,\n };\n switch (layer.type) {\n case 'pixel':\n out.bounds = boundsOf(layer);\n out.offset = { ...layer.offset };\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.filters = (layer.filters ?? []).map((f) => ({ ...f }));\n if (layer.source === 'document') out.source = 'document';\n break;\n case 'adjustment':\n out.kind = layer.kind;\n out.params = JSON.parse(JSON.stringify(layer.params));\n break;\n case 'annotation':\n out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\n break;\n case 'group':\n out.collapsed = Boolean(layer.collapsed);\n out.children = layer.children.map((c) => describeLayer(c, activeId, boundsOf));\n break;\n case 'smart':\n out.bounds = boundsOf(layer);\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.contents = { id: layer.contents.id, name: layer.contents.name, kind: layer.contents.kind, width: layer.contents.width, height: layer.contents.height };\n out.smartFilters = (layer.smartFilters?.filters ?? []).map((f) => ({ id: f.id, kind: f.kind, enabled: f.enabled }));\n out.filterMask = maskSummary(layer.smartFilters?.mask);\n break;\n }\n return out;\n}\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers.' } as const;\nconst PLACEMENT = {\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (same group).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer (same group).' },\n parentId: { type: 'string', description: 'Put the layer inside this group (at index, default its top).' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom (with parentId, or at the top level).' },\n} as const;\nconst BLEND = { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' } as const;\n\nfunction placementFrom(args: Record<string, unknown>): LayerPlacement {\n const p: LayerPlacement = {};\n if (typeof args.aboveId === 'string') p.aboveId = args.aboveId;\n if (typeof args.belowId === 'string') p.belowId = args.belowId;\n if (typeof args.parentId === 'string') p.parentId = args.parentId;\n if (typeof args.index === 'number') p.index = args.index;\n return p;\n}\n\n/**\n * Agent tools for the image editor's layer stack: `list_layers`, `set_active_layer`,\n * `add_pixel_layer`, `duplicate_layer`, `remove_layer`, `move_layer`, `set_layer_properties`,\n * `group_layers`, `ungroup_layer`, `merge_down`, `merge_visible`, `flatten_image`, `offset_layer`,\n * `add_layer_mask`, `apply_layer_mask`, `remove_layer_mask`, `set_layer_mask_properties`,\n * `layer_via_copy`, `add_layer_filter`, `update_layer_filter` and `remove_layer_filter`. Each write\n * is one undo step, validates before changing anything and returns what it changed.\n */\nexport function createImageLayerTools(controller: ImageEditorController): AgentTool[] {\n const describe = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n if (!loc) return null;\n return {\n ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)),\n parentId: loc.parent?.id ?? null,\n index: loc.index,\n };\n };\n const exists = (id: unknown, what = 'layerId') => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError(`${what} is required (a layer id from list_layers).`);\n requireLayer(controller.getLayers(), id);\n return id;\n };\n\n const listLayers: AgentTool = {\n name: 'list_layers',\n title: 'List image layers',\n description:\n '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.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'List image layers', ...READ },\n handler: () => {\n const s = controller.getState();\n return {\n canvas: { ...s.imageDimensions },\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n };\n },\n };\n\n const setActive: AgentTool = {\n name: 'set_active_layer',\n title: 'Set the active layer',\n description:\n 'Makes a layer active (the base sliders of set_adjustments then edit its develop filter when it is a pixel layer) and chooses whether painting targets its pixels or its mask. View state, not an undo step. Returns the active layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Layer to activate (from list_layers).' },\n target: { type: 'string', enum: ['pixels', 'mask'], description: 'Paint target (default pixels; mask needs a mask).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the active layer', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, target }) => {\n const id = exists(layerId);\n const layer = controller.getLayer(id)!;\n if (target === 'mask' && !layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n controller.setActiveLayer(id, (target as 'pixels' | 'mask' | undefined) ?? 'pixels');\n return { layer: describe(id), activeTarget: controller.getState().activeTarget };\n },\n };\n\n const addPixelLayer: AgentTool = {\n name: 'add_pixel_layer',\n title: 'Add a pixel layer',\n description:\n 'Adds a pixel layer and makes it active: empty (default), filled with color over the canvas, or holding an image (PNG data URL or base64 PNG; other formats and URLs only in a browser) placed at x, y. Goes above the active layer unless aboveId / belowId / parentId / index is given. One undo step. Returns the new layer with its id, bounds and position.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Layer name (default \"Layer N\").' },\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa; omit for an empty layer.' },\n image: { type: 'string', description: 'Image to place: PNG data URL or base64 PNG (or any URL in a browser).' },\n x: { type: 'integer', description: 'Left of the image in document pixels (default 0).' },\n y: { type: 'integer', description: 'Top of the image in document pixels (default 0).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1 (default 1).' },\n blend: BLEND,\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a pixel layer', ...WRITE },\n handler: async (args) => {\n if (args.color !== undefined && args.image !== undefined) throw new LayerOperationError('Give color or image, not both.');\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n const init = {\n name: args.name as string | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n ...placementFrom(args),\n };\n const id =\n typeof args.image === 'string'\n ? await controller.addPixelLayerFromImage(args.image, init)\n : controller.addPixelLayer({ ...init, color: args.color as string | undefined });\n return { layer: describe(id) };\n },\n };\n\n const duplicate: AgentTool = {\n name: 'duplicate_layer',\n title: 'Duplicate a layer',\n description: 'Duplicates a layer (groups with their children) directly above it and makes the copy active. One undo step. Returns the copy with its new id.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' } },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Duplicate a layer', ...WRITE },\n handler: ({ layerId, name }) => ({ layer: describe(controller.duplicateLayer(exists(layerId), name as string | undefined)) }),\n };\n\n const remove: AgentTool = {\n name: 'remove_layer',\n title: 'Delete a layer',\n description:\n 'Deletes a layer (a group with its children). Refused for a layer locked with all. Prefer hiding it (set_layer_properties visible false) when it may be wanted back. One undo step. Returns the removed id and the remaining top-level ids.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayer(id);\n return { removed: id, layers: controller.getLayers().map((l) => l.id) };\n },\n };\n\n const move: AgentTool = {\n name: 'move_layer',\n title: 'Move a layer in the stack',\n description:\n 'Moves a layer above or below another layer (aboveId / belowId, in that layer\\'s group), into a group (parentId, optional index), or to an index at the top level. Give exactly one of aboveId, belowId, parentId, or index alone. One undo step. Returns the new parentId and index.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, ...PLACEMENT },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer in the stack', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n return { layerId: id, ...controller.moveLayer(id, placementFrom(args)) };\n },\n };\n\n const setProps: AgentTool = {\n name: 'set_layer_properties',\n title: 'Set layer properties',\n description:\n 'Changes a layer\\'s name, visible, opacity (0..1), fillOpacity (0..1, pixel layers: fades the pixels but not masks), blend (pass-through is for groups), clipped (clip to the layer below), locks ({pixels, transparency, position, all}) or collapsed (groups). Only the given fields change. Refused (except visible, locks and collapsed) on a layer locked with all. One undo step. Returns the layer after the change.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n name: { type: 'string', description: 'New name.' },\n visible: { type: 'boolean', description: 'Show (true) or hide (false).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1.' },\n fillOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill 0..1 (pixel layers).' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode; pass-through for groups only.' },\n clipped: { type: 'boolean', description: 'Clip to the nearest unclipped layer below.' },\n locks: {\n type: 'object',\n description: 'Locks to set (missing keys are unlocked).',\n properties: {\n pixels: { type: 'boolean', description: 'Refuse painting and filling.' },\n transparency: { type: 'boolean', description: 'Painting keeps alpha.' },\n position: { type: 'boolean', description: 'Refuse moving.' },\n all: { type: 'boolean', description: 'Everything, including properties and deletion.' },\n },\n additionalProperties: false,\n },\n collapsed: { type: 'boolean', description: 'Collapse a group in the layers panel.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer properties', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['name', 'visible', 'opacity', 'fillOpacity', 'blend', 'clipped', 'locks', 'collapsed']) if (args[k] !== undefined) props[k] = args[k];\n if (!Object.keys(props).length) throw new LayerOperationError('Give at least one property to change.');\n controller.setLayerProperties(id, props);\n return { layer: describe(id) };\n },\n };\n\n const group: AgentTool = {\n name: 'group_layers',\n title: 'Group layers',\n description:\n 'Puts sibling layers (same group or top level) into a new pass-through group at the position of the topmost one, keeping their order. One undo step. Returns the group with its id and children.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Ids of the layers to group (from list_layers).' },\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n },\n required: ['layerIds'],\n additionalProperties: false,\n },\n annotations: { title: 'Group layers', ...WRITE },\n handler: ({ layerIds, name }) => {\n const ids = (layerIds as string[]).map((id) => exists(id, 'layerIds[]'));\n return { group: describe(controller.groupLayers(ids, name as string | undefined)) };\n },\n };\n\n const addGroup: AgentTool = {\n name: 'add_group',\n title: 'Add an empty group',\n description:\n 'Adds an empty group (pass-through unless blend is given) above the active layer, or at aboveId / belowId / parentId / index, and makes it active. Move layers into it with move_layer parentId, or use group_layers to group existing layers. One undo step. Returns the group.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode (default pass-through).' },\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add an empty group', ...WRITE },\n handler: (args) => {\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n return { group: describe(controller.addGroup({ name: args.name as string | undefined, blend: args.blend as never, ...placementFrom(args) })) };\n },\n };\n\n const ungroup: AgentTool = {\n name: 'ungroup_layer',\n title: 'Ungroup a group',\n description: 'Replaces a group by its children at its position (the group\\'s own opacity, blend and mask are dropped). One undo step. Returns the child ids bottom to top.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'Group id.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Ungroup a group', ...WRITE },\n handler: ({ layerId }) => ({ childIds: controller.ungroupLayer(exists(layerId)) }),\n };\n\n const mergeDown: AgentTool = {\n name: 'merge_down',\n title: 'Merge a layer down',\n description:\n 'Composites a layer (pixel, adjustment, group or annotation) onto the pixel layer directly below it. The lower layer keeps its name, opacity and blend; masks, fill and filters are baked into its pixels. Refused when the layer below is not a pixel layer or is locked, or when the layer is hidden. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'The upper layer to merge down.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge a layer down', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.mergeDown(exists(layerId))) }),\n };\n\n const mergeVisible: AgentTool = {\n name: 'merge_visible',\n title: 'Merge visible layers',\n description: 'Composites every visible top-level layer into one normal pixel layer at the position of the lowest of them; hidden layers stay. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Merge visible layers', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.mergeVisible()) }),\n };\n\n const flatten: AgentTool = {\n name: 'flatten_image',\n title: 'Flatten the image',\n description: 'Composites every visible layer into one Background pixel layer and discards hidden layers. Crop, rotation and output opacity stay. One undo step. Returns the Background layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Flatten the image', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.flattenImage()) }),\n };\n\n const offset: AgentTool = {\n name: 'offset_layer',\n title: 'Move a layer\\'s pixels',\n description:\n 'Moves a pixel layer (and its mask) to an absolute offset x, y or by dx, dy, in document pixels (integers). Refused when its position is locked. One undo step. Returns the new offset and document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n x: { type: 'integer', description: 'New horizontal offset in document pixels.' },\n y: { type: 'integer', description: 'New vertical offset in document pixels.' },\n dx: { type: 'integer', description: 'Move right by this many pixels (negative: left).' },\n dy: { type: 'integer', description: 'Move down by this many pixels (negative: up).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer\\'s pixels', ...WRITE },\n handler: ({ layerId, x, y, dx, dy }) => {\n const id = exists(layerId);\n if ([x, y, dx, dy].every((v) => v === undefined)) throw new LayerOperationError('Give x and y, or dx and dy.');\n if ((x !== undefined || y !== undefined) && (dx !== undefined || dy !== undefined)) throw new LayerOperationError('Give x / y or dx / dy, not both.');\n return { layerId: id, ...controller.offsetLayer(id, { x: x as number | undefined, y: y as number | undefined, dx: dx as number | undefined, dy: dy as number | undefined }) };\n },\n };\n\n const addMask: AgentTool = {\n name: 'add_layer_mask',\n title: 'Add a layer mask',\n description:\n 'Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal) or selection (the selection\\'s coverage). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection'], description: 'What the mask starts as (default reveal-all).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px (default 0).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer mask', ...WRITE },\n handler: ({ layerId, from, density, feather }) => {\n const id = exists(layerId);\n const mask = controller.addLayerMask(id, (from as 'reveal-all' | 'hide-all' | 'selection' | undefined) ?? 'reveal-all', {\n density: density as number | undefined,\n feather: feather as number | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_layer_mask',\n title: 'Apply a layer mask',\n description: \"Bakes a pixel layer's mask into its pixels (alpha × coverage, including density and feather) and removes the mask. Pixel layers only; refused when its pixels are locked. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Apply a layer mask', ...DESTRUCTIVE },\n handler: async ({ layerId }) => {\n const id = exists(layerId);\n await controller.applyLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const removeMask: AgentTool = {\n name: 'remove_layer_mask',\n title: 'Delete a layer mask',\n description: \"Deletes a layer's mask without applying it (the layer shows everywhere again). Disable it instead with set_layer_mask_properties enabled false to keep it. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer mask', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const maskProps: AgentTool = {\n name: 'set_layer_mask_properties',\n title: 'Set layer mask properties',\n description:\n \"Changes a layer mask's density (0..1), feather (px), enabled flag or linked flag; its pixels are kept. An unlinked mask (linked false) stays in place when the layer is moved, transformed or warped; transform it alone with transform_layer target mask. One undo step. Returns the mask summary.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Density 0..1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px.' },\n enabled: { type: 'boolean', description: 'false ignores the mask but keeps it.' },\n linked: { type: 'boolean', description: 'false unlinks the mask from the layer pixels (it stays put when they move or transform); true links it again.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer mask properties', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, density, feather, enabled, linked }) => {\n const id = exists(layerId);\n if ([density, feather, enabled, linked].every((v) => v === undefined)) throw new LayerOperationError('Give density, feather, enabled or linked.');\n const mask = controller.setLayerMaskProps(id, {\n density: density as number | undefined,\n feather: feather as number | undefined,\n enabled: enabled as boolean | undefined,\n linked: linked as boolean | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const viaCopy: AgentTool = {\n name: 'layer_via_copy',\n title: 'New layer from the selection',\n description:\n 'Layer via Copy: copies the selected pixels of a pixel layer (layerId, default the active layer) to a new layer directly above it and makes it active. cut: true also removes them from the source (Layer via Cut). Needs a selection. One undo step. Returns the new layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Pixel layer to copy from (default the active layer).' },\n cut: { type: 'boolean', description: 'Remove the pixels from the source too (default false).' },\n name: { type: 'string', description: 'Name of the new layer.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'New layer from the selection', ...WRITE },\n handler: ({ layerId, cut, name }) => {\n if (layerId !== undefined) exists(layerId);\n return { layer: describe(controller.layerViaCopy({ layerId: layerId as string | undefined, cut: cut === true, name: name as string | undefined })) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_layer',\n title: 'Rasterize a layer',\n description:\n 'Rasterize Layer: turns an annotation layer into a pixel layer (same id, name, opacity, blend and mask; needs a browser) or bakes a pixel layer\\'s filters (develop, blur) into its pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a layer', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) }),\n };\n\n const filterArgs = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it.' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n } as const;\n const filterValues = (args: Record<string, unknown>) => {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(filterArgs)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n };\n\n const addFilter: AgentTool = {\n name: 'add_layer_filter',\n title: 'Add a layer filter',\n description:\n 'Adds a live filter to a pixel layer: develop (the base sliders: brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px). One of each kind per layer, applied bottom to top before the mask. One undo step. Returns the layer\\'s filters with their ids.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' }, ...filterArgs },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer filter', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n const filters = controller.addLayerFilter(id, { kind: args.kind as LayerFilter['kind'], ...filterValues(args) } as never);\n return { layerId: id, filters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_layer_filter',\n title: 'Change a layer filter',\n description: \"Changes one filter of a pixel layer (only the given values; the kind cannot change). One undo step. Returns the layer's filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers or add_layer_filter.' }, ...filterArgs },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const changes = filterValues(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n return { layerId: id, filters: controller.updateLayerFilter(id, args.filterId as string, changes) };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_layer_filter',\n title: 'Remove a layer filter',\n description: \"Removes one filter from a pixel layer. One undo step. Returns the layer's remaining filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers.' } },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Remove a layer filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = exists(layerId);\n return { layerId: id, filters: controller.removeLayerFilter(id, filterId as string) };\n },\n };\n\n return [\n listLayers,\n setActive,\n addPixelLayer,\n duplicate,\n remove,\n move,\n setProps,\n group,\n addGroup,\n ungroup,\n mergeDown,\n mergeVisible,\n flatten,\n offset,\n addMask,\n applyMask,\n removeMask,\n maskProps,\n viaCopy,\n rasterize,\n addFilter,\n updateFilter,\n removeFilter,\n ];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES } from './blend';\nimport { LayerOperationError, requireLayer } from './layers/ops';\nimport {\n effectsMargin,\n isLayerEffectKind,\n layerEffectLabel,\n LAYER_EFFECT_KINDS,\n type LayerEffectKind,\n type LayerEffects,\n} from '../raster/effects/model';\nimport { builtinPattern, builtinPatternIds } from '../raster/effects/patterns';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers (a pixel, annotation or group layer).' } as const;\nconst EFFECT_ID = { type: 'string', description: 'Effect id from list_layer_effects or add_layer_effect.' } as const;\nconst BLEND_IDS = BLEND_MODES.map((m) => m.id).filter((id) => id !== 'pigment');\n\n/** Shared shapes, referenced as `#/$defs/...`. */\nconst DEFS = {\n color: { type: 'string', pattern: '^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$', description: 'Colour as #rrggbb.' },\n contour: {\n type: 'object',\n 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}].',\n properties: {\n name: { type: 'string', description: 'Display name (e.g. \"Cone\", \"Gaussian\").' },\n points: {\n type: 'array',\n minItems: 2,\n description: 'Curve points.',\n items: {\n type: 'object',\n properties: {\n input: { type: 'number', minimum: 0, maximum: 1, description: 'Coverage in (0..1).' },\n output: { type: 'number', minimum: 0, maximum: 1, description: 'Coverage out (0..1).' },\n corner: { type: 'boolean', description: 'Sharp corner instead of a smooth point (default false).' },\n },\n required: ['input', 'output'],\n additionalProperties: false,\n },\n },\n },\n required: ['name', 'points'],\n additionalProperties: false,\n },\n gradient: {\n type: 'object',\n description: 'A gradient: colour stops, optional opacity stops and its layout.',\n properties: {\n stops: {\n type: 'array',\n minItems: 1,\n description: 'Colour stops { position 0..1, color #rrggbb }.',\n items: {\n type: 'object',\n properties: { position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' }, color: { $ref: '#/$defs/color' } },\n required: ['position', 'color'],\n additionalProperties: false,\n },\n },\n opacityStops: {\n type: 'array',\n description: 'Opacity stops { position 0..1, opacity 0..1 } (default opaque).',\n items: {\n type: 'object',\n properties: {\n position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1.' },\n },\n required: ['position', 'opacity'],\n additionalProperties: false,\n },\n },\n style: { type: 'string', enum: ['linear', 'radial', 'angle', 'reflected', 'diamond'], description: 'Geometry.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Degrees, 90 = bottom to top.' },\n scale: { type: 'number', minimum: 0.1, maximum: 1.5, description: '1 = 100 %.' },\n reverse: { type: 'boolean', description: 'Swap the ends.' },\n align: { type: 'boolean', description: 'Lay out over the layer bounds (true) or the canvas.' },\n offset: {\n type: 'object',\n description: 'Centre offset as fractions of the bounds.',\n 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.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n required: ['stops', 'style', 'angle', 'scale', 'reverse', 'align', 'offset'],\n additionalProperties: false,\n },\n phase: {\n type: 'object',\n description: 'Pattern origin offset in px.',\n properties: { x: { type: 'number', description: 'px.' }, y: { type: 'number', description: 'px.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n fill: {\n description: 'What a stroke or glow paints with. Glows take color or gradient only.',\n anyOf: [\n {\n type: 'object',\n properties: { type: { const: 'color', description: 'Solid colour.' }, color: { $ref: '#/$defs/color' } },\n required: ['type', 'color'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: { type: { const: 'gradient', description: 'Gradient.' }, gradient: { $ref: '#/$defs/gradient' } },\n required: ['type', 'gradient'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: {\n type: { const: 'pattern', description: 'Pattern (strokes only).' },\n patternId: { type: 'string', description: 'Pattern id from list_layer_effects.patterns.' },\n name: { type: 'string', description: 'Display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 %.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Rotation in degrees (default 0).' },\n link: { type: 'boolean', description: 'Tile from the layer (default true) or the canvas origin.' },\n phase: { $ref: '#/$defs/phase' },\n },\n required: ['type', 'patternId', 'scale'],\n additionalProperties: false,\n },\n ],\n },\n} as const;\n\nconst nullable = (schema: Record<string, unknown>, description: string) => ({ anyOf: [schema, { type: 'null' }], description });\n\n/** Every effect setting (each kind takes a subset; the description says which). */\nfunction settingsSchema(forUpdate: boolean): Record<string, unknown> {\n const opt = (schema: Record<string, unknown>, description: string) => (forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description });\n return {\n type: 'object',\n description: forUpdate\n ? 'Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect.'\n : \"Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.\",\n properties: {\n enabled: { type: 'boolean', description: 'All kinds: false keeps the effect but hides it (default true).' },\n blend: { type: 'string', enum: BLEND_IDS, description: 'All but bevel-emboss: blend mode (shadows multiply, glows screen, others normal).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'All but bevel-emboss: 0..1 (shadows / glows 0.75, satin 0.5, others 1).' },\n color: { $ref: '#/$defs/color', description: 'drop-shadow, inner-shadow, satin, color-overlay: #rrggbb.' },\n 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).' },\n useGlobalLight: { type: 'boolean', description: 'Shadows, bevel-emboss: follow the document global light instead of angle (default true).' },\n distance: { type: 'number', minimum: 0, maximum: 30000, description: 'Shadows, satin: offset in px (default 5; satin 11).' },\n spread: { type: 'number', minimum: 0, maximum: 1, description: 'drop-shadow, outer-glow: 0..1 of the size made solid (default 0).' },\n choke: { type: 'number', minimum: 0, maximum: 1, description: 'inner-shadow, inner-glow: 0..1 of the size made solid (default 0).' },\n 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).' },\n contour: opt({ $ref: '#/$defs/contour' }, 'Shadows, glows, satin: coverage contour (default linear).'),\n antiAlias: { type: 'boolean', description: 'Shadows, glows, satin: smooth the contour (default false; satin true).' },\n noise: { type: 'number', minimum: 0, maximum: 1, description: 'Shadows, glows: speckle 0..1 (default 0).' },\n knocksOut: { type: 'boolean', description: 'drop-shadow: the layer hides the shadow beneath its see-through fill (default true).' },\n fill: { $ref: '#/$defs/fill', description: 'outer-glow, inner-glow (color or gradient), stroke (color, gradient or pattern).' },\n 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).' },\n range: { type: 'number', minimum: 0.01, maximum: 1, description: 'Glows: the part of the glow the contour spans (default 0.5).' },\n jitter: { type: 'number', minimum: 0, maximum: 1, description: 'Glows: gradient jitter 0..1 (stored, not rendered; default 0).' },\n source: { type: 'string', enum: ['edge', 'center'], description: 'inner-glow: glow from the edge (default) or the centre.' },\n style: { type: 'string', enum: ['inner-bevel', 'outer-bevel', 'emboss', 'pillow-emboss', 'stroke-emboss'], description: 'bevel-emboss: style (default inner-bevel).' },\n depth: { type: 'number', minimum: 0, maximum: 10, description: 'bevel-emboss: depth, 1 = 100 % (default 1).' },\n direction: { type: 'string', enum: ['up', 'down'], description: 'bevel-emboss: raised (up, default) or sunk (down).' },\n soften: { type: 'number', minimum: 0, maximum: 16, description: 'bevel-emboss: blur of the height in px (default 0).' },\n altitude: { type: 'number', minimum: 0, maximum: 90, description: 'bevel-emboss: light altitude in degrees (default 30).' },\n glossContour: opt({ $ref: '#/$defs/contour' }, 'bevel-emboss: gloss contour over the shading (default linear).'),\n highlightColor: { $ref: '#/$defs/color', description: 'bevel-emboss: highlight colour (default #ffffff).' },\n highlightBlend: { type: 'string', enum: BLEND_IDS, description: 'bevel-emboss: highlight mode (default screen).' },\n highlightOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'bevel-emboss: highlight opacity (default 0.75).' },\n shadowColor: { $ref: '#/$defs/color', description: 'bevel-emboss: shadow colour (default #000000).' },\n shadowBlend: { type: 'string', enum: BLEND_IDS, description: 'bevel-emboss: shadow mode (default multiply).' },\n shadowOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'bevel-emboss: shadow opacity (default 0.75).' },\n contourElement: opt(\n {\n type: 'object',\n properties: {\n contour: { $ref: '#/$defs/contour' },\n range: { type: 'number', minimum: 0, maximum: 1, description: 'Stored range (default 0.5).' },\n antiAlias: { type: 'boolean', description: 'Smooth the contour.' },\n },\n required: ['contour', 'range', 'antiAlias'],\n additionalProperties: false,\n },\n \"bevel-emboss: the Contour element, shaping the bevel's height profile (default off).\"\n ),\n texture: opt(\n {\n type: 'object',\n properties: {\n patternId: { type: 'string', description: 'Pattern id from list_layer_effects.patterns.' },\n name: { type: 'string', description: 'Display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 %.' },\n depth: { type: 'number', minimum: -10, maximum: 10, description: '1 = +100 %; negative carves the pattern in.' },\n invert: { type: 'boolean', description: 'Invert the pattern.' },\n link: { type: 'boolean', description: 'Tile from the layer (default true).' },\n phase: { $ref: '#/$defs/phase' },\n },\n required: ['patternId', 'scale', 'depth', 'invert'],\n additionalProperties: false,\n },\n \"bevel-emboss: the Texture element, a pattern's luminance as extra height (default off).\"\n ),\n invert: { type: 'boolean', description: 'satin: invert (default true).' },\n gradient: { $ref: '#/$defs/gradient', description: 'gradient-overlay: the gradient (default black to white, 90°).' },\n dither: { type: 'boolean', description: 'gradient-overlay: dither (stored; default false).' },\n patternId: { type: 'string', description: 'pattern-overlay: pattern id from list_layer_effects.patterns (default checker).' },\n name: { type: 'string', description: 'pattern-overlay: pattern display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: 'pattern-overlay: 1 = 100 % (default 1).' },\n link: { type: 'boolean', description: 'pattern-overlay: tile from the layer (default true) or the canvas origin.' },\n phase: { $ref: '#/$defs/phase', description: 'pattern-overlay: origin offset in px (default 0, 0).' },\n position: { type: 'string', enum: ['outside', 'inside', 'center'], description: 'stroke: where the stroke sits on the edge (default outside).' },\n },\n additionalProperties: false,\n };\n}\n\n/** The patterns pattern effects can use: the document's (from a PSD), then the built-in ones. */\nfunction patternList(controller: ImageEditorController): Array<{ id: string; name: string; width: number; height: number; builtin: boolean }> {\n const doc = controller.getDocumentPatterns().map((p) => ({ id: p.id, name: p.name, width: p.width, height: p.height, builtin: false }));\n const builtins = builtinPatternIds().map((id) => {\n const p = builtinPattern(id)!;\n return { id, name: p.name, width: p.width, height: p.height, builtin: true };\n });\n return [...doc, ...builtins];\n}\n\n/** A layer's effects for agents: the master switch, scale, reach and every effect with its settings. */\nexport function describeLayerEffects(effects: LayerEffects): Record<string, unknown> {\n return {\n enabled: effects.enabled,\n scale: effects.scale ?? 1,\n margin: effectsMargin(effects),\n fromPsd: Boolean(effects.psd),\n effects: effects.items.map((e) => ({ ...JSON.parse(JSON.stringify(e)), label: layerEffectLabel(e.kind) })),\n };\n}\n\n/**\n * Agent tools for layer effects: `list_layer_effects`, `add_layer_effect`, `update_layer_effect`,\n * `remove_layer_effect`, `set_layer_effects_enabled` and `set_global_light`. Each write validates\n * every setting first, is one undo step and returns the layer's effects after the change.\n */\nexport function createImageEffectTools(controller: ImageEditorController): AgentTool[] {\n const layerArg = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a layer id from list_layers).');\n requireLayer(controller.getLayers(), id);\n return id;\n };\n const result = (layerId: string, extra: Record<string, unknown> = {}) => ({ layerId, ...extra, ...describeLayerEffects(controller.getLayerEffects(layerId)) });\n\n const list: AgentTool = {\n name: 'list_layer_effects',\n title: 'List layer effects',\n description:\n \"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.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'List layer effects', ...READ },\n handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(controller), kinds: [...LAYER_EFFECT_KINDS] }),\n };\n\n const add: AgentTool = {\n name: 'add_layer_effect',\n title: 'Add a layer effect',\n description:\n \"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.\",\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: [...LAYER_EFFECT_KINDS], description: 'Effect kind.' },\n settings: settingsSchema(false),\n index: { type: 'integer', minimum: 0, description: \"Position in the layer's effect list (default the end).\" },\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer effect', ...WRITE },\n handler: ({ layerId, kind, settings, index }) => {\n const id = layerArg(layerId);\n if (!isLayerEffectKind(kind)) throw new LayerOperationError(`kind must be one of ${LAYER_EFFECT_KINDS.join(', ')}.`);\n const { effect } = controller.addLayerEffect(id, kind as LayerEffectKind, (settings as Record<string, unknown>) ?? {}, index as number | undefined);\n return result(id, { effectId: effect.id });\n },\n };\n\n const update: AgentTool = {\n name: 'update_layer_effect',\n title: 'Change a layer effect',\n description:\n \"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.\",\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: { layerId: LAYER_ID, effectId: EFFECT_ID, settings: settingsSchema(true) },\n required: ['layerId', 'effectId', 'settings'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer effect', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, effectId, settings }) => {\n const id = layerArg(layerId);\n const changes = (settings as Record<string, unknown>) ?? {};\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one setting to change.');\n controller.updateLayerEffect(id, effectId as string, changes);\n return result(id);\n },\n };\n\n const remove: AgentTool = {\n name: 'remove_layer_effect',\n title: 'Remove a layer effect',\n 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.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, effectId: EFFECT_ID }, required: ['layerId', 'effectId'], additionalProperties: false },\n annotations: { title: 'Remove a layer effect', ...DESTRUCTIVE },\n handler: ({ layerId, effectId }) => {\n const id = layerArg(layerId);\n controller.removeLayerEffect(id, effectId as string);\n return result(id);\n },\n };\n\n const setEnabled: AgentTool = {\n name: 'set_layer_effects_enabled',\n title: 'Switch layer effects on or off',\n description:\n \"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.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n enabled: { type: 'boolean', description: 'true renders the enabled effects; false hides them all.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: 'Scale Effects, 1 = 100 % (unchanged when left out).' },\n },\n required: ['layerId', 'enabled'],\n additionalProperties: false,\n },\n annotations: { title: 'Switch layer effects on or off', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, enabled, scale }) => {\n const id = layerArg(layerId);\n controller.setLayerEffectsEnabled(id, enabled as boolean, scale as number | undefined);\n return result(id);\n },\n };\n\n const light: AgentTool = {\n name: 'set_global_light',\n title: 'Set the global light',\n description:\n 'Sets the document global light that shadows and bevels with useGlobalLight follow (Photoshop default: angle 120°, altitude 30°). One undo step. Returns the light.',\n inputSchema: {\n type: 'object',\n properties: {\n angle: { type: 'number', minimum: -360, maximum: 360, description: \"Degrees, counter-clockwise from 3 o'clock: where the light comes from (unchanged when left out).\" },\n altitude: { type: 'number', minimum: 0, maximum: 90, description: 'Degrees above the canvas, for bevels (unchanged when left out).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the global light', ...WRITE_IDEMPOTENT },\n handler: ({ angle, altitude }) => {\n if (angle === undefined && altitude === undefined) throw new LayerOperationError('Give angle, altitude or both.');\n const next: { angle?: number; altitude?: number } = {};\n if (angle !== undefined) next.angle = angle as number;\n if (altitude !== undefined) next.altitude = altitude as number;\n return { globalLight: controller.setGlobalLight(next) };\n },\n };\n\n return [list, add, update, remove, setEnabled, light];\n}\n","/**\n * The paint tools' view model, shared by the React and Svelte editors: the brush options bar's\n * controls, Photoshop's shortcuts (B, Shift+B, E, V, [ ], Shift+[ ], number keys, D, X) and a\n * pointer gesture that turns pointer events into strokes (Brush, Mixer Brush, Eraser) or layer\n * moves (Move).\n */\n\nimport type { ControlSpec } from '../controls';\nimport type { ImageEditorController, PaintStrokeSession } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { MAX_BRUSH_SIZE, type BrushSettings, type MixerSettings } from '../../paint';\n\nconst plain = { kind: 'plain' } as const;\n\n/** Controls of the brush options bar (`hardness`, `opacity`, `flow`, `smoothing` shown in %). */\nexport const BRUSH_OPTION_CONTROLS: readonly (ControlSpec & { id: 'size' | 'hardness' | 'opacity' | 'flow' | 'smoothing' })[] = [\n { id: 'size', label: 'Size', section: 'layer', min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain, unit: 'px' },\n { id: 'hardness', label: 'Hardness', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'flow', label: 'Flow', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'smoothing', label: 'Smoothing', section: 'layer', min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** Controls of the Mixer Brush (shown in %, with the brush controls, while the mixer is active). */\nexport const MIXER_OPTION_CONTROLS: readonly (ControlSpec & { id: 'wet' | 'load' | 'mix' })[] = [\n { id: 'wet', label: 'Wet', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'load', label: 'Load', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'mix', label: 'Mix', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** The Mixer Brush's mixing models, for its RGB / Pigment toggle. */\nexport const MIXER_MIXING_OPTIONS: readonly { id: MixerSettings['mixing']; label: string; title: string }[] = [\n { id: 'rgb', label: 'RGB', title: 'Mix colours as RGB averages' },\n { id: 'pigment', label: 'Pigment', title: 'Mix colours like paint: blue and yellow make green' },\n];\n\n/** The paint tools in the toolbar. */\nexport const PAINT_TOOLS = [\n { id: 'move', label: 'Move', shortcut: 'V' },\n { id: 'brush', label: 'Brush', shortcut: 'B' },\n { id: 'mixer', label: 'Mixer', shortcut: 'Shift+B' },\n { id: 'eraser', label: 'Eraser', shortcut: 'E' },\n] as const;\n\nexport type PaintTool = (typeof PAINT_TOOLS)[number]['id'];\n\nexport const isPaintTool = (tool: string): tool is PaintTool => tool === 'move' || tool === 'brush' || tool === 'mixer' || tool === 'eraser';\n\n/** `#rrggbb` of an sRGB triple (0..1), e.g. the Mixer Brush's reservoir. */\nexport function rgbUnitToHex(c: readonly [number, number, number]): string {\n return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')).join('')}`;\n}\n\n/** Photoshop's bracket step for a brush size. */\nfunction sizeStep(size: number): number {\n return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;\n}\n\nexport interface ShortcutEvent {\n key: string;\n shiftKey?: boolean;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n /** The event target: shortcuts are ignored while typing in inputs. */\n target?: unknown;\n}\n\nconst isTyping = (t: unknown) => {\n const el = t as { tagName?: string; isContentEditable?: boolean } | null;\n return Boolean(el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'));\n};\n\n/**\n * Applies a paint shortcut. Returns true when it was one (the caller then prevents the default).\n * B / Shift+B (Mixer Brush) / E / V choose the tool; [ and ] change the size, with Shift the hardness by 25 %; 1..9 set the\n * opacity to 10..90 % and 0 to 100 %; D resets the colours to black and white; X swaps them.\n */\nexport function handlePaintShortcut(controller: ImageEditorController, e: ShortcutEvent): boolean {\n if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;\n const brush = controller.getState().paint.brush;\n const set = (b: Partial<BrushSettings>) => controller.setPaintOptions({ brush: b });\n const key = e.key;\n switch (key.toLowerCase()) {\n case 'b':\n controller.setTool(e.shiftKey || key === 'B' ? 'mixer' : 'brush');\n return true;\n case 'e':\n controller.setTool('eraser');\n return true;\n case 'v':\n controller.setTool('move');\n return true;\n case 'd':\n controller.resetPaintColors();\n return true;\n case 'x':\n controller.swapPaintColors();\n return true;\n }\n if (key === '[' || key === '{') {\n if (e.shiftKey || key === '{') set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });\n else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });\n return true;\n }\n if (key === ']' || key === '}') {\n if (e.shiftKey || key === '}') set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });\n else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });\n return true;\n }\n if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {\n set({ opacity: key === '0' ? 1 : Number(key) / 10 });\n return true;\n }\n return false;\n}\n\n/** One pointer sample in document pixels. */\nexport interface PaintPointerSample extends ImagePoint {\n /** 0..1; a mouse is 1. */\n pressure: number;\n}\n\n/** Pressure of a pointer event: pens report it; mice report 0.5 while pressed, which means \"full\". */\nexport function pointerPressure(e: { pointerType?: string; pressure?: number }): number {\n if (e.pointerType === 'pen' && typeof e.pressure === 'number' && e.pressure > 0) return Math.min(1, e.pressure);\n return 1;\n}\n\n/**\n * Turns pointer events into strokes (Brush, Mixer Brush, Eraser on the active layer's target) or layer moves\n * (Move on the active pixel layer). `down` returns an error message when the tool cannot act.\n */\nexport class PaintGesture {\n private stroke: PaintStrokeSession | null = null;\n private move: { id: string; start: ImagePoint; origin: { x: number; y: number }; moved: boolean } | null = null;\n /** The last refusal (e.g. \"layer is locked\"), for the editor to show. */\n error: string | null = null;\n\n constructor(private readonly controller: ImageEditorController) {}\n\n get active(): boolean {\n return Boolean(this.stroke || this.move);\n }\n\n down(sample: PaintPointerSample): string | null {\n this.error = null;\n const tool = this.controller.getState().activeTool;\n try {\n if (tool === 'move') {\n const layer = this.controller.getActiveLayer();\n if (!layer) throw new Error('Select a layer to move in the Layers panel.');\n if (layer.type !== 'pixel') throw new Error(`Only pixel layers move; \"${layer.name}\" is a ${layer.type} layer.`);\n if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer \"${layer.name}\" has its position locked.`);\n this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };\n return null;\n }\n if (tool === 'brush' || tool === 'eraser' || tool === 'mixer') {\n this.stroke = this.controller.beginStroke({ tool });\n this.stroke.push([sample]);\n return null;\n }\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.stroke = null;\n this.move = null;\n }\n return this.error;\n }\n\n /** Feeds pointer samples (all coalesced events of one move, in order). */\n moveTo(samples: PaintPointerSample[]): void {\n if (!samples.length) return;\n if (this.stroke) {\n this.stroke.push(samples);\n return;\n }\n const m = this.move;\n if (m) {\n const last = samples[samples.length - 1];\n const x = m.origin.x + Math.round(last.x - m.start.x);\n const y = m.origin.y + Math.round(last.y - m.start.y);\n try {\n // The first move of a drag records the undo step; the rest replace it.\n this.controller.offsetLayer(m.id, { x, y }, !m.moved);\n m.moved = true;\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.move = null;\n }\n }\n }\n\n up(): void {\n if (this.stroke) {\n try {\n this.stroke.end();\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n }\n this.stroke = null;\n this.move = null;\n }\n\n cancel(): void {\n this.stroke?.cancel();\n this.stroke = null;\n this.move = null;\n }\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { MAX_BRUSH_SIZE, MIXING_MODELS, type BrushSettings, type MixerLoad, type MixerSettings, type StrokePoint } from '../paint';\nimport type { LayerTarget } from './layers/model';\nimport { rgbUnitToHex } from './layers/paint-ui';\nimport { LayerOperationError } from './layers/ops';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to paint (default the active layer; see list_layers).' },\n channel: {\n type: 'string',\n enum: ['pixels', 'mask', 'filter-mask'],\n description: \"pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals.\",\n },\n} as const;\n\nconst BRUSH = {\n type: 'object',\n description: 'Brush settings; missing fields use the current paint options (default round, hard, 20 px).',\n properties: {\n size: { type: 'number', minimum: 1, maximum: MAX_BRUSH_SIZE, description: 'Diameter in px (default 20).' },\n hardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness 0 (soft) .. 1 (hard, default).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees (default 0).' },\n roundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness (default 1).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity: overlapping dabs never exceed it (default 1).' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab towards the opacity (default 1).' },\n smoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Point smoothing 0..0.95 (default 0).' },\n pressureSize: { type: 'boolean', description: 'Point pressure scales the size (default true).' },\n pressureOpacity: { type: 'boolean', description: 'Point pressure scales the opacity (default false).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'How paint combines with the layer (default normal). pigment mixes it in like real paint (blue into yellow gives green) instead of laying it over.' },\n },\n additionalProperties: false,\n} as const;\n\nconst MIXER = {\n type: 'object',\n description:\n 'Mixer Brush settings (tool mixer only); missing fields use the current mixer options (wet 0.5, load 0.5, mix 0.5, loadEachStroke true, cleanEachStroke true, mixing rgb).',\n properties: {\n wet: { type: 'number', minimum: 0, maximum: 1, description: 'How much canvas colour each dab picks up, 0 (dry: paints only the reservoir) .. 1 (default 0.5).' },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'How much paint the reservoir holds, 0 .. 1 (default 0.5); below 1 it drains per dab, so long strokes run dry.' },\n mix: { type: 'number', minimum: 0, maximum: 1, description: \"How far each dab's paint moves from the reservoir towards the picked-up colour, 0 .. 1 (default 0.5).\" },\n loadEachStroke: { type: 'boolean', description: 'Refill the reservoir with `color` (or the foreground) before the stroke (default true). Off, the brush keeps what is left of its last load.' },\n cleanEachStroke: { type: 'boolean', description: 'Empty the pickup well after the stroke (default true). Off, the next stroke starts with the colour this one picked up.' },\n mixing: { type: 'string', enum: [...MIXING_MODELS], description: 'rgb (default): RGB averages. pigment: mixes like paint (Kubelka-Munk), so blue dragged through yellow makes green.' },\n },\n additionalProperties: false,\n} as const;\n\n/** A Mixer Brush load as tool output: hex colours and the amount left. */\nexport function mixerLoadResult(load: MixerLoad): { reservoir: string | null; amount: number; pickup: string | null } {\n return {\n reservoir: load.reservoir ? rgbUnitToHex(load.reservoir) : null,\n amount: Math.round(load.amount * 1000) / 1000,\n pickup: load.pickup ? rgbUnitToHex(load.pickup) : null,\n };\n}\n\nfunction readTarget(args: Record<string, unknown>): { layerId?: string; channel?: LayerTarget } {\n return { layerId: args.layerId as string | undefined, channel: args.channel as LayerTarget | undefined };\n}\n\n/**\n * Agent tools for painting: `paint_stroke` (brush, Mixer Brush or eraser along points, on pixels\n * or a mask), `fill_pixels` and `clear_pixels` (Edit › Fill / Clear inside the selection), each\n * one undo step returning the layer, channel and changed bounds; and the Mixer Brush's load:\n * `get_mixer_brush`, `load_mixer_brush` and `clean_mixer_brush` (view state, no undo).\n */\nexport function createImagePaintTools(controller: ImageEditorController): AgentTool[] {\n const paintStroke: AgentTool = {\n name: 'paint_stroke',\n title: 'Paint a brush stroke',\n description:\n 'Paints one stroke with the brush, smears and blends paint with the Mixer Brush (tool mixer: its reservoir mixes with colour picked up from the layer under each dab, in RGB or pigment mixing), or removes pixels with the eraser, through points in document pixels (optional pressure 0..1 each) on a pixel layer or a layer mask (the mixer paints pixels only). The selection limits the paint; a transparency-locked layer keeps its alpha (the eraser then paints the background colour). One undo step. Returns the layerId, channel, changed bounds and dab count, and for the mixer its mixerLoad after the stroke ({reservoir, amount, pickup}).',\n inputSchema: {\n type: 'object',\n properties: {\n tool: { type: 'string', enum: ['brush', 'eraser', 'mixer'], description: 'brush (default), eraser, or mixer (the Mixer Brush: picks up and blends the layer colour under each dab).' },\n points: {\n type: 'array',\n minItems: 1,\n maxItems: 10000,\n description: 'The path in document pixels; one point paints one dab.',\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in px.' },\n y: { type: 'number', description: 'Document y in px.' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pen pressure (default 1).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n color: {\n type: 'string',\n description:\n 'Paint colour #rrggbb (default the foreground). On a mask its grey value: #000000 hides, #ffffff reveals. For the mixer, the colour its reservoir is loaded with before the stroke (given, it loads even with loadEachStroke false).',\n },\n brush: BRUSH,\n mixer: MIXER,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: (args) => {\n const tool = args.tool as 'brush' | 'eraser' | 'mixer' | undefined;\n if (args.mixer !== undefined && tool !== 'mixer') throw new LayerOperationError('mixer settings apply to the Mixer Brush only; pass tool \"mixer\", or drop mixer.');\n const result = controller.paintStroke({\n tool,\n points: args.points as StrokePoint[],\n color: args.color as string | undefined,\n brush: args.brush as Partial<BrushSettings> | undefined,\n mixer: args.mixer as Partial<MixerSettings> | undefined,\n ...readTarget(args),\n });\n return result.mixerLoad ? { ...result, mixerLoad: mixerLoadResult(result.mixerLoad) } : result;\n },\n };\n\n const getMixer: AgentTool = {\n name: 'get_mixer_brush',\n title: 'Get the Mixer Brush',\n description:\n \"Reads the Mixer Brush: its settings (wet, load, mix, loadEachStroke, cleanEachStroke, mixing) and what it carries now (reservoir #rrggbb or null, amount 0..1 left, pickup #rrggbb or null). Call it before a paint_stroke with tool mixer and loadEachStroke false, to know the brush's starting paint.\",\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Get the Mixer Brush', ...READ },\n handler: () => {\n const { mixer, mixerLoad } = controller.getState().paint;\n return { settings: { ...mixer }, load: mixerLoadResult(mixerLoad) };\n },\n };\n\n const loadMixerTool: AgentTool = {\n name: 'load_mixer_brush',\n title: 'Load the Mixer Brush',\n description:\n 'Fills the Mixer Brush reservoir with a colour (amount 1); the picked-up colour is kept. View state, not an undo step. Use it with loadEachStroke false to carry one load across several paint_stroke calls. Returns the load {reservoir, amount, pickup}.',\n inputSchema: {\n type: 'object',\n properties: { color: { type: 'string', description: 'Paint colour #rrggbb (default the foreground).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Load the Mixer Brush', ...WRITE, idempotentHint: true },\n handler: (args) => mixerLoadResult(controller.loadMixerBrush(args.color as string | undefined)),\n };\n\n const cleanMixerTool: AgentTool = {\n name: 'clean_mixer_brush',\n title: 'Clean the Mixer Brush',\n description:\n 'Cleans the Mixer Brush: empties the pickup well so the next stroke starts without canvas colour; with emptyReservoir also drops the loaded paint (the brush then only smears). View state, not an undo step. Returns the load {reservoir, amount, pickup}.',\n inputSchema: {\n type: 'object',\n properties: { emptyReservoir: { type: 'boolean', description: 'Also drop the reservoir (default false).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Clean the Mixer Brush', ...WRITE, idempotentHint: true },\n handler: (args) => mixerLoadResult(args.emptyReservoir === true ? controller.emptyMixerBrush() : controller.cleanMixerBrush()),\n };\n\n const fill: AgentTool = {\n name: 'fill_pixels',\n title: 'Fill pixels',\n description:\n 'Edit › Fill: fills the selection (or the whole canvas without one, or with useSelection false) on a pixel layer or a mask with a colour (default the foreground) at opacity, with a blend mode. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa (default the foreground).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill opacity 0..1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal). pigment mixes the colour into the pixels like paint, opacity being the share of new paint.' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill pixels', ...WRITE },\n handler: (args) =>\n controller.fillPixels({\n color: args.color as string | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n useSelection: args.useSelection as boolean | undefined,\n ...readTarget(args),\n }),\n };\n\n const clear: AgentTool = {\n name: 'clear_pixels',\n title: 'Clear pixels',\n description:\n 'Edit › Clear: removes the selected pixels of a pixel layer (or, without a selection or with useSelection false, all of them); on a transparency-locked layer or a mask it fills with the background colour instead. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'How much to clear 0..1 (default 1).' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Clear pixels', ...DESTRUCTIVE },\n handler: (args) =>\n controller.clearPixels({ opacity: args.opacity as number | undefined, useSelection: args.useSelection as boolean | undefined, ...readTarget(args) }),\n };\n\n return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { describeLayer } from './layer-tools';\nimport { base64ToBytes, bytesToBase64 } from '../raster/png';\nimport { PsdError } from './psd/binary';\nimport { importPsd } from './psd/import';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst DATA = {\n type: 'string',\n minLength: 1,\n description: 'The PSD or PSB file as base64 (a `data:` URL prefix is accepted).',\n} as const;\n\n/** Base64 (or a data URL) → bytes, refusing anything that is not base64 text. */\nexport function psdBytesFromBase64(data: unknown): Uint8Array {\n if (typeof data !== 'string' || !data.trim()) throw new PsdError('data is required: the PSD file as base64.');\n const body = data.trim().replace(/^data:[^,]*;base64,/i, '');\n if (!/^[A-Za-z0-9+/=_\\s-]+$/.test(body)) throw new PsdError('data is not base64; send the PSD file bytes base64-encoded.');\n const bytes = base64ToBytes(body);\n if (bytes.length < 26) throw new PsdError(`data decodes to ${bytes.length} bytes, too short for a PSD (at least 26).`);\n return bytes;\n}\n\n/**\n * Agent tools for Photoshop files: `inspect_psd` (what a file holds and how it would map),\n * `import_psd` (open it as the document) and `export_psd` (the document as a PSD).\n */\nexport function createImagePsdTools(controller: ImageEditorController): AgentTool[] {\n const inspect: AgentTool = {\n name: 'inspect_psd',\n title: 'Inspect a PSD',\n description:\n '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.',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Inspect a PSD', ...READ },\n handler: ({ data }) => {\n const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });\n return {\n width: res.width,\n height: res.height,\n ...res.info,\n resolution: res.resolution ?? null,\n layers: res.summary.map(({ id: _id, ...s }) => s),\n warnings: res.warnings,\n };\n },\n };\n\n const open: AgentTool = {\n name: 'import_psd',\n title: 'Open a PSD as the document',\n description:\n 'Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; 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).',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Open a PSD as the document', ...DESTRUCTIVE },\n handler: ({ data }) => {\n const res = controller.loadPsd(psdBytesFromBase64(data));\n const s = controller.getState();\n return {\n width: res.width,\n height: res.height,\n layerIds: res.layerIds,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n activeLayerId: s.activeLayerId,\n warnings: res.warnings,\n };\n },\n };\n\n const save: AgentTool = {\n name: 'export_psd',\n title: 'Export the document as a PSD',\n description:\n 'Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). 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.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Export the document as a PSD', ...READ },\n handler: async () => {\n const res = await controller.toPsd();\n return {\n data: bytesToBase64(res.bytes),\n mimeType: 'image/vnd.adobe.photoshop',\n size: res.bytes.length,\n psb: res.psb,\n width: res.width,\n height: res.height,\n warnings: res.warnings,\n };\n },\n };\n\n return [inspect, open, save];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { LayerOperationError } from './layers/ops';\nimport { FREE_TRANSFORM_CONTENTS, type FreeTransformContent, type FreeTransformInit, type FreeTransformResult } from './free-transform';\nimport {\n TRANSFORM_ANCHORS,\n WARP_STYLES,\n anchorPoint,\n isTransformAnchor,\n type AffineMatrix,\n type Quad,\n type TransformParams,\n type WarpMesh,\n type WarpStyle,\n} from './transform';\nimport { INTERPOLATIONS, type Interpolation } from '../raster/transform';\nimport { identityWarpMesh, warpMeshColumns, warpMeshRows } from '../geom/warp';\nimport type { LayerTarget } from './layers/model';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\nconst roundPoint = (p: { x: number; y: number }) => ({ x: round(p.x), y: round(p.y) });\n\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'Document pixels from the left.' }, y: { type: 'number', description: 'Document pixels from the top.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to transform (from list_layers; default the active layer).' },\n content: {\n type: 'string',\n enum: FREE_TRANSFORM_CONTENTS.filter((c) => c !== 'selection-outline'),\n description:\n 'What moves: layer (all its pixels with a linked mask), selection (only the selected pixels, committed back into the layer; the outline moves too), selection-copy (Layer via Copy: the selected pixels on a new layer above, then transformed; the original stays) or auto (default: selection when something is selected, else layer).',\n },\n channel: {\n type: 'string',\n enum: ['pixels', 'mask'],\n description: 'pixels (default) moves the pixels and a linked mask; mask moves an unlinked mask alone (a linked mask always moves with its pixels).',\n },\n interpolation: { type: 'string', enum: INTERPOLATIONS, description: 'Resampling: nearest (hard pixels), bilinear or bicubic (default, Photoshop’s Bicubic).' },\n} as const;\n\nconst GEOMETRY = {\n scaleX: { type: 'number', description: 'Width in percent of the frame (default 100; negative flips horizontally).' },\n scaleY: { type: 'number', description: 'Height in percent of the frame (default 100; negative flips vertically).' },\n rotate: { type: 'number', description: 'Rotation in degrees, clockwise (default 0).' },\n skewX: { type: 'number', minimum: -89, maximum: 89, description: 'Horizontal skew in degrees: vertical edges lean right for positive angles (default 0).' },\n skewY: { type: 'number', minimum: -89, maximum: 89, description: 'Vertical skew in degrees: horizontal edges lean down for positive angles (default 0).' },\n dx: { type: 'number', description: 'Move right after scaling and rotating, in document pixels (default 0).' },\n dy: { type: 'number', description: 'Move down after scaling and rotating, in document pixels (default 0).' },\n reference: {\n description: 'Fixed point of scale, skew and rotate: an anchor of the frame (top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right; default center) or a document point {x, y}.',\n anyOf: [{ type: 'string', enum: TRANSFORM_ANCHORS }, POINT],\n },\n matrix: {\n type: 'array',\n items: { type: 'number' },\n minItems: 6,\n maxItems: 6,\n description: \"Instead of the numeric fields: an affine matrix [a, b, c, d, e, f] in document pixels (x' = a·x + c·y + e, y' = b·x + d·y + f).\",\n },\n corners: {\n type: 'array',\n items: POINT,\n minItems: 4,\n maxItems: 4,\n description:\n 'Instead of the numeric fields: where the frame’s corners go, [top-left, top-right, bottom-right, bottom-left] in document pixels (Distort; for Perspective move a pair of corners symmetrically). The quad must stay convex. Get the frame and its corners from get_transform_frame.',\n },\n} as const;\n\nconst NUMERIC = ['scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'dx', 'dy', 'reference'] as const;\n\nfunction geometryFrom(args: Record<string, unknown>): { matrix?: AffineMatrix; corners?: Quad; params?: TransformParams } {\n const numeric = NUMERIC.filter((k) => args[k] !== undefined);\n const ways = [numeric.length > 0, args.matrix !== undefined, args.corners !== undefined].filter(Boolean).length;\n if (ways === 0) throw new LayerOperationError('Give the transform one way: numeric fields (scaleX, scaleY, rotate, skewX, skewY, dx, dy, reference), matrix, or corners.');\n if (ways > 1) throw new LayerOperationError('Give the transform only one way: numeric fields, matrix or corners, not a mix.');\n if (args.matrix !== undefined) return { matrix: args.matrix as unknown as AffineMatrix };\n if (args.corners !== undefined) return { corners: args.corners as unknown as Quad };\n const params: TransformParams = {};\n for (const k of NUMERIC) if (args[k] !== undefined) (params as Record<string, unknown>)[k] = args[k];\n if (params.scaleX === 0 || params.scaleY === 0) throw new LayerOperationError('scaleX and scaleY must not be 0 (that collapses the layer to a line).');\n if (typeof params.reference === 'string' && !isTransformAnchor(params.reference)) throw new LayerOperationError(`Unknown reference \"${params.reference}\". Use ${TRANSFORM_ANCHORS.join(', ')} or {x, y}.`);\n return { params };\n}\n\nfunction targetFrom(args: Record<string, unknown>): FreeTransformInit {\n const init: FreeTransformInit = {};\n if (typeof args.layerId === 'string') init.layerId = args.layerId;\n if (args.content !== undefined) init.content = args.content as FreeTransformContent;\n init.channel = (args.channel as LayerTarget | undefined) ?? 'pixels';\n if (args.interpolation !== undefined) init.interpolation = args.interpolation as Interpolation;\n return init;\n}\n\nfunction summary(res: FreeTransformResult) {\n return {\n layerId: res.layerId,\n content: res.content,\n moved: res.moved,\n changed: res.changed,\n frame: { ...res.box },\n corners: res.corners.map(roundPoint),\n bounds: res.bounds,\n };\n}\n\n/**\n * Agent tools for Free Transform in the image editor: `get_transform_frame`, `transform_layer`\n * (scale, rotate, skew and move as numbers, a matrix, or distort / perspective corners),\n * `warp_layer` (a preset or a Bézier mesh), `transform_selection` (the outline only),\n * `flip_layer` and `rotate_layer_90`. Every write is one undo step, resamples tile by tile and\n * returns the new bounds.\n */\nexport function createImageTransformTools(controller: ImageEditorController): AgentTool[] {\n const getFrame: AgentTool = {\n name: 'get_transform_frame',\n title: 'Get the Free Transform frame',\n description:\n 'Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. For a smart object (smartObject true) the frame is the contents’ rectangle in contents pixels and the corners are where they land now. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: TARGET.layerId,\n content: { type: 'string', enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET.content.description} selection-outline frames the selection outline alone.` },\n channel: TARGET.channel,\n meshColumns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns of an identity warp mesh to return (omit for none).' },\n meshRows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows of an identity warp mesh to return (default meshColumns).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Get the Free Transform frame', ...READ },\n handler: (args) => {\n const init = targetFrom(args);\n // A copy's frame is the selection's; framing it must not add the layer.\n if (init.content === 'selection-copy') init.content = 'selection';\n const s = controller.beginFreeTransform(init);\n const out: Record<string, unknown> = {\n layerId: s.layerId,\n content: args.content === 'selection-copy' ? 'selection-copy' : s.content,\n moved: [...s.moves],\n frame: { ...s.box },\n corners: s.startGeometry.corners.map(roundPoint),\n center: roundPoint(anchorPoint(s.startGeometry, 'center')),\n };\n if (s.smartObject) out.smartObject = true;\n s.cancel();\n if (args.meshColumns !== undefined) {\n const cols = args.meshColumns as number;\n const rows = (args.meshRows as number | undefined) ?? cols;\n const mesh = identityWarpMesh(s.box, cols, rows);\n out.mesh = { columns: cols, rows, pointsPerRow: warpMeshColumns(mesh), pointRows: warpMeshRows(mesh), points: mesh.points.map(roundPoint) };\n }\n return out;\n },\n };\n\n const transformLayer: AgentTool = {\n name: 'transform_layer',\n title: 'Free Transform a layer',\n description:\n 'Edit › Free Transform: scales, rotates, skews, moves, distorts or puts in perspective a pixel layer (with its linked mask), the selected pixels, a copy of them on a new layer, or an unlinked mask. Give the transform one way: the numeric fields (as Photoshop’s options bar, about a reference point), a matrix, or the frame’s new corners (distort / perspective; read the frame with get_transform_frame). Resamples once, tile by tile; one undo step. Returns layerId (the new layer for selection-copy), what moved, the frame, the corners and the new document bounds.',\n inputSchema: {\n type: 'object',\n properties: { ...TARGET, ...GEOMETRY },\n additionalProperties: false,\n },\n annotations: { title: 'Free Transform a layer', ...WRITE },\n handler: (args) => summary(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) })),\n };\n\n const warpLayer: AgentTool = {\n name: 'warp_layer',\n title: 'Warp a layer',\n description:\n 'Edit › Transform › Warp: bends a pixel layer (with its linked mask), the selected pixels, a copy of them, or an unlinked mask through a preset (style with bend and distortions in percent) or a Bézier mesh over the frame (get the frame and an identity mesh to edit from get_transform_frame with meshColumns). Give style or mesh, not both. One undo step. Returns what moved and the new document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n ...TARGET,\n style: { type: 'string', enum: WARP_STYLES, description: 'Preset warp (Photoshop’s menu): arc, arcLower, arcUpper, arch, bulge, shellLower, shellUpper, flag, wave, fish, rise, fisheye, inflate, squeeze, twist.' },\n bend: { type: 'number', minimum: -100, maximum: 100, description: 'Preset bend in percent, −100..100 (default 50).' },\n horizontal: { type: 'number', minimum: -100, maximum: 100, description: 'Preset horizontal distortion in percent, −100..100 (default 0).' },\n vertical: { type: 'number', minimum: -100, maximum: 100, description: 'Preset vertical distortion in percent, −100..100 (default 0).' },\n orientation: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Bend along the horizontal (default) or vertical axis.' },\n mesh: {\n type: 'object',\n description: 'A custom warp: patch columns and rows, and the (3·columns + 1) × (3·rows + 1) control points row by row in document pixels (anchors where both indices are multiples of 3, Bézier handles between). Start from get_transform_frame’s identity mesh.',\n properties: {\n columns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns.' },\n rows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows.' },\n points: { type: 'array', items: POINT, minItems: 16, description: 'Control points, row by row, document pixels.' },\n },\n required: ['columns', 'rows', 'points'],\n additionalProperties: false,\n },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Warp a layer', ...WRITE },\n handler: (args) => {\n const hasPreset = ['style', 'bend', 'horizontal', 'vertical', 'orientation'].some((k) => args[k] !== undefined);\n if (hasPreset && args.mesh !== undefined) throw new LayerOperationError('Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.');\n if (!hasPreset && args.mesh === undefined) throw new LayerOperationError('Give a preset style or a mesh.');\n let warp: WarpMesh | { style: WarpStyle; bend: number; horizontal?: number; vertical?: number; orientation?: 'horizontal' | 'vertical' };\n if (hasPreset) {\n if (args.style === undefined) throw new LayerOperationError(`style is required with a preset. Styles: ${WARP_STYLES.join(', ')}.`);\n warp = {\n style: args.style as WarpStyle,\n bend: (args.bend as number | undefined) ?? 50,\n horizontal: args.horizontal as number | undefined,\n vertical: args.vertical as number | undefined,\n orientation: args.orientation as 'horizontal' | 'vertical' | undefined,\n };\n } else {\n const m = args.mesh as { columns: number; rows: number; points: { x: number; y: number }[] };\n const want = (3 * m.columns + 1) * (3 * m.rows + 1);\n if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} × ${m.rows} mesh needs (3·${m.columns} + 1) × (3·${m.rows} + 1) = ${want} points, got ${m.points.length}.`);\n const knots = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n return summary(controller.transformLayer({ ...targetFrom(args), warp }));\n },\n };\n\n const transformSelection: AgentTool = {\n name: 'transform_selection',\n title: 'Transform the selection outline',\n description:\n 'Select › Transform Selection: scales, rotates, skews, moves or distorts only the selection outline; no pixels change. Give the transform one way: the numeric fields (about a reference point of the selection’s bounds), a matrix, or the new corners of its bounds. Needs a selection. One undo step. Returns the new selection bounds.',\n inputSchema: {\n type: 'object',\n properties: { interpolation: { ...TARGET.interpolation, description: 'Resampling of soft edges: nearest, bilinear or bicubic (default).' }, ...GEOMETRY },\n additionalProperties: false,\n },\n annotations: { title: 'Transform the selection outline', ...WRITE },\n handler: (args) => {\n const res = controller.transformLayer({ content: 'selection-outline', interpolation: args.interpolation as Interpolation | undefined, ...geometryFrom(args) });\n return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };\n },\n };\n\n const flipLayer: AgentTool = {\n name: 'flip_layer',\n title: 'Flip a layer',\n description:\n 'Edit › Transform › Flip Horizontal or Vertical: mirrors a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (no resampling). To flip the whole image use flip_image. One undo step. Returns the new bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left ↔ right, vertical top ↔ bottom.' },\n layerId: TARGET.layerId,\n content: TARGET.content,\n channel: TARGET.channel,\n },\n required: ['axis'],\n additionalProperties: false,\n },\n annotations: { title: 'Flip a layer', ...WRITE },\n handler: (args) => summary(controller.flipLayer(args.axis as 'horizontal' | 'vertical', targetFrom(args))),\n };\n\n const rotate90: AgentTool = {\n name: 'rotate_layer_90',\n title: 'Rotate a layer by quarter turns',\n description:\n 'Edit › Transform › Rotate 90° / 180°: turns a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (pixels land on whole pixels; no resampling). To rotate the whole image use set_rotation. One undo step. Returns the new bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n turn: { type: 'integer', enum: [90, -90, 180], description: 'Degrees: 90 clockwise, -90 counter-clockwise, or 180.' },\n layerId: TARGET.layerId,\n content: TARGET.content,\n channel: TARGET.channel,\n },\n required: ['turn'],\n additionalProperties: false,\n },\n annotations: { title: 'Rotate a layer by quarter turns', ...WRITE },\n handler: (args) => summary(controller.rotateLayer90(args.turn as 90 | -90 | 180, targetFrom(args))),\n };\n\n return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];\n}\n\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { bytesToBase64 } from '../raster/png';\nimport { INTERPOLATIONS, type Interpolation } from '../raster/transform';\nimport { WARP_STYLES, type AffineMatrix, type Quad, type WarpMesh, type WarpStyle } from './transform';\nimport { BLEND_MODES } from './blend';\nimport { findLayer } from './layers/model';\nimport { LayerOperationError, requireLayer } from './layers/ops';\nimport { requireSmartObject } from './layers/smart';\nimport { describeLayer, maskSummary } from './layer-tools';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\nconst roundPoint = (p: { x: number; y: number }) => ({ x: round(p.x), y: round(p.y) });\n\nconst LAYER_ID = { type: 'string', description: 'Smart object layer id from list_layers (type smart).' } as const;\nconst FILTER_ID = { type: 'string', description: 'Smart filter id from get_smart_object or add_smart_filter.' } as const;\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'Document pixels from the left.' }, y: { type: 'number', description: 'Document pixels from the top.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\nconst FILTER_BLENDS = BLEND_MODES.map((m) => m.id).filter((m) => m !== 'pigment');\n\nconst FILTER_SETTINGS = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it (the eye in the Layers panel; default true).' },\n blend: { type: 'string', enum: FILTER_BLENDS, description: 'How the filter’s result blends with the result below it (Blending Options; default normal).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Strength of the filter’s result over the result below it, 0..1 (default 1).' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 blue to yellow (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n} as const;\n\nfunction settingsFrom(args: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(FILTER_SETTINGS)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n}\n\n/** A PNG (data URL or base64) as a data URL. */\nfunction imageArg(image: unknown): string {\n if (typeof image !== 'string' || !image.trim()) throw new LayerOperationError('image is required: a PNG as a data URL or base64 (other formats need a browser).');\n return image.trim();\n}\n\n/**\n * Agent tools for smart objects in the image editor: `get_smart_object`, `convert_to_smart_object`,\n * `rasterize_smart_object`, `set_smart_object_transform`, `replace_smart_object_contents`,\n * `export_smart_object_contents`, `copy_smart_object`, `edit_smart_object_contents`,\n * `close_smart_object_contents`, and the smart filter stack: `add_smart_filter`,\n * `update_smart_filter`, `remove_smart_filter`, `move_smart_filter`, `set_smart_filters_enabled`,\n * `set_smart_filter_mask`. Every write is one undo step, validates first and returns what changed.\n */\nexport function createImageSmartObjectTools(controller: ImageEditorController): AgentTool[] {\n const smartId = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a smart object id from list_layers).');\n requireSmartObject(controller.getLayers(), id);\n return id;\n };\n const info = (id: string) => {\n const i = controller.getSmartObject(id);\n return {\n ...i,\n placement: { ...i.placement, corners: i.placement.corners.map(roundPoint), center: roundPoint(i.placement.center), scaleX: round(i.placement.scaleX), scaleY: round(i.placement.scaleY), rotate: round(i.placement.rotate), skewX: round(i.placement.skewX) },\n };\n };\n const layerSummary = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;\n };\n\n const getSmart: AgentTool = {\n name: 'get_smart_object',\n title: 'Read a smart object',\n description:\n 'Returns a smart object’s contents (id shared by linked instances, name, kind raster or document, pixel size, the file type it came from, the instances placed from it), its placement (corners in document pixels, warp mesh in contents pixels, interpolation, and scale / rotate / skew in percent and degrees of the contents’ own size when affine), its smart filters bottom to top (ids, kind, settings, enabled, blend, opacity; kind psd marks a Photoshop filter kept but not drawn) with the filter mask, and the rendered bounds. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Read a smart object', ...READ },\n handler: ({ layerId }) => info(smartId(layerId)),\n };\n\n const convert: AgentTool = {\n name: 'convert_to_smart_object',\n title: 'Convert to Smart Object',\n description:\n 'Layer › Smart Objects › Convert to Smart Object: sibling layers become one smart object at the topmost one’s position, so later transforms and filters are non-destructive. One pixel layer keeps its id, name, opacity, blend, fill, mask and effects, and its filters become smart filters (it looks exactly the same); several layers, a group or an adjustment become a nested layer document (edit it with edit_smart_object_contents). One undo step. Returns the new layer and its contents.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Sibling layer ids from list_layers (default the active layer).' },\n name: { type: 'string', description: 'Name of the smart object (default the top layer’s name).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Convert to Smart Object', ...WRITE },\n handler: ({ layerIds, name }) => {\n for (const id of (layerIds as string[] | undefined) ?? []) requireLayer(controller.getLayers(), id);\n const id = controller.convertToSmartObject(layerIds as string[] | undefined, { name: name as string | undefined });\n return { layer: layerSummary(id), smartObject: info(id) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_smart_object',\n title: 'Rasterize a smart object',\n description:\n 'Layer › Smart Objects › Rasterize: the smart object becomes a pixel layer holding what it shows now (smart filters baked; same id, mask, effects, opacity, blend and fill). The contents, placement and filters are gone, so later transforms resample the pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a smart object', ...DESTRUCTIVE },\n handler: ({ layerId }) => ({ layer: layerSummary(controller.rasterizeSmartObject(smartId(layerId))) }),\n };\n\n const setTransform: AgentTool = {\n name: 'set_smart_object_transform',\n title: 'Set a smart object’s transform',\n description:\n 'Sets where a smart object’s contents land, absolutely (not relative to the current transform), and renders it again from the contents, so nothing degrades however often it is transformed. Give at most one of: corners (Distort / Perspective), matrix (contents pixels → document), or the numbers scaleX / scaleY (percent of the contents’ own size), rotate, skewX, skewY and x, y (where the contents’ centre goes; unset numbers keep their current values). warp sets a preset or a mesh in contents pixels (null removes it); with nothing else given the corners stay. A linked mask follows a warp-free change. For a relative nudge use transform_layer. One undo step. Returns the placement and bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n corners: { type: 'array', items: POINT, minItems: 4, maxItems: 4, description: 'Where the contents’ corners land: [top-left, top-right, bottom-right, bottom-left] in document pixels. The quad must stay convex.' },\n matrix: { type: 'array', items: { type: 'number' }, minItems: 6, maxItems: 6, description: \"Affine map from contents pixels to document pixels [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f).\" },\n scaleX: { type: 'number', description: 'Width in percent of the contents’ own width (negative flips).' },\n scaleY: { type: 'number', description: 'Height in percent of the contents’ own height (negative flips).' },\n rotate: { type: 'number', description: 'Rotation in degrees, clockwise.' },\n skewX: { type: 'number', minimum: -89, maximum: 89, description: 'Horizontal skew in degrees.' },\n skewY: { type: 'number', minimum: -89, maximum: 89, description: 'Vertical skew in degrees.' },\n x: { type: 'number', description: 'Document x of the contents’ centre (default where it is now).' },\n y: { type: 'number', description: 'Document y of the contents’ centre (default where it is now).' },\n warp: {\n description: 'A warp over the contents: a preset {style, bend, horizontal, vertical, orientation}, a mesh {columns, rows, points} in contents pixels, or null to remove the warp. Omit to keep the current warp.',\n anyOf: [\n { type: 'null' },\n {\n type: 'object',\n properties: {\n style: { type: 'string', enum: WARP_STYLES, description: 'Preset (Photoshop’s menu).' },\n bend: { type: 'number', minimum: -100, maximum: 100, description: 'Bend in percent (default 50).' },\n horizontal: { type: 'number', minimum: -100, maximum: 100, description: 'Horizontal distortion in percent (default 0).' },\n vertical: { type: 'number', minimum: -100, maximum: 100, description: 'Vertical distortion in percent (default 0).' },\n orientation: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Bend axis (default horizontal).' },\n },\n required: ['style'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: {\n columns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns.' },\n rows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows.' },\n points: { type: 'array', items: POINT, minItems: 16, description: 'The (3·columns + 1) × (3·rows + 1) control points row by row, in contents pixels (0..width × 0..height is no warp).' },\n },\n required: ['columns', 'rows', 'points'],\n additionalProperties: false,\n },\n ],\n },\n interpolation: { type: 'string', enum: INTERPOLATIONS, description: 'Resampling: nearest, bilinear or bicubic (default: as before, bicubic for new smart objects).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set a smart object’s transform', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = smartId(args.layerId);\n let warp: WarpMesh | { style: WarpStyle; bend: number; horizontal?: number; vertical?: number; orientation?: 'horizontal' | 'vertical' } | null | undefined;\n const w = args.warp as Record<string, unknown> | null | undefined;\n if (w === null) warp = null;\n else if (w && 'style' in w) warp = { style: w.style as WarpStyle, bend: (w.bend as number | undefined) ?? 50, horizontal: w.horizontal as number | undefined, vertical: w.vertical as number | undefined, orientation: w.orientation as 'horizontal' | 'vertical' | undefined };\n else if (w) {\n const m = w as { columns: number; rows: number; points: { x: number; y: number }[] };\n const want = (3 * m.columns + 1) * (3 * m.rows + 1);\n if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} × ${m.rows} mesh needs (3·${m.columns} + 1) × (3·${m.rows} + 1) = ${want} points, got ${m.points.length}.`);\n const knots = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n const keys = ['corners', 'matrix', 'scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'x', 'y', 'warp', 'interpolation'];\n if (!keys.some((k) => args[k] !== undefined)) throw new LayerOperationError(`Give something to set: ${keys.join(', ')}.`);\n const res = controller.setSmartObjectTransform(id, {\n corners: args.corners as Quad | undefined,\n matrix: args.matrix as AffineMatrix | undefined,\n scaleX: args.scaleX as number | undefined,\n scaleY: args.scaleY as number | undefined,\n rotate: args.rotate as number | undefined,\n skewX: args.skewX as number | undefined,\n skewY: args.skewY as number | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n warp,\n interpolation: args.interpolation as Interpolation | undefined,\n });\n return { layerId: id, placement: info(id).placement, masksMoved: res.masksMoved, bounds: controller.getLayerBounds(id) };\n },\n };\n\n const replace: AgentTool = {\n name: 'replace_smart_object_contents',\n title: 'Replace a smart object’s contents',\n description:\n 'Layer › Smart Objects › Replace Contents: new raster contents from an image for the smart object and every linked instance (same contents id). Each instance keeps its scale and rotation about the contents’ centre and its smart filters, and renders again. One undo step. Returns the contents id, the new size and the layers that changed.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n image: { type: 'string', description: 'The new contents: a PNG as a data URL or base64 (other formats and URLs need a browser).' },\n name: { type: 'string', description: 'File name of the contents (default the old name with .png).' },\n },\n required: ['layerId', 'image'],\n additionalProperties: false,\n },\n annotations: { title: 'Replace a smart object’s contents', ...DESTRUCTIVE },\n handler: async ({ layerId, image, name }) => controller.replaceSmartObjectContentsFromImage(smartId(layerId), imageArg(image), name as string | undefined),\n };\n\n const exportContents: AgentTool = {\n name: 'export_smart_object_contents',\n title: 'Export a smart object’s contents',\n description:\n 'Layer › Smart Objects › Export Contents: the contents as a file, base64: the file they came from while unedited (a PSB, PNG, JPEG…), else a PNG for raster contents or a PSD / PSB of a nested document. Returns name, mimeType, byteLength and base64. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Export a smart object’s contents', ...READ },\n handler: ({ layerId }) => {\n const f = controller.exportSmartObjectContents(smartId(layerId));\n return { name: f.name, mimeType: f.mimeType, byteLength: f.bytes.length, base64: bytesToBase64(f.bytes) };\n },\n };\n\n const copy: AgentTool = {\n name: 'copy_smart_object',\n title: 'Copy a smart object',\n description:\n 'Duplicates a smart object directly above it. linked true (Duplicate Layer) shares the contents, so editing or replacing them changes both; linked false (Layer › Smart Objects › New Smart Object via Copy) gives the copy its own contents. Placement, filters, mask and effects are copied either way. One undo step. Returns the copy.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n linked: { type: 'boolean', description: 'true shares the contents with the original; false (default) makes independent contents.' },\n name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Copy a smart object', ...WRITE },\n handler: ({ layerId, linked, name }) => {\n const id = controller.copySmartObject(smartId(layerId), linked === true, name as string | undefined);\n return { layer: layerSummary(id), contentsId: controller.getSmartObject(id).contents.id };\n },\n };\n\n const editContents: AgentTool = {\n name: 'edit_smart_object_contents',\n title: 'Edit a smart object’s contents',\n description:\n 'Layer › Smart Objects › Edit Contents: opens the contents as a document of their own (a layer document, or raster contents as one pixel layer \"Layer 0\") with their own undo history. Every other tool (list_layers, paint, adjustments…) then works inside the contents until close_smart_object_contents saves them back. Contents can nest. Returns the contents’ size and layers.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Edit a smart object’s contents', ...WRITE },\n handler: ({ layerId }) => {\n const res = controller.editSmartObjectContents(smartId(layerId));\n const s = controller.getState();\n return { ...res, editing: controller.getEditingContents(), layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))) };\n },\n };\n\n const closeContents: AgentTool = {\n name: 'close_smart_object_contents',\n title: 'Close a smart object’s contents',\n description:\n 'Closes the contents opened with edit_smart_object_contents. save true (default) writes the edits back: the smart object and every linked instance render again, as one undo step in the document; save false drops them. Returns the smart object, whether it changed, the layers that changed and what is still open.',\n inputSchema: {\n type: 'object',\n properties: { save: { type: 'boolean', description: 'true (default) saves the edits into the smart object; false discards them.' } },\n additionalProperties: false,\n },\n annotations: { title: 'Close a smart object’s contents', ...WRITE },\n handler: ({ save }) => ({ ...controller.closeSmartObjectContents(save !== false), editing: controller.getEditingContents() }),\n };\n\n const addFilter: AgentTool = {\n name: 'add_smart_filter',\n title: 'Add a smart filter',\n description:\n 'Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. One undo step. Returns the filter (with its id) and the stack.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' },\n index: { type: 'integer', minimum: 0, description: 'Position in the stack, 0 = bottom, applied first (default the top).' },\n ...FILTER_SETTINGS,\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a smart filter', ...WRITE },\n handler: (args) => {\n const id = smartId(args.layerId);\n const filter = controller.addSmartFilter(id, { kind: args.kind as string, ...settingsFrom(args) }, args.index as number | undefined);\n return { layerId: id, filter, smartFilters: info(id).smartFilters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_smart_filter',\n title: 'Change a smart filter',\n description:\n 'Changes one smart filter: its settings, enabled, blend or opacity (only the given values; the kind cannot change). A Photoshop filter kept from a PSD (kind psd) only takes enabled, blend and opacity. One undo step. Returns the filter and the stack.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: FILTER_ID, ...FILTER_SETTINGS },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a smart filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = smartId(args.layerId);\n const changes = settingsFrom(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n const filter = controller.updateSmartFilter(id, args.filterId as string, changes);\n return { layerId: id, filter, smartFilters: info(id).smartFilters };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_smart_filter',\n title: 'Remove a smart filter',\n description: 'Removes one smart filter (the filter mask goes with the last one). One undo step. Returns the remaining stack.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, filterId: FILTER_ID }, required: ['layerId', 'filterId'], additionalProperties: false },\n annotations: { title: 'Remove a smart filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = smartId(layerId);\n controller.removeSmartFilter(id, filterId as string);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const moveFilter: AgentTool = {\n name: 'move_smart_filter',\n title: 'Reorder a smart filter',\n description: 'Moves one smart filter to a position in the stack (0 = bottom, applied first), as dragging it in the Layers panel. One undo step. Returns the stack in its new order.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: FILTER_ID, index: { type: 'integer', minimum: 0, description: 'New position, 0 = bottom (applied first).' } },\n required: ['layerId', 'filterId', 'index'],\n additionalProperties: false,\n },\n annotations: { title: 'Reorder a smart filter', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, filterId, index }) => {\n const id = smartId(layerId);\n controller.moveSmartFilter(id, filterId as string, index as number);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const setEnabled: AgentTool = {\n name: 'set_smart_filters_enabled',\n title: 'Turn smart filters on or off',\n description: 'Layer › Smart Filter › Disable / Enable Smart Filters: switches the whole stack; each filter keeps its own enabled flag. One undo step. Returns the stack.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, enabled: { type: 'boolean', description: 'false hides every smart filter; true shows them again.' } },\n required: ['layerId', 'enabled'],\n additionalProperties: false,\n },\n annotations: { title: 'Turn smart filters on or off', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, enabled }) => {\n const id = smartId(layerId);\n controller.setSmartFiltersEnabled(id, enabled === true);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const setMask: AgentTool = {\n name: 'set_smart_filter_mask',\n title: 'Set the smart filter mask',\n description:\n 'Sets the one filter mask of a smart object’s smart filters, which says where the filtered result shows over the unfiltered contents: from reveal-all (white), hide-all (black), selection (the selection’s coverage) or none (remove it); keep changes only its properties. Paint it with paint_stroke channel filter-mask. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection', 'none', 'keep'], description: 'What the mask starts from; none removes it; keep only changes the properties below.' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'How much hidden areas hide, 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Gaussian softening in px (default 0).' },\n enabled: { type: 'boolean', description: 'false keeps the mask but ignores it.' },\n linked: { type: 'boolean', description: 'false keeps the mask in place when the smart object is transformed or moved (default linked).' },\n },\n required: ['layerId', 'from'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the smart filter mask', ...WRITE },\n handler: (args) => {\n const id = smartId(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['density', 'feather', 'enabled', 'linked']) if (args[k] !== undefined) props[k] = args[k];\n const mask = controller.setSmartFilterMask(id, args.from as 'reveal-all' | 'hide-all' | 'selection' | 'none' | 'keep', props);\n return { layerId: id, filterMask: maskSummary(mask ?? undefined) };\n },\n };\n\n return [getSmart, convert, rasterize, setTransform, replace, exportContents, copy, editContents, closeContents, addFilter, updateFilter, removeFilter, moveFilter, setEnabled, setMask];\n}\n","import type { AgentTool, AgentToolProvider } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind, ImageAdjustments } from './types';\nimport { normalizeProcessVersion } from './color';\nimport {\n ADJUSTMENT_KINDS,\n isAdjustmentKind,\n} from './adjustment-layers';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport {\n ADJUSTMENT_LAYER_CONTROLS,\n ADJUSTMENT_LAYER_OPACITY_CONTROL,\n IMAGE_ADJUSTMENT_CONTROLS,\n clampControlValue,\n findAdjustmentControl,\n findLayerControl,\n readAdjustmentControls,\n setControlPath,\n} from './controls';\nimport { createImageSelectionTools } from './selection-tools';\nimport { createImageCaptureSurface } from './capture';\nimport { createCaptureTool } from '../tools/capture.js';\nimport { createImageEditTools } from './edit-tools';\nimport { createImageLayerTools } from './layer-tools';\nimport { createImageEffectTools } from './effect-tools';\nimport { createImagePaintTools } from './paint-tools';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { createImagePsdTools } from './psd-tools';\nimport { createImageTransformTools } from './transform-tools';\nimport { createImageSmartObjectTools } from './smart-tools';\n\nexport interface ImageEditorToolProviderOptions {\n /** Provider id (unique per registry). Default `image-editor`. */\n id?: string;\n /** Always prefix tool names with `<namespace>__`. */\n namespace?: string;\n}\n\nconst LAYER_PATH = /^layers\\.([^.]+)\\.(.+)$/;\n\nfunction adjustmentsSummary(adjustments: ImageAdjustments) {\n return {\n process: normalizeProcessVersion(adjustments.process),\n values: readAdjustmentControls(adjustments),\n layers: (adjustments.layers ?? []).map((l) => JSON.parse(JSON.stringify(l))),\n };\n}\n\n/**\n * Agent tools for an image editor, for a `ToolRegistry`:\n *\n * - `get_adjustments`: base slider values (by control id), process version and adjustment layers.\n * - `list_controls`: the control table (ids, ranges, defaults, steps), layer kinds and blend modes.\n * - `set_adjustments {values, label}`: sets base controls (`exposure`) and layer parameters\n * (`layers.<id>.vibrance`, `layers.<id>.opacity`) in one undo step; values are clamped to the\n * control ranges and the applied values are echoed.\n * - `add_adjustment_layer {kind, params?, opacity?, blend?, name?}`.\n * - `render_preview {maxSide}`: the rendered image as a PNG, longest side at most `maxSide`.\n * - `capture_view {region, rect?, ...}`: a screenshot of the whole image, the selection or a rect in\n * natural pixels (up to 8× for close inspection), optionally with the selection shaded.\n * - Selections and masks: `get_selection`, `select_region`, `select_by_color`, `modify_selection`,\n * `refine_selection`, `apply_selection_as_mask` (see `createImageSelectionTools`).\n * - Layers, geometry and history: `get_image_info`, `get_histogram`, `update_adjustment_layer`,\n * `remove_adjustment_layer`, `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`,\n * `flip_image`, `undo_image`, `redo_image` (see `createImageEditTools`).\n * - Free Transform: `get_transform_frame`, `transform_layer`, `warp_layer`, `transform_selection`,\n * `flip_layer`, `rotate_layer_90` (see `createImageTransformTools`).\n * - Smart objects and smart filters: `get_smart_object`, `convert_to_smart_object`,\n * `rasterize_smart_object`, `set_smart_object_transform`, `replace_smart_object_contents`,\n * `export_smart_object_contents`, `copy_smart_object`, `edit_smart_object_contents`,\n * `close_smart_object_contents`, `add_smart_filter`, `update_smart_filter`, `remove_smart_filter`,\n * `move_smart_filter`, `set_smart_filters_enabled`, `set_smart_filter_mask` (see\n * `createImageSmartObjectTools`).\n */\nexport function createImageEditorToolProvider(\n controller: ImageEditorController,\n options: ImageEditorToolProviderOptions = {}\n): AgentToolProvider {\n const getAdjustments: AgentTool = {\n name: 'get_adjustments',\n title: 'Read image adjustments',\n description:\n 'Returns the image editor adjustment values by control id, the rendering process version and the adjustment layers (bottom to top).',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'Read image adjustments',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => adjustmentsSummary(controller.getState().adjustments),\n };\n\n const listControls: AgentTool = {\n name: 'list_controls',\n title: 'List adjustment controls',\n description:\n 'Lists every adjustment control (id, label, section, min, max, step, default), the parameters of each adjustment-layer kind, and the blend modes.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'List adjustment controls',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => ({\n controls: IMAGE_ADJUSTMENT_CONTROLS.map(({ id, label, section, min, max, step, default: def, requiresProcess }) => ({\n id, label, section, min, max, step, default: def, ...(requiresProcess ? { requiresProcess } : {}),\n })),\n layerKinds: ADJUSTMENT_KINDS.map(({ kind, label }) => ({\n kind,\n label,\n controls: [...ADJUSTMENT_LAYER_CONTROLS[kind], ADJUSTMENT_LAYER_OPACITY_CONTROL].map(({ id, label: l, min, max, step, default: def }) => ({\n id, label: l, min, max, step, default: def,\n })),\n })),\n blendModes: BLEND_MODES.map((m) => m.id),\n layerPathFormat: 'layers.<layerId>.<controlId>',\n }),\n };\n\n const setAdjustments: AgentTool = {\n name: 'set_adjustments',\n title: 'Set image adjustments',\n description:\n 'Sets adjustment values by control id in one undoable step. Base controls use their id (e.g. \"exposure\"); layer parameters use \"layers.<layerId>.<controlId>\" (e.g. \"layers.adj_1.vibrance\"). Values are clamped to each control range; the applied values are returned.',\n inputSchema: {\n type: 'object',\n properties: {\n values: {\n type: 'object',\n description: 'Map of control id to number. See list_controls for ids and ranges.',\n additionalProperties: { type: 'number' },\n },\n label: { type: 'string', description: 'Short description of the change (echoed back).' },\n },\n required: ['values'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Set image adjustments',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: ({ values, label }) => {\n const entries = Object.entries((values ?? {}) as Record<string, unknown>);\n if (!entries.length) throw new Error('No values given. Pass {\"values\": {\"<control id>\": number}}.');\n const state = controller.getState();\n const base: Partial<ImageAdjustments> = {};\n const layers = (state.adjustments.layers ?? []).map((l) => ({ ...l }));\n const applied: Record<string, number> = {};\n const clamped: string[] = [];\n const unknown: string[] = [];\n for (const [id, raw] of entries) {\n const m = LAYER_PATH.exec(id);\n if (m) {\n const layer = layers.find((l) => l.id === m[1]);\n const spec = layer ? findLayerControl(layer.kind, m[2]) : undefined;\n if (!layer || !spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n if (spec.id === 'opacity') layer.opacity = v;\n else layer.params = setControlPath(layer.params, spec.id, v) as typeof layer.params;\n applied[id] = v;\n continue;\n }\n const spec = findAdjustmentControl(id);\n if (!spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n (base as Record<string, number>)[id] = v;\n applied[id] = v;\n }\n if (unknown.length) {\n const accepted = IMAGE_ADJUSTMENT_CONTROLS.map((c) => c.id).join(', ');\n throw new Error(\n `Unknown control id(s): ${unknown.join(', ')}. Accepted: ${accepted}, or layers.<layerId>.<controlId> for an existing layer.`\n );\n }\n const changedLayers = Object.keys(applied).some((id) => id.startsWith('layers.'));\n controller.setAdjustments(changedLayers ? { ...base, layers } : base);\n const result: Record<string, unknown> = { applied, clamped };\n if (typeof label === 'string' && label) result.label = label;\n const notes: string[] = [];\n if ('tint' in applied && normalizeProcessVersion(controller.getState().adjustments.process) < 2) {\n notes.push('Tint has no effect on process 1 edits.');\n }\n if (notes.length) result.notes = notes;\n return result;\n },\n };\n\n const addLayer: AgentTool = {\n name: 'add_adjustment_layer',\n title: 'Add an adjustment layer',\n description:\n 'Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: ' +\n ADJUSTMENT_KINDS.map((k) => k.kind).join(', ') +\n '. Omitted params are neutral.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Adjustment kind.' },\n params: { type: 'object', description: 'Initial parameters (see list_controls); merged with neutral defaults.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' },\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (from list_layers). Default: above the other adjustment layers, below annotations.' },\n parentId: { type: 'string', description: 'Put the layer at the top of this group (from list_layers).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Add an adjustment layer',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {\n if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind \"${kind}\".`);\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n if (aboveId !== undefined && parentId !== undefined) throw new Error('Give aboveId or parentId, not both.');\n for (const ref of [aboveId, parentId]) {\n if (ref !== undefined && !controller.getLayer(ref as string)) throw new Error(`Unknown layer \"${ref}\". Call list_layers for ids.`);\n }\n if (parentId !== undefined && controller.getLayer(parentId as string)?.type !== 'group') throw new Error(`Layer \"${parentId}\" is not a group.`);\n const id = controller.addAdjustmentLayer({ kind: kind as AdjustmentKind, params, opacity, blend, name, aboveId, parentId });\n const layer = controller.getAdjustmentLayer(id)!;\n return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };\n },\n };\n\n const renderPreview: AgentTool = {\n name: 'render_preview',\n title: 'Render a preview',\n description:\n 'Renders the edited image (crop, adjustments, layers, annotations) as a PNG whose longest side is at most maxSide pixels. With layerId, renders only that layer (a group with its children) through the same crop and rotation.',\n inputSchema: {\n type: 'object',\n properties: {\n maxSide: { type: 'integer', minimum: 16, maximum: 2048, description: 'Longest side in pixels (default 512).' },\n layerId: { type: 'string', description: 'Render only this layer (from list_layers).' },\n },\n additionalProperties: false,\n },\n annotations: {\n title: 'Render a preview',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: async ({ maxSide, layerId }) => {\n if (layerId !== undefined && !controller.getLayer(layerId as string)) throw new Error(`Unknown layer \"${layerId}\". Call list_layers for ids.`);\n const side = Math.max(16, Math.min(2048, Math.round(typeof maxSide === 'number' ? maxSide : 512)));\n const state = controller.getState();\n const crop = state.crop ?? { x: 0, y: 0, ...state.imageDimensions };\n const quarter = Math.abs(Math.round((state.transform.rotate % 180) / 90)) % 2 === 1;\n const w = quarter ? crop.height : crop.width;\n const h = quarter ? crop.width : crop.height;\n const k = Math.min(1, side / Math.max(w, h, 1));\n const dataUrl = await controller.toDataURL({\n format: 'image/png',\n width: Math.max(1, Math.round(w * k)),\n height: Math.max(1, Math.round(h * k)),\n ...(layerId !== undefined ? { layerIds: [layerId as string] } : {}),\n });\n const comma = dataUrl.indexOf(',');\n if (!dataUrl.startsWith('data:image/png') || comma < 0 || comma === dataUrl.length - 1) {\n throw new Error('Rendering is not available in this environment.');\n }\n return {\n content: [\n { type: 'image', data: dataUrl.slice(comma + 1), mimeType: 'image/png' },\n { type: 'text', text: `Preview ${Math.round(w * k)}×${Math.round(h * k)}` },\n ],\n };\n },\n };\n\n return {\n id: options.id ?? 'image-editor',\n namespace: options.namespace,\n tools: [\n getAdjustments,\n listControls,\n setAdjustments,\n addLayer,\n renderPreview,\n createCaptureTool(createImageCaptureSurface(controller), { subject: 'the image (source orientation, before crop and rotation)', defaultPadding: 0 }),\n ...createImageSelectionTools(controller),\n ...createImageEditTools(controller),\n ...createImageLayerTools(controller),\n ...createImageTransformTools(controller),\n ...createImageSmartObjectTools(controller),\n ...createImageEffectTools(controller),\n ...createImagePaintTools(controller),\n ...createPigmentTools(),\n ...createImagePsdTools(controller),\n ],\n };\n}\n\n","import type { CurvePoint } from './types';\nimport { curveLutFromLevels } from './adjustment-layers';\n\n/**\n * Framework-neutral helpers for a Curves editor drawn in a `width` × `height` box (levels 0..255\n * on both axes, output up). The React and Svelte curve editors share them.\n */\n\nconst clampLevel = (v: number) => Math.min(255, Math.max(0, Math.round(v)));\n\n/** Box coordinates of a curve point. */\nexport function curvePointToBox(p: CurvePoint, width: number, height: number): { x: number; y: number } {\n return { x: (p.input / 255) * width, y: height - (p.output / 255) * height };\n}\n\n/** The curve point (whole levels, clamped) under box coordinates. */\nexport function boxToCurvePoint(x: number, y: number, width: number, height: number): CurvePoint {\n return { input: clampLevel((x / (width || 1)) * 255), output: clampLevel(((height - y) / (height || 1)) * 255) };\n}\n\n/** An SVG polyline path of the evaluated curve (spline, clamped, constant beyond the end points). */\nexport function curveSvgPath(points: readonly CurvePoint[], width: number, height: number, samples = 64): string {\n const lut = curveLutFromLevels(points);\n const n = lut.length - 1;\n let d = '';\n for (let i = 0; i <= samples; i++) {\n const t = i / samples;\n const y = lut[Math.round(t * n)];\n d += `${i === 0 ? 'M' : ' L'}${round2(t * width)} ${round2(height - y * height)}`;\n }\n return d;\n}\n\n/** Index of the point within `radius` box units of (x, y), nearest first; -1 for none. */\nexport function hitTestCurvePoint(\n points: readonly CurvePoint[],\n x: number,\n y: number,\n width: number,\n height: number,\n radius = 8\n): number {\n let best = -1;\n let bestD = radius * radius;\n points.forEach((p, i) => {\n const b = curvePointToBox(p, width, height);\n const d = (b.x - x) ** 2 + (b.y - y) ** 2;\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n });\n return best;\n}\n\n/** Inserts a point in input order (replacing one at the same input). Returns the new list and its index. */\nexport function insertCurvePoint(points: readonly CurvePoint[], p: CurvePoint): { points: CurvePoint[]; index: number } {\n const point = { input: clampLevel(p.input), output: clampLevel(p.output) };\n const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));\n let index = out.findIndex((q) => q.input > point.input);\n if (index === -1) index = out.length;\n out.splice(index, 0, point);\n return { points: out, index };\n}\n\n/** Moves point `index`, keeping its input strictly between its neighbours'. */\nexport function moveCurvePoint(points: readonly CurvePoint[], index: number, p: CurvePoint): CurvePoint[] {\n const out = points.map((q) => ({ ...q }));\n if (index < 0 || index >= out.length) return out;\n const lo = index > 0 ? out[index - 1].input + 1 : 0;\n const hi = index < out.length - 1 ? out[index + 1].input - 1 : 255;\n out[index] = { input: Math.min(hi, Math.max(lo, clampLevel(p.input))), output: clampLevel(p.output) };\n return out;\n}\n\n/** Removes point `index`; a curve keeps at least two points. */\nexport function removeCurvePoint(points: readonly CurvePoint[], index: number): CurvePoint[] {\n if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q) => ({ ...q }));\n return points.filter((_, i) => i !== index).map((q) => ({ ...q }));\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { ImagePoint } from '../types';\nimport type { ImageEditorController } from '../controller';\nimport { ellipsePoints, normalizeRect } from './shapes';\nimport { selectionModeFromModifiers, type SelectionToolOptions } from './tool-options';\n\nexport interface GestureModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n}\n\n/**\n * Pointer gestures of the selection tools, shared by the React and Svelte image editors: feed\n * it pointer positions in natural image pixels (`canvasToImagePoint(..., false)`).\n *\n * - Magic wand: selects on `begin` (async, as it may read pixels).\n * - Rectangle / ellipse: drag a box; a click without a drag deselects (replace mode).\n * - Lasso: drag a free-hand path, closed on `end`.\n *\n * Shift adds, Alt subtracts, both intersect (overriding the options bar mode). While dragging,\n * {@link draftPath} is the outline to show (`RenderOptions.draftSelectionPath`).\n */\nexport class SelectionGesture {\n private start: ImagePoint | null = null;\n private points: ImagePoint[] = [];\n private current: ImagePoint | null = null;\n private modifiers: GestureModifiers = {};\n /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */\n public error: string | null = null;\n\n constructor(\n readonly controller: ImageEditorController,\n private readonly getOptions: () => SelectionToolOptions\n ) {}\n\n get active(): boolean {\n return this.start !== null;\n }\n\n /** The outline of the shape being drawn, or `null`. */\n get draftPath(): ImagePoint[] | null {\n if (!this.start || !this.current) return null;\n const { tool } = this.getOptions();\n if (tool === 'lasso') return this.points.length > 1 ? this.points.slice() : null;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current.x - this.start.x,\n height: this.current.y - this.start.y,\n });\n if (rect.width < 1 && rect.height < 1) return null;\n if (tool === 'ellipse') return ellipsePoints(rect);\n return [\n { x: rect.x, y: rect.y },\n { x: rect.x + rect.width, y: rect.y },\n { x: rect.x + rect.width, y: rect.y + rect.height },\n { x: rect.x, y: rect.y + rect.height },\n ];\n }\n\n /** Pointer down. Resolves once a wand click has been applied. */\n async begin(point: ImagePoint, modifiers: GestureModifiers = {}): Promise<void> {\n const options = this.getOptions();\n this.error = null;\n this.modifiers = modifiers;\n if (options.tool === 'wand') {\n this.start = null;\n try {\n await this.controller.magicWand(point, {\n tolerance: options.tolerance,\n contiguous: options.contiguous,\n antiAlias: options.antiAlias,\n distance: options.distance,\n sampleMerged: options.sampleMerged,\n sample: options.sample,\n mode: selectionModeFromModifiers(modifiers, options.mode),\n });\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n return;\n }\n this.start = point;\n this.current = point;\n this.points = [point];\n }\n\n /** Pointer move; returns true when the draft changed (re-render). */\n move(point: ImagePoint): boolean {\n if (!this.start) return false;\n this.current = point;\n if (this.getOptions().tool === 'lasso') {\n const last = this.points[this.points.length - 1];\n if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;\n this.points.push(point);\n }\n return true;\n }\n\n /** Pointer up: commits the shape. Returns true when the selection changed. */\n end(point?: ImagePoint, modifiers?: GestureModifiers): boolean {\n if (!this.start) return false;\n if (point) this.move(point);\n const options = this.getOptions();\n const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);\n const path = this.draftPath;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current!.x - this.start.x,\n height: this.current!.y - this.start.y,\n });\n this.start = null;\n this.current = null;\n this.points = [];\n if (!path) return mode === 'replace' ? this.controller.deselect() : false;\n const opts = { mode, antiAlias: options.antiAlias };\n if (options.tool === 'lasso') return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;\n return options.tool === 'ellipse' ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);\n }\n\n /** Abandons a drag without changing the selection. */\n cancel(): void {\n this.start = null;\n this.current = null;\n this.points = [];\n }\n}\n","/**\n * Region-based mosaic (pixelation) and box-blur filters for redacting or anonymizing\n * rectangular bounding boxes on an ImageData-like buffer.\n */\n\nexport interface MosaicRegion {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\t/**\n\t * When true, coordinates are interpreted as normalized values:\n\t * - [0..1] when all values are <= 1\n\t * - [0..1000] when any value is > 1 (e.g. Gemini 0..1000 bounding boxes)\n\t */\n\tnormalized?: boolean;\n}\n\nexport interface MosaicImageDataLike {\n\tdata: Uint8ClampedArray;\n\twidth: number;\n\theight: number;\n}\n\n/**\n * Resolves a MosaicRegion to clamped integer pixel bounds [x0, y0, x1, y1] within the image dimensions.\n */\nexport function resolveMosaicRegionBounds(\n\tregion: MosaicRegion,\n\timgWidth: number,\n\timgHeight: number\n): { x0: number; y0: number; x1: number; y1: number } | null {\n\tif (imgWidth <= 0 || imgHeight <= 0 || region.width <= 0 || region.height <= 0) {\n\t\treturn null;\n\t}\n\n\tlet rx = region.x;\n\tlet ry = region.y;\n\tlet rw = region.width;\n\tlet rh = region.height;\n\n\tconst isZeroToOne =\n\t\trx >= 0 &&\n\t\try >= 0 &&\n\t\trw > 0 &&\n\t\trh > 0 &&\n\t\trx <= 1 &&\n\t\try <= 1 &&\n\t\trw <= 1 &&\n\t\trh <= 1 &&\n\t\t(region.normalized === true ||\n\t\t\t(region.normalized === undefined &&\n\t\t\t\t(imgWidth > 1 || imgHeight > 1) &&\n\t\t\t\t(!Number.isInteger(rx) || !Number.isInteger(ry) || !Number.isInteger(rw) || !Number.isInteger(rh))));\n\n\tif (isZeroToOne) {\n\t\trx = rx * imgWidth;\n\t\try = ry * imgHeight;\n\t\trw = rw * imgWidth;\n\t\trh = rh * imgHeight;\n\t} else if (region.normalized === true) {\n\t\t// 0..1000 normalized coordinates\n\t\trx = (rx / 1000) * imgWidth;\n\t\try = (ry / 1000) * imgHeight;\n\t\trw = (rw / 1000) * imgWidth;\n\t\trh = (rh / 1000) * imgHeight;\n\t}\n\n\tconst x0 = Math.max(0, Math.min(imgWidth, Math.floor(rx)));\n\tconst y0 = Math.max(0, Math.min(imgHeight, Math.floor(ry)));\n\tconst x1 = Math.max(0, Math.min(imgWidth, Math.ceil(rx + rw)));\n\tconst y1 = Math.max(0, Math.min(imgHeight, Math.ceil(ry + rh)));\n\n\tif (x1 <= x0 || y1 <= y0) return null;\n\treturn { x0, y0, x1, y1 };\n}\n\n/**\n * Pixelates each rectangular region by averaging `blockSize x blockSize` blocks.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionMosaic<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tblockSize: number = 12\n): T {\n\tconst { data, width, height } = imageData;\n\tconst step = Math.max(2, Math.round(blockSize));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\n\t\tfor (let by = y0; by < y1; by += step) {\n\t\t\tconst bh = Math.min(step, y1 - by);\n\t\t\tfor (let bx = x0; bx < x1; bx += step) {\n\t\t\t\tconst bw = Math.min(step, x1 - bx);\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tconst count = bw * bh;\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\trSum += data[idx];\n\t\t\t\t\t\tgSum += data[idx + 1];\n\t\t\t\t\t\tbSum += data[idx + 2];\n\t\t\t\t\t\taSum += data[idx + 3];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst rAvg = Math.round(rSum / count);\n\t\t\t\tconst gAvg = Math.round(gSum / count);\n\t\t\t\tconst bAvg = Math.round(bSum / count);\n\t\t\t\tconst aAvg = Math.round(aSum / count);\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\tdata[idx] = rAvg;\n\t\t\t\t\t\tdata[idx + 1] = gAvg;\n\t\t\t\t\t\tdata[idx + 2] = bAvg;\n\t\t\t\t\t\tdata[idx + 3] = aAvg;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n\n/**\n * Applies a separable 2-pass box blur to each rectangular region.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionBlur<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tradius: number = 10\n): T {\n\tconst { data, width, height } = imageData;\n\tconst rad = Math.max(1, Math.min(64, Math.round(radius)));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\t\tconst rw = x1 - x0;\n\t\tconst rh = y1 - y0;\n\t\tconst temp = new Uint8ClampedArray(rw * rh * 4);\n\n\t\t// Horizontal pass into temp\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst srcY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, x - rad);\n\t\t\t\tconst kEnd = Math.min(rw - 1, x + rad);\n\t\t\t\tfor (let kx = kStart; kx <= kEnd; kx++) {\n\t\t\t\t\tconst srcIdx = (srcY * width + (x0 + kx)) * 4;\n\t\t\t\t\trSum += data[srcIdx];\n\t\t\t\t\tgSum += data[srcIdx + 1];\n\t\t\t\t\tbSum += data[srcIdx + 2];\n\t\t\t\t\taSum += data[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (y * rw + x) * 4;\n\t\t\t\ttemp[dstIdx] = Math.round(rSum / count);\n\t\t\t\ttemp[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\ttemp[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\ttemp[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\n\t\t// Vertical pass from temp back into imageData.data\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst dstY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, y - rad);\n\t\t\t\tconst kEnd = Math.min(rh - 1, y + rad);\n\t\t\t\tfor (let ky = kStart; ky <= kEnd; ky++) {\n\t\t\t\t\tconst srcIdx = (ky * rw + x) * 4;\n\t\t\t\t\trSum += temp[srcIdx];\n\t\t\t\t\tgSum += temp[srcIdx + 1];\n\t\t\t\t\tbSum += temp[srcIdx + 2];\n\t\t\t\t\taSum += temp[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (dstY * width + (x0 + x)) * 4;\n\t\t\t\tdata[dstIdx] = Math.round(rSum / count);\n\t\t\t\tdata[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\tdata[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\tdata[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n","/**\n * The layers panel's view model, shared by the React and Svelte panels: the rows to show (top of\n * the stack first, groups indented, collapsed groups hiding their children), where a drop lands,\n * and the menu of layer operations with whether each one applies to the active layer.\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport type { ImageEditorController } from '../controller';\nimport { findLayer, type ImageLayer, type SmartFilter, type SmartObjectLayer } from './model';\nimport type { LayerPlacement } from './ops';\n\nexport interface LayerPanelRow {\n layer: ImageLayer;\n /** Nesting depth (0 = top level). */\n depth: number;\n parentId: string | null;\n /** Index among its siblings, 0 = bottom. */\n index: number;\n active: boolean;\n /** Display name (adjustment layers without a name show their kind). */\n label: string;\n /** Clipped to the layer below (drawn with an indent arrow). */\n clipped: boolean;\n}\n\n/** Rows top to bottom; children of collapsed groups are left out. */\nexport function layerPanelRows(layers: readonly ImageLayer[], activeId: string | null): LayerPanelRow[] {\n const rows: LayerPanelRow[] = [];\n const walk = (list: readonly ImageLayer[], depth: number, parentId: string | null) => {\n for (let i = list.length - 1; i >= 0; i--) {\n const layer = list[i];\n rows.push({\n layer,\n depth,\n parentId,\n index: i,\n active: layer.id === activeId,\n label: layerLabel(layer),\n clipped: Boolean(layer.clipped),\n });\n if (layer.type === 'group' && !layer.collapsed) walk(layer.children, depth + 1, layer.id);\n }\n };\n walk(layers, 0, null);\n return rows;\n}\n\nexport function layerLabel(layer: ImageLayer): string {\n if (layer.name) return layer.name;\n if (layer.type === 'adjustment') return adjustmentKindLabel(layer.kind);\n return layer.type === 'group' ? 'Group' : 'Layer';\n}\n\n/** A short type badge for rows without a thumbnail. */\nexport function layerBadge(layer: ImageLayer): string {\n switch (layer.type) {\n case 'pixel':\n return 'px';\n case 'adjustment':\n return 'adj';\n case 'group':\n return 'grp';\n case 'annotation':\n return layer.annotation.type === 'text' ? 'T' : 'ann';\n case 'smart':\n return 'so';\n }\n}\n\n/** Where a row was dropped on: the top or bottom part of the row, or the middle of a group row. */\nexport type DropZone = 'above' | 'below' | 'inside';\n\n/** The zone for a pointer at `fraction` (0 top .. 1 bottom) of a row's height. */\nexport function dropZone(fraction: number, isGroup: boolean): DropZone {\n if (isGroup && fraction > 0.3 && fraction < 0.7) return 'inside';\n return fraction < 0.5 ? 'above' : 'below';\n}\n\n/** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */\nexport function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null {\n if (dragId === targetId) return null;\n const target = findLayer(layers, targetId);\n if (!target) return null;\n if (zone === 'inside' && target.layer.type === 'group') return { parentId: targetId };\n // The panel lists the top first: dropping above a row means higher in the stack.\n return zone === 'above' ? { aboveId: targetId } : { belowId: targetId };\n}\n\nexport type LayerMenuAction =\n | 'new-layer'\n | 'new-group'\n | 'duplicate'\n | 'delete'\n | 'group-selected'\n | 'ungroup'\n | 'merge-down'\n | 'merge-visible'\n | 'flatten'\n | 'via-copy'\n | 'via-cut'\n | 'add-mask'\n | 'add-mask-selection'\n | 'apply-mask'\n | 'delete-mask'\n | 'toggle-mask'\n | 'toggle-mask-link'\n | 'toggle-clip'\n | 'rasterize'\n | 'convert-smart'\n | 'edit-contents'\n | 'smart-via-copy'\n | 'add-filter-mask';\n\nexport interface LayerMenuItem {\n action: LayerMenuAction;\n label: string;\n enabled: boolean;\n}\n\n/** The layer menu for the controller's current state (active layer, selection). */\nexport function layerMenu(controller: ImageEditorController): LayerMenuItem[] {\n const s = controller.getState();\n const active = controller.getActiveLayer();\n const loc = active ? findLayer(s.layers, active.id) : null;\n const siblings = loc ? (loc.parent ? loc.parent.children : s.layers) : [];\n const below = loc ? siblings[loc.index - 1] : undefined;\n const hasSel = controller.hasSelection;\n const isPixel = active?.type === 'pixel';\n return [\n { action: 'new-layer', label: 'New Layer', enabled: true },\n { action: 'new-group', label: 'New Group', enabled: true },\n { action: 'duplicate', label: 'Duplicate Layer', enabled: Boolean(active) },\n { action: 'delete', label: 'Delete Layer', enabled: Boolean(active) },\n { action: 'group-selected', label: 'Group Layer', enabled: Boolean(active) },\n { action: 'ungroup', label: 'Ungroup', enabled: active?.type === 'group' },\n { action: 'merge-down', label: 'Merge Down', enabled: Boolean(active?.visible && below?.type === 'pixel') },\n { action: 'merge-visible', label: 'Merge Visible', enabled: s.layers.some((l) => l.visible) },\n { action: 'flatten', label: 'Flatten Image', enabled: s.layers.length > 0 },\n { action: 'via-copy', label: 'Layer via Copy', enabled: isPixel && hasSel },\n { action: 'via-cut', label: 'Layer via Cut', enabled: isPixel && hasSel },\n { action: 'add-mask', label: 'Add Layer Mask', enabled: Boolean(active) },\n { action: 'add-mask-selection', label: 'Add Mask from Selection', enabled: Boolean(active) && hasSel },\n { action: 'apply-mask', label: 'Apply Layer Mask', enabled: isPixel && Boolean(active?.mask) },\n { action: 'delete-mask', label: 'Delete Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask', label: active?.mask?.enabled === false ? 'Enable Layer Mask' : 'Disable Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask-link', label: active?.mask?.linked === false ? 'Link Layer Mask' : 'Unlink Layer Mask', enabled: Boolean(active?.mask && !active.mask.source) },\n { action: 'rasterize', label: 'Rasterize Layer', enabled: active?.type === 'annotation' || active?.type === 'smart' || (isPixel && Boolean(active?.type === 'pixel' && active.filters?.length)) },\n { action: 'convert-smart', label: 'Convert to Smart Object', enabled: Boolean(active && active.type !== 'annotation') },\n { action: 'edit-contents', label: 'Edit Contents', enabled: active?.type === 'smart' },\n { action: 'smart-via-copy', label: 'New Smart Object via Copy', enabled: active?.type === 'smart' },\n { action: 'add-filter-mask', label: 'Add Smart Filter Mask', enabled: active?.type === 'smart' && !active.smartFilters?.mask },\n { action: 'toggle-clip', label: active?.clipped ? 'Release Clipping Mask' : 'Create Clipping Mask', enabled: Boolean(loc && loc.index > 0) },\n ];\n}\n\n/** Runs a menu action on the active layer. Resolves to the error message when it was refused. */\nexport async function runLayerMenuAction(controller: ImageEditorController, action: LayerMenuAction): Promise<string | null> {\n const active = controller.getActiveLayer();\n const id = active?.id;\n try {\n switch (action) {\n case 'new-layer':\n controller.addPixelLayer();\n break;\n case 'new-group':\n controller.addGroup();\n break;\n case 'duplicate':\n if (id) controller.duplicateLayer(id);\n break;\n case 'delete':\n if (id) controller.removeLayer(id);\n break;\n case 'group-selected':\n if (id) controller.groupLayers([id]);\n break;\n case 'ungroup':\n if (id) controller.ungroupLayer(id);\n break;\n case 'merge-down':\n if (id) await controller.mergeDown(id);\n break;\n case 'merge-visible':\n await controller.mergeVisible();\n break;\n case 'flatten':\n await controller.flattenImage();\n break;\n case 'via-copy':\n case 'via-cut':\n controller.layerViaCopy({ cut: action === 'via-cut' });\n break;\n case 'add-mask':\n if (id) controller.addLayerMask(id, 'reveal-all');\n break;\n case 'add-mask-selection':\n if (id) controller.addLayerMask(id, 'selection');\n break;\n case 'apply-mask':\n if (id) await controller.applyLayerMask(id);\n break;\n case 'delete-mask':\n if (id) controller.removeLayerMask(id);\n break;\n case 'toggle-mask':\n if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });\n break;\n case 'toggle-mask-link':\n if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });\n break;\n case 'rasterize':\n if (id) await controller.rasterizeLayer(id);\n break;\n case 'convert-smart':\n if (id) controller.convertToSmartObject([id]);\n break;\n case 'edit-contents':\n if (id) controller.editSmartObjectContents(id);\n break;\n case 'smart-via-copy':\n if (id) controller.copySmartObject(id, false);\n break;\n case 'add-filter-mask':\n if (id) controller.setSmartFilterMask(id, 'reveal-all');\n break;\n case 'toggle-clip':\n if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });\n break;\n }\n return null;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n}\n\n/** One row of a smart object's filter list (Photoshop shows them under the layer, top filter first). */\nexport interface SmartFilterRow {\n filter: SmartFilter;\n /** Display name: Develop, Blur (radius), or the Photoshop filter's name. */\n label: string;\n /** Index in the stack, 0 = bottom (applied first). */\n index: number;\n /** False for Photoshop filters we keep but do not draw. */\n drawn: boolean;\n}\n\n/** The filter rows of a smart object, top filter first (empty when it has none). */\nexport function smartFilterRows(layer: ImageLayer): SmartFilterRow[] {\n if (layer.type !== 'smart') return [];\n const list = (layer as SmartObjectLayer).smartFilters?.filters ?? [];\n const rows: SmartFilterRow[] = list.map((f, index) => ({\n filter: f,\n index,\n drawn: f.kind !== 'psd',\n label: f.kind === 'psd' ? f.name.replace(/&/g, '').replace(/\\.\\.\\.$/, '') : f.kind === 'blur' ? `Blur (${f.radius} px)` : 'Develop',\n }));\n return rows.reverse();\n}\n","/**\n * The layer effects section's view model, shared by the React and Svelte panels: whether a layer\n * can take effects, its effect rows, the menu of kinds to add, and per effect a flat list of\n * controls (sliders, colours, toggles, choices) with how a change maps to `updateLayerEffect`.\n */\n\nimport { BLEND_MODES } from '../blend';\nimport {\n defaultEffectGradient,\n hasActiveEffects,\n layerEffectLabel,\n LAYER_EFFECT_KINDS,\n type EffectFill,\n type LayerEffect,\n type LayerEffectKind,\n} from '../../raster/effects/model';\nimport { builtinPattern, builtinPatternIds, type EffectPattern } from '../../raster/effects/patterns';\nimport type { ImageLayer } from './model';\n\nexport interface LayerEffectRow {\n id: string;\n kind: LayerEffectKind;\n label: string;\n enabled: boolean;\n}\n\nexport interface LayerEffectsPanelModel {\n /** Whether the layer can carry effects (adjustment layers cannot; a fully locked layer refuses changes). */\n available: boolean;\n /** Why not, when unavailable or read-only. */\n reason?: string;\n readOnly: boolean;\n enabled: boolean;\n /** Scale Effects, 1 = 100 %. */\n scale: number;\n rows: LayerEffectRow[];\n}\n\n/** The effects section for `layer`. */\nexport function layerEffectsPanel(layer: ImageLayer | null | undefined): LayerEffectsPanelModel {\n const empty = { readOnly: true, enabled: true, scale: 1, rows: [] };\n if (!layer) return { ...empty, available: false, reason: 'Select a layer to add effects.' };\n if (layer.type === 'adjustment') return { ...empty, available: false, reason: 'Adjustment layers cannot have effects.' };\n const fx = layer.effects;\n const readOnly = Boolean(layer.locks?.all);\n return {\n available: true,\n readOnly,\n reason: readOnly ? 'The layer is locked; unlock it to change its effects.' : undefined,\n enabled: fx?.enabled ?? true,\n scale: fx?.scale ?? 1,\n rows: (fx?.items ?? []).map((e) => ({ id: e.id, kind: e.kind, label: layerEffectLabel(e.kind), enabled: e.enabled })),\n };\n}\n\n/** The fx badge of a layer row: `on` (effects render), `off` (effects but switched off), or `null`. */\nexport function layerEffectsBadge(layer: ImageLayer): 'on' | 'off' | null {\n if (!layer.effects?.items.length) return null;\n return hasActiveEffects(layer.effects) ? 'on' : 'off';\n}\n\n/** The kinds the \"Add effect\" menu offers, in the Layer Style dialog's order. */\nexport const LAYER_EFFECT_MENU: ReadonlyArray<{ kind: LayerEffectKind; label: string }> = LAYER_EFFECT_KINDS.map((kind) => ({ kind, label: layerEffectLabel(kind) }));\n\nexport type EffectControl =\n | { key: string; label: string; type: 'range'; min: number; max: number; step: number; value: number; /** Shown and edited as percent (value × 100). */ percent?: boolean; unit?: string }\n | { key: string; label: string; type: 'color'; value: string }\n | { key: string; label: string; type: 'checkbox'; value: boolean }\n | { key: string; label: string; type: 'select'; value: string; options: Array<{ value: string; label: string }> };\n\nconst BLENDS = BLEND_MODES.filter((m) => m.id !== 'pigment').map((m) => ({ value: m.id as string, label: m.label }));\nconst opt = (...pairs: Array<[string, string]>) => pairs.map(([value, label]) => ({ value, label }));\n\nfunction patternOptions(patterns: readonly EffectPattern[]): Array<{ value: string; label: string }> {\n return [...patterns.map((p) => ({ value: p.id, label: p.name || p.id })), ...builtinPatternIds().map((id) => ({ value: id, label: builtinPattern(id)!.name }))];\n}\n\ntype Rec = Record<string, unknown>;\n\n/** Reads a dotted path (`fill.color`, `gradient.stops.last.color`). */\nfunction getPath(o: unknown, path: string): unknown {\n let v: unknown = o;\n for (const p of path.split('.')) {\n if (v === null || v === undefined) return undefined;\n if (Array.isArray(v)) v = p === 'last' ? v[v.length - 1] : v[Number(p)];\n else v = (v as Rec)[p];\n }\n return v;\n}\n\n/** A copy of `o` with `path` set to `value` (arrays and objects copied along the path). */\nfunction setPath(o: unknown, parts: string[], value: unknown): unknown {\n if (!parts.length) return value;\n const [p, ...rest] = parts;\n if (Array.isArray(o)) {\n const i = p === 'last' ? o.length - 1 : Number(p);\n const out = [...o];\n out[i] = setPath(o[i], rest, value);\n return out;\n }\n const r = (o ?? {}) as Rec;\n return { ...r, [p]: setPath(r[p], rest, value) };\n}\n\nfunction fillControls(f: EffectFill, allowPattern: boolean, patterns: readonly EffectPattern[]): EffectControl[] {\n const types = allowPattern ? opt(['color', 'Color'], ['gradient', 'Gradient'], ['pattern', 'Pattern']) : opt(['color', 'Color'], ['gradient', 'Gradient']);\n const out: EffectControl[] = [{ key: 'fill.type', label: 'Fill', type: 'select', value: f.type, options: types }];\n if (f.type === 'color') out.push({ key: 'fill.color', label: 'Color', type: 'color', value: f.color });\n else if (f.type === 'gradient') out.push(...gradientControls('fill.gradient', f.gradient));\n else {\n out.push(\n { key: 'fill.patternId', label: 'Pattern', type: 'select', value: f.patternId, options: patternOptions(patterns) },\n { key: 'fill.scale', label: 'Scale', type: 'range', min: 0.01, max: 10, step: 0.01, value: f.scale, percent: true, unit: '%' }\n );\n }\n return out;\n}\n\nfunction gradientControls(prefix: string, g: { stops: Array<{ color: string }>; style: string; angle: number; scale: number; reverse: boolean; align: boolean }): EffectControl[] {\n return [\n { key: `${prefix}.stops.0.color`, label: 'Start colour', type: 'color', value: g.stops[0]?.color ?? '#000000' },\n { key: `${prefix}.stops.last.color`, label: 'End colour', type: 'color', value: g.stops[g.stops.length - 1]?.color ?? '#ffffff' },\n { key: `${prefix}.style`, label: 'Style', type: 'select', value: g.style, options: opt(['linear', 'Linear'], ['radial', 'Radial'], ['angle', 'Angle'], ['reflected', 'Reflected'], ['diamond', 'Diamond']) },\n { key: `${prefix}.angle`, label: 'Angle', type: 'range', min: -180, max: 180, step: 1, value: g.angle, unit: '°' },\n { key: `${prefix}.scale`, label: 'Scale', type: 'range', min: 0.1, max: 1.5, step: 0.01, value: g.scale, percent: true, unit: '%' },\n { key: `${prefix}.reverse`, label: 'Reverse', type: 'checkbox', value: g.reverse },\n { key: `${prefix}.align`, label: 'Align with layer', type: 'checkbox', value: g.align },\n ];\n}\n\nconst px = (key: string, label: string, value: number, max = 250): EffectControl => ({ key, label, type: 'range', min: 0, max, step: 1, value, unit: 'px' });\nconst pct = (key: string, label: string, value: number, min = 0, max = 1): EffectControl => ({ key, label, type: 'range', min, max, step: 0.01, value, percent: true, unit: '%' });\nconst deg = (key: string, label: string, value: number, min = -180, max = 180): EffectControl => ({ key, label, type: 'range', min, max, step: 1, value, unit: '°' });\n\n/**\n * The controls for one effect, in the Layer Style dialog's order. `patterns` are the document's\n * patterns (the built-in ones are always offered).\n */\nexport function layerEffectControls(e: LayerEffect, patterns: readonly EffectPattern[] = []): EffectControl[] {\n const blended: EffectControl[] =\n e.kind === 'bevel-emboss'\n ? []\n : [\n { key: 'blend', label: 'Blend mode', type: 'select', value: e.blend, options: BLENDS },\n pct('opacity', 'Opacity', e.opacity),\n ];\n switch (e.kind) {\n case 'drop-shadow':\n case 'inner-shadow':\n return [\n ...blended,\n { key: 'color', label: 'Color', type: 'color', value: e.color },\n { key: 'useGlobalLight', label: 'Use global light', type: 'checkbox', value: e.useGlobalLight },\n deg('angle', 'Angle', e.angle),\n px('distance', 'Distance', e.distance, 500),\n e.kind === 'drop-shadow' ? pct('spread', 'Spread', e.spread) : pct('choke', 'Choke', e.choke),\n px('size', 'Size', e.size),\n pct('noise', 'Noise', e.noise),\n ...(e.kind === 'drop-shadow' ? [{ key: 'knocksOut', label: 'Layer knocks out shadow', type: 'checkbox', value: e.knocksOut } as EffectControl] : []),\n ];\n case 'outer-glow':\n case 'inner-glow':\n return [\n ...blended,\n ...fillControls(e.fill, false, patterns),\n { key: 'technique', label: 'Technique', type: 'select', value: e.technique, options: opt(['softer', 'Softer'], ['precise', 'Precise']) },\n ...(e.kind === 'inner-glow' ? [{ key: 'source', label: 'Source', type: 'select', value: e.source, options: opt(['edge', 'Edge'], ['center', 'Center']) } as EffectControl] : []),\n e.kind === 'outer-glow' ? pct('spread', 'Spread', e.spread) : pct('choke', 'Choke', e.choke),\n px('size', 'Size', e.size),\n pct('range', 'Range', e.range, 0.01, 1),\n pct('noise', 'Noise', e.noise),\n ];\n case 'bevel-emboss':\n return [\n { key: 'style', label: 'Style', type: 'select', value: e.style, options: opt(['inner-bevel', 'Inner Bevel'], ['outer-bevel', 'Outer Bevel'], ['emboss', 'Emboss'], ['pillow-emboss', 'Pillow Emboss'], ['stroke-emboss', 'Stroke Emboss']) },\n { key: 'technique', label: 'Technique', type: 'select', value: e.technique, options: opt(['smooth', 'Smooth'], ['chisel-hard', 'Chisel Hard'], ['chisel-soft', 'Chisel Soft']) },\n pct('depth', 'Depth', e.depth, 0, 10),\n { key: 'direction', label: 'Direction', type: 'select', value: e.direction, options: opt(['up', 'Up'], ['down', 'Down']) },\n px('size', 'Size', e.size),\n px('soften', 'Soften', e.soften, 16),\n { key: 'useGlobalLight', label: 'Use global light', type: 'checkbox', value: e.useGlobalLight },\n deg('angle', 'Angle', e.angle),\n deg('altitude', 'Altitude', e.altitude, 0, 90),\n { key: 'highlightColor', label: 'Highlight', type: 'color', value: e.highlightColor },\n { key: 'highlightBlend', label: 'Highlight mode', type: 'select', value: e.highlightBlend, options: BLENDS },\n pct('highlightOpacity', 'Highlight opacity', e.highlightOpacity),\n { key: 'shadowColor', label: 'Shadow', type: 'color', value: e.shadowColor },\n { key: 'shadowBlend', label: 'Shadow mode', type: 'select', value: e.shadowBlend, options: BLENDS },\n pct('shadowOpacity', 'Shadow opacity', e.shadowOpacity),\n ];\n case 'satin':\n return [\n ...blended,\n { key: 'color', label: 'Color', type: 'color', value: e.color },\n deg('angle', 'Angle', e.angle),\n px('distance', 'Distance', e.distance),\n px('size', 'Size', e.size),\n { key: 'invert', label: 'Invert', type: 'checkbox', value: e.invert },\n ];\n case 'color-overlay':\n return [...blended, { key: 'color', label: 'Color', type: 'color', value: e.color }];\n case 'gradient-overlay':\n return [...blended, ...gradientControls('gradient', e.gradient)];\n case 'pattern-overlay':\n return [\n ...blended,\n { key: 'patternId', label: 'Pattern', type: 'select', value: e.patternId, options: patternOptions(patterns) },\n pct('scale', 'Scale', e.scale, 0.01, 10),\n deg('angle', 'Angle', e.angle ?? 0),\n { key: 'link', label: 'Link with layer', type: 'checkbox', value: e.link ?? true },\n ];\n case 'stroke':\n return [\n ...blended,\n px('size', 'Size', e.size),\n { key: 'position', label: 'Position', type: 'select', value: e.position, options: opt(['outside', 'Outside'], ['inside', 'Inside'], ['center', 'Center']) },\n ...fillControls(e.fill, true, patterns),\n ];\n }\n}\n\n/** A new fill of `type`, keeping the colour where it can. */\nfunction fillOfType(current: EffectFill, type: string, patterns: readonly EffectPattern[]): EffectFill {\n if (type === 'gradient') return { type: 'gradient', gradient: defaultEffectGradient() };\n if (type === 'pattern') return { type: 'pattern', patternId: patterns[0]?.id ?? 'checker', scale: 1 };\n return { type: 'color', color: current.type === 'color' ? current.color : '#000000' };\n}\n\n/**\n * The `updateLayerEffect` settings for setting control `key` of `effect` to `value` (a nested\n * object such as `fill` or `gradient` is replaced whole, with the one field changed). Values are\n * in the effect's own units (fractions for percent controls).\n */\nexport function effectControlChange(effect: LayerEffect, key: string, value: unknown, patterns: readonly EffectPattern[] = []): Record<string, unknown> {\n const [top, ...rest] = key.split('.');\n if (top === 'fill' && rest[0] === 'type') return { fill: fillOfType((effect as { fill: EffectFill }).fill, String(value), patterns) };\n if (top === 'patternId' && !rest.length) {\n const name = patternOptions(patterns).find((o) => o.value === value)?.label;\n return name ? { patternId: value, name } : { patternId: value };\n }\n if (!rest.length) return { [top]: value };\n // A pattern name follows its id.\n if (top === 'fill' && rest[0] === 'patternId') {\n const name = patternOptions(patterns).find((o) => o.value === value)?.label;\n return { fill: { ...(effect as { fill: Rec }).fill, patternId: value, ...(name ? { name } : {}) } };\n }\n return { [top]: setPath(getPath(effect, top), rest, value) };\n}\n","/** Framework-free helpers behind the PSD buttons of the React and Svelte layer panels. */\n\nimport type { ImageEditorController } from '../controller';\n\n/** Types the file pickers accept. */\nexport const PSD_FILE_ACCEPT = '.psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop';\n\nfunction blobBytes(file: Blob): Promise<Uint8Array> {\n if (typeof file.arrayBuffer === 'function') return file.arrayBuffer().then((b) => new Uint8Array(b));\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));\n reader.onerror = () => reject(reader.error ?? new Error('The file could not be read.'));\n reader.readAsArrayBuffer(file);\n });\n}\n\n/** Reads a picked file and opens it as the document (one undo step). Returns the import warnings. */\nexport async function openPsdFile(controller: ImageEditorController, file: Blob): Promise<string[]> {\n return controller.loadPsd(await blobBytes(file)).warnings;\n}\n\n/** A file name for the exported document: `name` with a `.psd` / `.psb` extension. */\nexport function psdFileName(name: string, psb: boolean): string {\n const base = name.replace(/\\.[a-z0-9]{2,4}$/i, '') || 'image';\n return `${base}.${psb ? 'psb' : 'psd'}`;\n}\n\n/**\n * Writes the document as a PSD and offers it as a download (in a browser). Returns the file\n * name and the export warnings.\n */\nexport async function downloadPsd(controller: ImageEditorController, name = 'image'): Promise<{ fileName: string; warnings: string[] }> {\n const res = await controller.toPsd();\n const fileName = psdFileName(name, res.psb);\n if (typeof document !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {\n const blob = new Blob([res.bytes as BlobPart], { type: 'image/vnd.adobe.photoshop' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = fileName;\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n }\n return { fileName, warnings: res.warnings };\n}\n","/**\n * Free Transform's view model, shared by the React and Svelte editors: the overlay (outline,\n * eight handles, the reference point), what a pointer is over, Photoshop's drags and modifiers,\n * the options bar's readout and fields, and the keyboard (Enter commits, Esc cancels, arrows\n * nudge). It drives a `FreeTransformSession`; the bindings forward pointer and key events and draw\n * `overlay()` through the renderer's `freeTransform` option.\n *\n * Drags (as Photoshop CC, where a corner scales proportionally by default):\n * - corner: scale from the opposite corner (Alt: from the reference point); Shift toggles\n * proportional; side: scale one axis;\n * - Mod (Ctrl / Cmd) + corner: distort that corner; Mod + side: skew along the side (Alt: about\n * the centre); Mod + Alt + Shift + corner: perspective;\n * - outside the box: rotate about the reference point (Shift: 15° steps); inside: move (Shift:\n * along one axis); the reference point: move it;\n * - the mode switch (rotate, skew, distort, perspective) makes every handle do that;\n * - Warp: drag a mesh anchor or handle (the grid stays smooth across its lines), or anywhere\n * inside to pull the surface; presets and the grid size come from the options bar. The warp is\n * applied in the frame's own space before the corner map, so both can be used together.\n */\n\nimport type { ImageEditorController } from '../controller';\nimport type { FreeTransformInit, FreeTransformSession, FreeTransformResult } from '../free-transform';\nimport {\n cornersFromMatrix,\n describeTransform,\n matrixFromParams,\n type FreeTransformMode,\n type Quad,\n type TransformAnchor,\n type TransformReadout,\n} from '../transform';\nimport { applyHomography, invertHomography, rectToQuad, type Homography } from '../../geom/homography';\nimport {\n WARP_STYLES,\n WARP_STYLE_LABELS,\n dragWarpMeshPoint,\n identityWarpMesh,\n pullWarpMesh,\n resizeWarpMesh,\n warpMeshEditPoints,\n warpMeshGridCurves,\n warpMeshParamAt,\n warpPresetMesh,\n type WarpPreset,\n} from '../../geom/warp';\nimport type { WarpMesh, WarpStyle } from '../../geom/types';\nimport type { Vec2 } from '../../geom/types';\nimport type { Interpolation } from '../../raster/transform';\nimport type { ImagePoint } from '../types';\nimport { flattenLayers } from './model';\nimport type { ShortcutEvent } from './paint-ui';\n\n/** What a pointer is over (`mesh:<index>`: a warp mesh control point). */\nexport type TransformHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'reference' | 'inside' | 'outside' | `mesh:${number}`;\n\nconst CORNERS = ['nw', 'ne', 'se', 'sw'] as const;\nconst SIDES = ['n', 'e', 's', 'w'] as const;\nconst CORNER_INDEX: Record<string, number> = { nw: 0, ne: 1, se: 2, sw: 3 };\nconst SIDE_CORNERS: Record<string, [number, number]> = { n: [0, 1], e: [1, 2], s: [2, 3], w: [3, 0] };\n/** Box fractions of each handle. */\nconst HANDLE_FRACTION: Record<string, [number, number]> = { nw: [0, 0], n: [0.5, 0], ne: [1, 0], e: [1, 0.5], se: [1, 1], s: [0.5, 1], sw: [0, 1], w: [0, 0.5] };\nconst ANCHOR_FRACTION: Record<TransformAnchor, [number, number]> = {\n 'top-left': [0, 0],\n top: [0.5, 0],\n 'top-right': [1, 0],\n left: [0, 0.5],\n center: [0.5, 0.5],\n right: [1, 0.5],\n 'bottom-left': [0, 1],\n bottom: [0.5, 1],\n 'bottom-right': [1, 1],\n};\n\nexport interface TransformModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n /** Ctrl or Cmd. */\n ctrlKey?: boolean;\n metaKey?: boolean;\n}\n\n/** The modes of the options bar's switch (Warp comes with its own mesh UI). */\nexport const FREE_TRANSFORM_MODE_OPTIONS: readonly { id: FreeTransformMode; label: string; title: string }[] = [\n { id: 'scale', label: 'Free', title: 'Free Transform: corners scale, outside rotates, Ctrl/Cmd distorts' },\n { id: 'rotate', label: 'Rotate', title: 'Every handle rotates about the reference point' },\n { id: 'skew', label: 'Skew', title: 'Sides slide along themselves' },\n { id: 'distort', label: 'Distort', title: 'Each corner moves freely' },\n { id: 'perspective', label: 'Perspective', title: 'A corner and its partner move symmetrically' },\n { id: 'warp', label: 'Warp', title: 'Bend through a mesh: drag its points or pull inside; presets in the bar' },\n];\n\n/** The Warp style menu: Custom (the mesh as dragged) and Photoshop's fifteen presets. */\nexport const WARP_STYLE_OPTIONS: readonly { id: WarpStyle | 'custom'; label: string }[] = [\n { id: 'custom', label: 'Custom' },\n ...WARP_STYLES.map((id) => ({ id, label: WARP_STYLE_LABELS[id] })),\n];\n\n/** The Warp grid menu: patches across and down. */\nexport const WARP_GRID_OPTIONS: readonly { id: string; label: string; columns: number; rows: number }[] = [\n { id: '1x1', label: 'Default', columns: 1, rows: 1 },\n { id: '3x3', label: '3 × 3', columns: 3, rows: 3 },\n { id: '5x5', label: '5 × 5', columns: 5, rows: 5 },\n];\n\n/** Warp preset settings as the options bar shows them (percent). */\nexport interface WarpBar {\n style: WarpStyle | 'custom';\n bend: number;\n horizontal: number;\n vertical: number;\n orientation: 'horizontal' | 'vertical';\n columns: number;\n rows: number;\n}\n\n/** The options bar's numeric fields (Photoshop's X, Y, W, H, angle and skews). */\nexport const FREE_TRANSFORM_FIELDS = [\n { id: 'x', label: 'X', unit: 'px', title: 'Reference point X (document pixels)' },\n { id: 'y', label: 'Y', unit: 'px', title: 'Reference point Y (document pixels)' },\n { id: 'scaleX', label: 'W', unit: '%', title: 'Width in percent of the original' },\n { id: 'scaleY', label: 'H', unit: '%', title: 'Height in percent of the original' },\n { id: 'rotate', label: '∠', unit: '°', title: 'Rotation in degrees, clockwise' },\n { id: 'skewX', label: 'H skew', unit: '°', title: 'Horizontal skew in degrees' },\n { id: 'skewY', label: 'V skew', unit: '°', title: 'Vertical skew in degrees' },\n] as const;\n\nexport type FreeTransformField = (typeof FREE_TRANSFORM_FIELDS)[number]['id'];\n\n/** What the options bar shows. */\nexport interface FreeTransformBar extends Record<FreeTransformField, number> {\n /** False after Distort or Perspective: the numbers no longer describe the shape. */\n affine: boolean;\n}\n\nexport interface TransformOverlay {\n /** The transformed frame, corner to corner (document pixels). */\n outline: ImagePoint[];\n /** The eight handles (none in Warp mode). */\n handles: Array<{ id: 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'; x: number; y: number }>;\n reference: ImagePoint;\n /** In Warp mode: the mesh's grid lines as cubics, its anchors, and its handles with the anchor each belongs to. */\n warp?: { curves: ImagePoint[][]; anchors: ImagePoint[]; handles: Array<{ point: ImagePoint; anchor: ImagePoint }> };\n}\n\nconst CURSOR: Record<TransformHandle, string> = {\n nw: 'nwse-resize',\n se: 'nwse-resize',\n ne: 'nesw-resize',\n sw: 'nesw-resize',\n n: 'ns-resize',\n s: 'ns-resize',\n e: 'ew-resize',\n w: 'ew-resize',\n reference: 'move',\n inside: 'move',\n outside: 'alias',\n};\nconst meshIndex = (h: TransformHandle | null) => (h && h.startsWith('mesh:') ? Number(h.slice(5)) : -1);\n\nconst mod = (m: TransformModifiers) => Boolean(m.ctrlKey || m.metaKey);\nconst add = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y });\nconst sub = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y });\n\nfunction rotateAbout(p: Vec2, c: Vec2, rad: number): Vec2 {\n const cos = Math.cos(rad);\n const sin = Math.sin(rad);\n const d = sub(p, c);\n return { x: c.x + d.x * cos - d.y * sin, y: c.y + d.x * sin + d.y * cos };\n}\n\nfunction insideQuad(q: readonly Vec2[], p: Vec2): boolean {\n let sign = 0;\n for (let i = 0; i < 4; i++) {\n const a = q[i];\n const b = q[(i + 1) % 4];\n const c = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);\n if (Math.abs(c) < 1e-12) continue;\n const s = Math.sign(c);\n if (sign && s !== sign) return false;\n sign = s;\n }\n return true;\n}\n\n/** Even-odd point-in-polygon (the warped mesh's border). */\nfunction insidePolygon(poly: readonly Vec2[], p: Vec2): boolean {\n let inside = false;\n for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {\n const a = poly[i];\n const b = poly[j];\n if (a.y > p.y !== b.y > p.y && p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x) inside = !inside;\n }\n return inside;\n}\n\nexport interface FreeTransformToolOptions {\n /** Defers preview resampling to the next frame (pass `requestAnimationFrame`; default: at once). */\n schedule?: (run: () => void) => void;\n /** Called whenever the overlay, readout or error changes. */\n onChange?: () => void;\n}\n\n/**\n * The interactive Free Transform (see the module comment). `begin` opens a session on the\n * controller; pointer and key methods drive it; `commit` / `cancel` close it.\n */\nexport class FreeTransformTool {\n private sessionRef: FreeTransformSession | null = null;\n private modeValue: FreeTransformMode = 'scale';\n /** The reference point in the untransformed frame's space (follows the transform). */\n private refBox: Vec2 = { x: 0, y: 0 };\n private corners: Quad | null = null;\n private drag: { handle: TransformHandle; start: Vec2; corners: Quad; refBox: Vec2; ref: Vec2; mesh: WarpMesh | null; st: { s: number; t: number } | null } | null = null;\n /** The warp mesh in the frame's own space (null: no warp). */\n private mesh: WarpMesh | null = null;\n private warpSettings: WarpBar = { style: 'custom', bend: 50, horizontal: 0, vertical: 0, orientation: 'horizontal', columns: 1, rows: 1 };\n private pending = false;\n private viewScale = 1;\n private viewClip: { x: number; y: number; width: number; height: number } | undefined;\n /** Values typed into the options bar since the last drag (V skew is not recovered from the shape). */\n private typed: Partial<Record<FreeTransformField, number>> = {};\n /** Width and height scale together when typed (the chain icon). */\n proportional = true;\n error: string | null = null;\n lastResult: FreeTransformResult | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n private readonly options: FreeTransformToolOptions = {}\n ) {}\n\n get active(): boolean {\n return Boolean(this.sessionRef?.isOpen);\n }\n\n get session(): FreeTransformSession | null {\n return this.active ? this.sessionRef : null;\n }\n\n get mode(): FreeTransformMode {\n return this.modeValue;\n }\n\n get interpolation(): Interpolation {\n return this.sessionRef?.interpolation ?? 'bicubic';\n }\n\n private readonly listeners = new Set<() => void>();\n\n /** Calls `listener` whenever the overlay, readout, mode or error changes. Returns the unsubscribe. */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private changed(): void {\n this.options.onChange?.();\n for (const l of this.listeners) l();\n }\n\n /** Opens a Free Transform (default: the selected pixels of the active layer, or the whole layer). Returns an error message or `null`. */\n begin(init: FreeTransformInit = {}): string | null {\n if (this.active) return null;\n try {\n if (!init.layerId && init.content !== 'selection-outline' && !this.controller.getState().activeLayerId) {\n // Photoshop always has a target layer: use the topmost visible pixel layer.\n const top = [...flattenLayers(this.controller.getState().layers)].reverse().find((l) => l.type === 'pixel' && l.visible);\n if (top) this.controller.setActiveLayer(top.id);\n }\n const s = this.controller.beginFreeTransform(init);\n this.sessionRef = s;\n this.corners = s.geometry.corners;\n this.refBox = { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 };\n this.modeValue = 'scale';\n // A smart object's warp is part of its placement: keep editing it.\n this.mesh = s.geometry.warp ?? null;\n this.warpSettings = { style: 'custom', bend: 50, horizontal: 0, vertical: 0, orientation: 'horizontal', columns: 1, rows: 1 };\n this.typed = {};\n this.error = null;\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n }\n this.changed();\n return this.error;\n }\n\n setMode(mode: FreeTransformMode): void {\n this.modeValue = mode;\n const s = this.session;\n if (mode === 'warp' && s && !this.mesh) this.mesh = identityWarpMesh(s.box, this.warpSettings.columns, this.warpSettings.rows);\n this.changed();\n }\n\n /** The Warp options bar's settings. */\n warpBar(): WarpBar {\n return { ...this.warpSettings };\n }\n\n /** The warp mesh (frame space), or null. */\n get warp(): WarpMesh | null {\n return this.mesh;\n }\n\n /**\n * Applies Warp options: a preset `style` with `bend` / `horizontal` / `vertical` (percent) and\n * `orientation` replaces the mesh; `custom` keeps it; the grid size re-splits it (exactly when it\n * can). Switches to Warp mode.\n */\n setWarp(changes: Partial<WarpBar>): void {\n const s = this.session;\n if (!s) return;\n this.modeValue = 'warp';\n const next = { ...this.warpSettings, ...changes };\n this.warpSettings = next;\n let mesh = this.mesh ?? identityWarpMesh(s.box, next.columns, next.rows);\n if (next.style !== 'custom' && ('style' in changes || 'bend' in changes || 'horizontal' in changes || 'vertical' in changes || 'orientation' in changes)) {\n const preset: WarpPreset = { style: next.style, bend: next.bend, horizontal: next.horizontal, vertical: next.vertical, orientation: next.orientation };\n mesh = warpPresetMesh(preset, s.box) ?? identityWarpMesh(s.box);\n mesh = resizeWarpMesh(mesh, next.columns, next.rows);\n }\n if ('columns' in changes || 'rows' in changes) mesh = resizeWarpMesh(mesh, next.columns, next.rows);\n this.mesh = mesh;\n this.changed();\n this.preview();\n }\n\n /** Sets the reference point to an anchor of the frame (the options bar's locator). */\n setReference(anchor: TransformAnchor): void {\n const s = this.session;\n if (!s) return;\n const [fx, fy] = ANCHOR_FRACTION[anchor];\n this.refBox = { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height };\n this.changed();\n }\n\n setInterpolation(interpolation: Interpolation): void {\n const s = this.session;\n if (!s) return;\n this.run(() => s.update({ interpolation }, false));\n }\n\n /** Screen pixels per document pixel and the visible document area, for previews. */\n setView(scale: number, clip?: { x: number; y: number; width: number; height: number }): void {\n this.viewScale = scale > 0 ? scale : 1;\n this.viewClip = clip;\n }\n\n private homography() {\n const s = this.session;\n return s && this.corners ? rectToQuad(s.box, this.corners) : null;\n }\n\n /** The reference point in document pixels. */\n referencePoint(): Vec2 | null {\n const h = this.homography();\n return h ? applyHomography(h, this.refBox) : null;\n }\n\n /** The outline, handles and reference point to draw, or `null` when no transform is open. */\n overlay(): TransformOverlay | null {\n const s = this.session;\n const h = this.homography();\n if (!s || !h || !this.corners) return null;\n const handles = [...CORNERS, ...SIDES].map((id) => {\n const [fx, fy] = HANDLE_FRACTION[id];\n const p = applyHomography(h, { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height }) ?? { x: 0, y: 0 };\n return { id, x: p.x, y: p.y };\n });\n const out: TransformOverlay = { outline: this.corners.map((p) => ({ x: p.x, y: p.y })), handles, reference: this.referencePoint() ?? { x: 0, y: 0 } };\n if (this.modeValue === 'warp' && this.mesh) {\n // Control points through the corner map: exact for affine corners, close for perspective.\n const m = this.mesh;\n const at = (p: Vec2) => applyHomography(h, p) ?? p;\n const pts = m.points.map(at);\n const edit = warpMeshEditPoints(m);\n out.handles = [];\n out.outline = warpMeshGridCurves({ ...m, points: pts }).length ? this.meshOutline(pts, m) : out.outline;\n out.warp = {\n curves: warpMeshGridCurves({ ...m, points: pts }),\n anchors: edit.anchors.map((i) => pts[i]),\n handles: edit.handles.map((e) => ({ point: pts[e.index], anchor: pts[e.anchor] })),\n };\n }\n return out;\n }\n\n /** The mesh's border anchors, clockwise (for the inside test). */\n private meshOutline(pts: Vec2[], m: WarpMesh): Vec2[] {\n const nx = 3 * (m.us.length - 1) + 1;\n const ny = 3 * (m.vs.length - 1) + 1;\n const ring: Vec2[] = [];\n for (let i = 0; i < nx; i++) ring.push(pts[i]);\n for (let j = 1; j < ny; j++) ring.push(pts[j * nx + nx - 1]);\n for (let i = nx - 2; i >= 0; i--) ring.push(pts[(ny - 1) * nx + i]);\n for (let j = ny - 2; j > 0; j--) ring.push(pts[j * nx]);\n return ring;\n }\n\n /** What a document point is over (`tolerance` in document pixels: a few screen pixels / scale). */\n hit(p: ImagePoint, tolerance: number): TransformHandle | null {\n const o = this.overlay();\n if (!o) return null;\n if (this.modeValue === 'warp' && this.mesh) {\n const h = this.homography()!;\n const edit = warpMeshEditPoints(this.mesh);\n let best = -1;\n let bestD = tolerance;\n for (const i of [...edit.anchors, ...edit.handles.map((e) => e.index)]) {\n const q = applyHomography(h, this.mesh.points[i]);\n if (!q) continue;\n const d = Math.hypot(q.x - p.x, q.y - p.y);\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n }\n if (best >= 0) return `mesh:${best}`;\n return insidePolygon(o.outline, p) ? 'inside' : 'outside';\n }\n // The nearest of the handles and the reference point within the tolerance (a small frame on\n // screen puts several within reach); handles win ties.\n let best: TransformHandle | null = null;\n let bestD = tolerance;\n for (const h of o.handles) {\n const d = Math.hypot(h.x - p.x, h.y - p.y);\n if (d <= bestD) {\n best = h.id;\n bestD = d;\n }\n }\n if (Math.hypot(o.reference.x - p.x, o.reference.y - p.y) < bestD) best = 'reference';\n if (best) return best;\n return insideQuad(o.outline, p) ? 'inside' : 'outside';\n }\n\n /** The CSS cursor for a handle. */\n cursor(handle: TransformHandle | null): string {\n if (!handle) return 'default';\n if (meshIndex(handle) >= 0) return 'pointer';\n if (this.modeValue === 'warp') return handle === 'inside' ? 'grab' : 'default';\n if (this.modeValue === 'rotate' && handle !== 'inside' && handle !== 'reference') return CURSOR.outside;\n return CURSOR[handle];\n }\n\n /** Starts a drag on what `p` is over. */\n down(p: ImagePoint, tolerance: number): TransformHandle | null {\n if (!this.session || !this.corners) return null;\n const handle = this.hit(p, tolerance);\n if (!handle) return null;\n let st: { s: number; t: number } | null = null;\n if (this.modeValue === 'warp' && this.mesh && handle === 'inside') {\n const q = this.toFrame(p, this.corners);\n if (q) {\n const r = warpMeshParamAt(this.mesh, q);\n st = { s: r.s, t: r.t };\n }\n }\n this.drag = { handle, start: { x: p.x, y: p.y }, corners: this.corners, refBox: { ...this.refBox }, ref: this.referencePoint() ?? p, mesh: this.mesh, st };\n return handle;\n }\n\n /** A document point in the frame's own space under `corners`. */\n private toFrame(p: Vec2, corners: Quad): Vec2 | null {\n const s = this.session;\n const h: Homography | null = s ? rectToQuad(s.box, corners) : null;\n const inv = h && invertHomography(h);\n return inv ? applyHomography(inv, p) : null;\n }\n\n /** Continues the drag to `p` with the modifiers held now. */\n move(p: ImagePoint, m: TransformModifiers = {}): void {\n const d = this.drag;\n const s = this.session;\n if (!d || !s) return;\n if (d.handle === 'reference') {\n const h = rectToQuad(s.box, d.corners);\n const inv = h && invertHomography(h);\n const q = inv && applyHomography(inv, p);\n if (q) this.refBox = q;\n this.changed();\n return;\n }\n if (this.modeValue === 'warp' && d.mesh) {\n // Warp: mesh points and pulls in the frame's own space; outside the mesh does nothing.\n const a = this.toFrame(d.start, d.corners);\n const b = this.toFrame(p, d.corners);\n if (!a || !b) return;\n const delta = { x: b.x - a.x, y: b.y - a.y };\n const i = meshIndex(d.handle);\n if (i >= 0) this.mesh = dragWarpMeshPoint(d.mesh, i, delta);\n else if (d.handle === 'inside' && d.st) this.mesh = pullWarpMesh(d.mesh, d.st.s, d.st.t, delta);\n else return;\n this.warpSettings = { ...this.warpSettings, style: 'custom' };\n this.changed();\n this.preview();\n return;\n }\n const corners = this.dragCorners(d, p, m);\n if (corners) this.setCorners(corners);\n }\n\n /** Ends the drag (the transform stays open). */\n up(): void {\n this.drag = null;\n }\n\n private dragCorners(d: NonNullable<FreeTransformTool['drag']>, p: Vec2, m: TransformModifiers): Quad | null {\n const s = this.session!;\n const c0 = d.corners;\n let delta = sub(p, d.start);\n const map = (f: (q: Vec2, i: number) => Vec2) => c0.map(f) as unknown as Quad;\n const handle = d.handle;\n const isCorner = handle in CORNER_INDEX;\n const isSide = handle in SIDE_CORNERS;\n const rotating = handle === 'outside' || (this.modeValue === 'rotate' && (isCorner || isSide));\n if (handle === 'inside') {\n if (m.shiftKey) delta = Math.abs(delta.x) >= Math.abs(delta.y) ? { x: delta.x, y: 0 } : { x: 0, y: delta.y };\n return map((q) => add(q, delta));\n }\n if (rotating) {\n const r = d.ref;\n let a = Math.atan2(p.y - r.y, p.x - r.x) - Math.atan2(d.start.y - r.y, d.start.x - r.x);\n if (m.shiftKey) a = Math.round(a / (Math.PI / 12)) * (Math.PI / 12);\n return map((q) => rotateAbout(q, r, a));\n }\n const perspective = this.modeValue === 'perspective' || (mod(m) && m.altKey && m.shiftKey);\n const distort = this.modeValue === 'distort' || (mod(m) && !perspective);\n const skew = this.modeValue === 'skew' || (mod(m) && isSide);\n if (isSide && (skew || perspective)) {\n // Slide the side along its own direction (Alt: the opposite side the other way).\n const [i, j] = SIDE_CORNERS[handle];\n const dir = sub(c0[j], c0[i]);\n const len = Math.hypot(dir.x, dir.y) || 1;\n const t = (delta.x * dir.x + delta.y * dir.y) / len;\n const v = { x: (dir.x / len) * t, y: (dir.y / len) * t };\n const [oi, oj] = SIDE_CORNERS[{ n: 's', s: 'n', e: 'w', w: 'e' }[handle as 'n']];\n return map((q, k) => (k === i || k === j ? add(q, v) : m.altKey && (k === oi || k === oj) ? sub(q, v) : q));\n }\n if (isCorner && perspective) {\n const k = CORNER_INDEX[handle];\n if (Math.abs(delta.x) >= Math.abs(delta.y)) {\n const partner = [1, 0, 3, 2][k];\n return map((q, i) => (i === k ? { x: q.x + delta.x, y: q.y } : i === partner ? { x: q.x - delta.x, y: q.y } : q));\n }\n const partner = [3, 2, 1, 0][k];\n return map((q, i) => (i === k ? { x: q.x, y: q.y + delta.y } : i === partner ? { x: q.x, y: q.y - delta.y } : q));\n }\n if (distort || (isCorner && this.modeValue === 'skew')) {\n if (isCorner) return map((q, i) => (i === CORNER_INDEX[handle] ? add(q, delta) : q));\n const [i, j] = SIDE_CORNERS[handle];\n return map((q, k) => (k === i || k === j ? add(q, delta) : q));\n }\n // Scale in the frame's own space, so a rotated or distorted frame scales along its sides.\n const h = rectToQuad(s.box, c0);\n const inv = h && invertHomography(h);\n if (!h || !inv) return null;\n const q = applyHomography(inv, p);\n const q0 = applyHomography(inv, d.start);\n if (!q || !q0) return null;\n const b = s.box;\n const [fx, fy] = HANDLE_FRACTION[handle];\n const grab = { x: b.x + fx * b.width, y: b.y + fy * b.height };\n const anchor = m.altKey ? d.refBox : { x: b.x + (1 - fx) * b.width, y: b.y + (1 - fy) * b.height };\n const target = add(grab, sub(q, q0));\n const ax = grab.x - anchor.x;\n const ay = grab.y - anchor.y;\n let sx = isCorner || handle === 'e' || handle === 'w' ? (Math.abs(ax) > 1e-9 ? (target.x - anchor.x) / ax : 1) : 1;\n let sy = isCorner || handle === 'n' || handle === 's' ? (Math.abs(ay) > 1e-9 ? (target.y - anchor.y) / ay : 1) : 1;\n // Corners scale proportionally by default; Shift frees them.\n if (isCorner && !m.shiftKey) {\n const k = Math.abs(sx) >= Math.abs(sy) ? sx : sy;\n sx = k;\n sy = k;\n }\n const scaled = [\n { x: b.x, y: b.y },\n { x: b.x + b.width, y: b.y },\n { x: b.x + b.width, y: b.y + b.height },\n { x: b.x, y: b.y + b.height },\n ].map((c) => applyHomography(h, { x: anchor.x + (c.x - anchor.x) * sx, y: anchor.y + (c.y - anchor.y) * sy }));\n return scaled.every(Boolean) ? (scaled as unknown as Quad) : null;\n }\n\n /** Sets new corners and refreshes the preview (scheduled). Invalid shapes keep the last good one. */\n setCorners(corners: Quad): void {\n const s = this.session;\n if (!s) return;\n const h = rectToQuad(s.box, corners);\n if (!h) return;\n this.corners = corners;\n this.typed = {};\n this.changed();\n this.preview();\n }\n\n private preview(): void {\n if (this.pending) return;\n this.pending = true;\n const run = () => {\n this.pending = false;\n const s = this.session;\n if (!s || !this.corners) return;\n this.run(() => s.update({ corners: this.corners!, warp: this.mesh }, { scale: this.viewScale, clip: this.viewClip }));\n };\n if (this.options.schedule) this.options.schedule(run);\n else run();\n }\n\n private run(fn: () => void): void {\n try {\n fn();\n this.error = null;\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n if (this.sessionRef && !this.sessionRef.isOpen) this.sessionRef = null;\n }\n this.changed();\n }\n\n /** The options bar's numbers for the current shape. */\n bar(): FreeTransformBar | null {\n const s = this.session;\n if (!s || !this.corners) return null;\n const read: TransformReadout = describeTransform({ box: s.box, corners: this.corners }, this.refBox);\n return {\n affine: read.affine,\n x: this.typed.x ?? read.x,\n y: this.typed.y ?? read.y,\n scaleX: this.typed.scaleX ?? read.scaleX,\n scaleY: this.typed.scaleY ?? read.scaleY,\n rotate: this.typed.rotate ?? read.rotate,\n skewX: this.typed.skewX ?? read.skewX,\n skewY: this.typed.skewY ?? 0,\n };\n }\n\n /** Applies a typed options-bar value (W and H together while `proportional`). */\n setField(field: FreeTransformField, value: number): void {\n const s = this.session;\n const cur = this.bar();\n if (!s || !cur || !Number.isFinite(value)) return;\n const next = { ...cur, [field]: value };\n if (this.proportional && field === 'scaleX') next.scaleY = cur.scaleY === 0 ? value : (value / cur.scaleX) * cur.scaleY;\n if (this.proportional && field === 'scaleY') next.scaleX = cur.scaleX === 0 ? value : (value / cur.scaleY) * cur.scaleX;\n if (!next.scaleX || !next.scaleY) {\n this.error = 'Width and height must not be 0 %.';\n this.changed();\n return;\n }\n const m = matrixFromParams(s.box, {\n scaleX: next.scaleX,\n scaleY: next.scaleY,\n rotate: next.rotate,\n skewX: next.skewX,\n skewY: next.skewY,\n reference: this.refBox,\n dx: next.x - this.refBox.x,\n dy: next.y - this.refBox.y,\n });\n this.setCorners(cornersFromMatrix(s.box, m));\n this.typed = { scaleX: next.scaleX, scaleY: next.scaleY, rotate: next.rotate, skewX: next.skewX, skewY: next.skewY, x: next.x, y: next.y };\n this.changed();\n }\n\n /** Finishes with full-quality resampling (one undo step). */\n commit(): FreeTransformResult | null {\n const s = this.session;\n if (!s) return null;\n let res: FreeTransformResult | null = null;\n this.pending = false;\n this.run(() => {\n if (this.corners) s.update({ corners: this.corners, warp: this.mesh }, false);\n res = s.commit();\n });\n this.lastResult = res;\n this.sessionRef = null;\n this.corners = null;\n this.mesh = null;\n this.drag = null;\n this.changed();\n return res;\n }\n\n /** Abandons the transform and restores everything. */\n cancel(): void {\n this.sessionRef?.cancel();\n this.sessionRef = null;\n this.corners = null;\n this.mesh = null;\n this.drag = null;\n this.pending = false;\n this.error = null;\n this.changed();\n }\n\n /**\n * Keyboard: Enter commits, Escape cancels, arrows nudge by 1 px (Shift: 10). Ctrl/Cmd+T opens a\n * Free Transform when none is open. Returns true when the key was used (prevent its default).\n */\n key(e: ShortcutEvent): boolean {\n const t = e.target as { tagName?: string; isContentEditable?: boolean } | null;\n const typing = Boolean(t && (t.isContentEditable || t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT'));\n if (!this.active) {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === 't' && !typing) {\n this.begin();\n return true;\n }\n return false;\n }\n if (e.key === 'Enter') {\n this.commit();\n return true;\n }\n if (e.key === 'Escape') {\n this.cancel();\n return true;\n }\n if (typing) return false;\n const step = e.shiftKey ? 10 : 1;\n const nudge: Record<string, Vec2> = { ArrowLeft: { x: -step, y: 0 }, ArrowRight: { x: step, y: 0 }, ArrowUp: { x: 0, y: -step }, ArrowDown: { x: 0, y: step } };\n const v = nudge[e.key];\n if (v && this.corners) {\n this.setCorners(this.corners.map((q) => add(q, v)) as unknown as Quad);\n return true;\n }\n return false;\n }\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","../../src/image-editor/histogram.ts","../../src/image-editor/selection/overlay.ts","../../src/image-editor/layers/migrate.ts","../../src/image-editor/layers/render.ts","../../src/image-editor/renderer.ts","../../src/image-editor/selection/shapes.ts","../../src/image-editor/selection/wand.ts","../../src/image-editor/selection/refine.ts","../../src/image-editor/free-transform.ts","../../src/image-editor/controller.ts","../../src/image-editor/document.ts","../../src/image-editor/controls.ts","../../src/image-editor/selection/tool-options.ts","../../src/image-editor/selection-tools.ts","../../src/image-editor/capture.ts","../../src/image-editor/edit-tools.ts","../../src/image-editor/layer-tools.ts","../../src/image-editor/effect-tools.ts","../../src/image-editor/layers/paint-ui.ts","../../src/image-editor/paint-tools.ts","../../src/image-editor/psd-tools.ts","../../src/image-editor/transform-tools.ts","../../src/image-editor/smart-tools.ts","../../src/image-editor/tools.ts","../../src/image-editor/curve-editing.ts","../../src/image-editor/selection/gesture.ts","../../src/image-editor/mosaic-filter.ts","../../src/image-editor/layers/panel.ts","../../src/image-editor/layers/effects-ui.ts","../../src/image-editor/psd/ui.ts","../../src/image-editor/layers/transform-ui.ts"],"names":["px","b","plain","data","out","pct","WRITE_IDEMPOTENT","LAYER_ID","opt","TARGET","summary","id","spec","bestD","q","partner"],"mappings":"AAAA;AACE;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACA;AC9bO,IAAM,eAAA,EAAiB,GAAA;AAgCvB,SAAS,gBAAA,CAAiB,SAAA,EAAwC;AACvE,EAAA,MAAM,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A;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACC,MAAA;AAEf,IAAA;AACL,IAAA;AACe,IAAA;AACI,IAAA;AACrB,EAAA;AACyC,EAAA;AACN,EAAA;AACrB,EAAA;AACI,IAAA;AACC,MAAA;AACJ,MAAA;AACA,MAAA;AACb,IAAA;AACF,EAAA;AACW,EAAA;AACU,EAAA;AACD,EAAA;AACgB,EAAA;AAG3B,EAAA;AACX;AAG8B;AACT,EAAA;AACA,EAAA;AACL,EAAA;AACA,EAAA;AACK,EAAA;AACX,EAAA;AACW,EAAA;AACE,EAAA;AACD,EAAA;AACC,EAAA;AACH,EAAA;AACL,EAAA;AACE,IAAA;AACM,IAAA;AACA,IAAA;AACrB,EAAA;AACQ,EAAA;AACU,EAAA;AACA,EAAA;AACE,IAAA;AACC,IAAA;AACnB,IAAA;AACF,EAAA;AACkB,EAAA;AACE,IAAA;AACH,IAAA;AACjB,EAAA;AACY,EAAA;AACF,EAAA;AACH,EAAA;AACT;AH+auB;AACA;AIzwBO;AACrB,EAAA;AACT;AAEuB;AACP,EAAA;AAChB;AAGgB;AACP,EAAA;AACL,IAAA;AACO,IAAA;AACU,IAAA;AACA,IAAA;AACV,IAAA;AACU,IAAA;AACA,IAAA;AACnB,EAAA;AACF;AAOgB;AAME,EAAA;AACG,EAAA;AACA,EAAA;AACH,EAAA;AACN,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACK,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACG,EAAA;AACA,EAAA;AACC,EAAA;AACD,IAAA;AACA,IAAA;AACD,IAAA;AACG,MAAA;AACF,MAAA;AACA,MAAA;AACD,MAAA;AACG,MAAA;AACA,MAAA;AACA,MAAA;AACnB,IAAA;AACF,EAAA;AACmB,EAAA;AACrB;AAGgB;AACG,EAAA;AACD,IAAA;AACE,IAAA;AAClB,EAAA;AACI,EAAA;AACN;AA2BgB;AACC,EAAA;AACI,EAAA;AACF,EAAA;AACE,EAAA;AACE,EAAA;AACX,EAAA;AACG,EAAA;AACI,EAAA;AACF,IAAA;AACC,IAAA;A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A;AACE,IAAA;AACb,IAAA;AACU,IAAA;AACK,IAAA;AACH,IAAA;AACT,IAAA;AACW,IAAA;AACA,IAAA;AACT,IAAA;AACX,EAAA;AACO,EAAA;AACT;APqgEuB;AACA;AQlyEV;AACH,EAAA;AACK,EAAA;AACL,EAAA;AACC,EAAA;AACC,EAAA;AACC,EAAA;AACb;AAGa;AAE2B;AAGxB;AACP,EAAA;AACY,IAAA;AACF,IAAA;AACE,IAAA;AACC,IAAA;AACC,IAAA;AACH,IAAA;AAClB,EAAA;AACF;AAEoD;AAC/B;AACC;AACD;AACL,EAAA;AACA,EAAA;AAChB;AAGgB;AACJ,EAAA;AACU,EAAA;AACL,EAAA;AAGR,EAAA;AAKT;AAGgD;AAC9B,EAAA;AACA,EAAA;AACI,EAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AAClB,IAAA;AACF,EAAA;AACgB,EAAA;AACI,EAAA;AACF,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACH,MAAA;AACf,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAOgB;AAQA,EAAA;AACO,EAAA;AACeC,EAAAA;AACpB,IAAA;AACE,IAAA;AACT,IAAA;AACT,EAAA;AACmB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACM,EAAA;AACA,IAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AAEA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACD,IAAA;AACG,MAAA;AACA,MAAA;AACA,MAAA;AACnB,IAAA;AACQ,IAAA;AACA,IAAA;AACA,IAAA;AACS,IAAA;AACnB,EAAA;AACoB,EAAA;AACA,EAAA;AACA,EAAA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ACA,QAAA;AACF,QAAA;AACD,UAAA;AACK,UAAA;AACf,QAAA;AACa,QAAA;AACf,MAAA;AACF,IAAA;AACgB,IAAA;AAC+B,IAAA;AACpC,MAAA;AACT,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACe,MAAA;AACE,MAAA;AACD,MAAA;AACF,MAAA;AAChB,IAAA;AACe,IAAA;AACA,IAAA;AACA,IAAA;AACI,IAAA;AACZ,IAAA;AACT,EAAA;AACF;AAMS;AAC4B,EAAA;AACnB,EAAA;AACA,EAAA;AACZ,EAAA;AACS,IAAA;AACI,IAAA;AACjB,EAAA;AACO,EAAA;AACT;AAGqB;AACE,EAAA;AACL,EAAA;AACF,EAAA;AACI,IAAA;AACJ,MAAA;AACJ,MAAA;AACG,MAAA;AACG,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACG,MAAA;AACV,IAAA;AACG,IAAA;AACN,EAAA;AACmB,EAAA;AACH,EAAA;AACK,IAAA;AACF,IAAA;AACnB,EAAA;AACiB,EAAA;AACJ,EAAA;AACM,EAAA;AACrB;AAGkB;AACC,EAAA;AACe,EAAA;AAChB,EAAA;AACD,IAAA;AACG,MAAA;AACR,MAAA;AACG,MAAA;AACT,MAAA;AACD,IAAA;AACH,EAAA;AACkB,EAAA;AACE,EAAA;AACH,EAAA;AACV,EAAA;AACT;AAGS;AACM,EAAA;AACR,IAAA;AACU,IAAA;AACM,IAAA;AACH,IAAA;AACA,IAAA;AACF,IAAA;AACD,IAAA;AACf,EAAA;AACkB,EAAA;AACb,EAAA;AACa,IAAA;AACH,IAAA;AACE,IAAA;AACD,MAAA;AACD,QAAA;AACA,QAAA;AACA,QAAA;AACV,MAAA;AACI,IAAA;AACK,MAAA;AACE,MAAA;AACK,MAAA;AACnB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGuB;AACS,EAAA;AACpB,EAAA;AACW,IAAA;AACD,IAAA;AACD,IAAA;AACnB,EAAA;AACU,EAAA;AACW,IAAA;AACP,IAAA;AACK,IAAA;AACnB,EAAA;AACU,EAAA;AACW,IAAA;AACT,IAAA;AACZ,EAAA;AACqB,EAAA;AACD,IAAA;AACC,IAAA;AACrB,EAAA;AACO,EAAA;AACT;AAGS;AACa,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACH,EAAA;AACP,EAAA;AACM,EAAA;AACC,IAAA;AACD,IAAA;AACD,IAAA;AACF,IAAA;AACI,IAAA;AACjB,EAAA;AACO,EAAA;AACT;AAGS;AACS,EAAA;AACI,EAAA;AACP,IAAA;AACK,IAAA;AAClB,EAAA;AACO,EAAA;AACT;AAES;AACS,EAAA;AACC,IAAA;AACE,IAAA;AACF,IAAA;AACE,MAAA;AACT,MAAA;AACS,MAAA;AACA,MAAA;AACjB,IAAA;AACF,EAAA;AACF;AAGoB;AACS,EAAA;AACX,EAAA;AACC,IAAA;AACA,IAAA;AACJ,IAAA;AACQ,MAAA;AACR,MAAA;AACC,MAAA;AACK,IAAA;AACnB,EAAA;AACO,EAAA;AACT;AAG6BA;AACZ,EAAA;AACC,EAAA;AACF,EAAA;AACA,EAAA;AACM,EAAA;AACF,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACE,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACC,QAAA;AACA,QAAA;AACN,QAAA;AACX,MAAA;AACF,IAAA;AACF,EAAA;AACgB,EAAA;AAClB;AAES;AACoB,EAAA;AACX,EAAA;AACC,IAAA;AACJ,IAAA;AACb,EAAA;AACO,EAAA;AACT;AAG2B;AACP,EAAA;AACG,IAAA;AACP,MAAA;AACZ,IAAA;AACmB,IAAA;AACrB,EAAA;AACU,EAAA;AACQ,EAAA;AACL,EAAA;AACS,EAAA;AACG,EAAA;AACL,EAAA;AACtB;AAGoD;AAC9B,EAAA;AACtB;AAGS;AACM,EAAA;AACf;AAEuB;AACH,EAAA;AACN,EAAA;AACA,EAAA;AACQ,EAAA;AACb,EAAA;AACT;AAGS;AACa,EAAA;AACF,EAAA;AACC,EAAA;AACZ,IAAA;AACS,MAAA;AACT,IAAA;AACS,MAAA;AACT,IAAA;AACS,MAAA;AACT,IAAA;AACS,MAAA;AACT,IAAA;AACA,IAAA;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,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACQ,MAAA;AACH,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACI,IAAA;AACN,MAAA;AACK,MAAA;AACC,MAAA;AACR,MAAA;AACX,IAAA;AACF,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACQ,MAAA;AACH,MAAA;AACX,MAAA;AACF,IAAA;AACe,IAAA;AACH,IAAA;AACC,MAAA;AACF,MAAA;AACX,IAAA;AACF,EAAA;AAEO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AjBowOuB;AACA;AkBh2PR;AACC;AACVD;AACgB;AAEH;AACC;AACF;AAGL;AACI,EAAA;AACN,EAAA;AACD,IAAA;AACO,IAAA;AACD,IAAA;AACI,MAAA;AACN,MAAA;AACA,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACD,YAAA;AACC,YAAA;AACA,YAAA;AACZ,UAAA;AACW,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AACmB,IAAA;AACnB,IAAA;AACF,EAAA;AACU,EAAA;AACF,IAAA;AACO,IAAA;AACD,IAAA;AACH,MAAA;AACC,QAAA;AACI,QAAA;AACG,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACD,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACc,MAAA;AACN,QAAA;AACO,QAAA;AACN,QAAA;AACC,UAAA;AACM,UAAA;AACA,YAAA;AACC,YAAA;AACb,UAAA;AACW,UAAA;AACX,UAAA;AACF,QAAA;AACF,MAAA;AACe,MAAA;AACA,MAAA;AACA,M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AA;AACd,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACG,MAAA;AACK,QAAA;AACR,QAAA;AACJ,IAAA;AACO,IAAA;AACK,EAAA;AACL,IAAA;AACT,EAAA;AACF;AAcgB;AACK,EAAA;AACsB,EAAA;AACL,EAAA;AAC1B,IAAA;AACR,IAAA;AACkB,IAAA;AACA,IAAA;AAClB,EAAA;AACkB,EAAA;AACtB;A5BwnTuB;AACA;A6Bn1TP;AACE,EAAA;AACK,EAAA;AACF,EAAA;AACF,EAAA;AACA,EAAA;AACV,EAAA;AACM,IAAA;AACX,IAAA;AACmB,IAAA;AACN,IAAA;AACF,IAAA;AACA,IAAA;AACb,EAAA;AACF;AAGgB;AACM,EAAA;AACb,EAAA;AACT;AAGa;AAQE;AACC;AAEP;AACa,EAAA;AACtB;AAK6B;AACV,EAAA;AACI,EAAA;AACD,IAAA;AACC,IAAA;AACE,IAAA;AACvB,EAAA;AACO,EAAA;AACT;AAG6B;AACR,EAAA;AACE,EAAA;AACF,EAAA;AACD,IAAA;AACC,IAAA;AACE,IAAA;AACZ,IAAA;AACT,EAAA;AACiB,EAAA;AACG,EAAA;AACtB;AAEsB;AACN,EAAA;AACuB,EAAA;AACtB,EAAA;AACK,EAAA;AACf,EAAA;AACC,IAAA;AACK,MAAA;AACA,MAAA;AACT,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAES;AACA,EAAA;AACW,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAClB,EAAA;AACF;AAEwC;AACC;AACA;AAMzB;AAEV,EAAA;AAGoB,IAAA;AACD,IAAA;AACjB,EAAA;AACU,EAAA;AACT,IAAA;AACA,IAAA;AACI,MAAA;AACF,QAAA;AACI,QAAA;AACA,QAAA;AACM,QAAA;AACE,QAAA;AACJ,QAAA;AACA,QAAA;AACE,QAAA;AACE,QAAA;AACjB,MAAA;AACG,IAAA;AACA,IAAA;AACI,M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,EAAA;AACpB,IAAA;AACA,IAAA;AACuB,IAAA;AACvB,IAAA;AACA,IAAA;AACK,MAAA;AACH,MAAA;AAChB,IAAA;AACiB,IAAA;AACA,IAAA;AACH,IAAA;AACH,IAAA;AACA,IAAA;AACC,IAAA;AACN,IAAA;AACT,EAAA;AAAA;AAGe,EAAA;AACI,oBAAA;AACC,IAAA;AACH,IAAA;AACH,IAAA;AACA,IAAA;AACG,IAAA;AACF,IAAA;AACA,IAAA;AACf,EAAA;AAAA;AAAA;AAAA;AAAA;AAM+B,EAAA;AACjB,IAAA;AACG,IAAA;AACG,IAAA;AACT,MAAA;AACM,QAAA;AACJ,QAAA;AACT,MAAA;AACO,MAAA;AACT,IAAA;AACc,IAAA;AACA,MAAA;AACL,MAAA;AACT,IAAA;AACc,IAAA;AACA,MAAA;AACL,MAAA;AACT,IAAA;AACmB,IAAA;AACJ,IAAA;AACuB,IAAA;AACpB,IAAA;AACJ,IAAA;AACI,MAAA;AACT,MAAA;AACT,IAAA;AACO,IAAA;AACT,EAAA;AACF;A/By6TuB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","sourcesContent":[null,"// Portions ported from photocraft-main/crates/algo/src/histogram.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Display bins of a histogram. */\nexport const HISTOGRAM_BINS = 256;\n\nexport interface ImageHistogram {\n /** 256-bin counts per channel. */\n red: Uint32Array;\n green: Uint32Array;\n blue: Uint32Array;\n /** 256-bin counts of Rec.709 luma `round(0.2126 R + 0.7152 G + 0.0722 B)` of the encoded values. */\n luminance: Uint32Array;\n /** Visible pixels counted (alpha > 0). Each counts once, partially transparent ones included. */\n samples: number;\n /** Fully transparent pixels, excluded from every bin. */\n transparent: number;\n /** Per channel (R, G, B): visible pixels at exactly 0. */\n shadows: [number, number, number];\n /** Per channel (R, G, B): visible pixels at exactly 255. */\n highlights: [number, number, number];\n /** Largest bin count over R, G, B and luminance (for scaling a plot). */\n max: number;\n}\n\ninterface PixelSource {\n data: ArrayLike<number>;\n width?: number;\n height?: number;\n}\n\n/**\n * Per-channel and luminance histograms of straight 8-bit RGBA pixels (an `ImageData` or anything\n * with `data`), with clipping counts at the exact endpoints. Transparent pixels cannot add\n * phantom peaks: they are counted separately and excluded.\n */\nexport function computeHistogram(imageData: PixelSource): ImageHistogram {\n const red = new Uint32Array(HISTOGRAM_BINS);\n const green = new Uint32Array(HISTOGRAM_BINS);\n const blue = new Uint32Array(HISTOGRAM_BINS);\n const luminance = new Uint32Array(HISTOGRAM_BINS);\n const shadows: [number, number, number] = [0, 0, 0];\n const highlights: [number, number, number] = [0, 0, 0];\n let samples = 0;\n let transparent = 0;\n const data = imageData.data;\n const len = data.length - (data.length % 4);\n for (let i = 0; i < len; i += 4) {\n if (!(data[i + 3] > 0)) {\n transparent++;\n continue;\n }\n samples++;\n const r = clampByte(data[i]);\n const g = clampByte(data[i + 1]);\n const b = clampByte(data[i + 2]);\n red[r]++;\n green[g]++;\n blue[b]++;\n luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) shadows[0]++;\n if (g === 0) shadows[1]++;\n if (b === 0) shadows[2]++;\n if (r === 255) highlights[0]++;\n if (g === 255) highlights[1]++;\n if (b === 255) highlights[2]++;\n }\n let max = 0;\n for (const bins of [red, green, blue, luminance]) {\n for (let k = 0; k < HISTOGRAM_BINS; k++) if (bins[k] > max) max = bins[k];\n }\n return { red, green, blue, luminance, samples, transparent, shadows, highlights, max };\n}\n\nfunction clampByte(v: number): number {\n return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);\n}\n\nexport type HistogramChannel = 'red' | 'green' | 'blue' | 'luminance';\n\n/**\n * An SVG path (`M … Z`) of one channel scaled into a `width` × `height` box, bins left to right,\n * counts up from the bottom. `scale: 'sqrt'` (default) keeps small populations visible.\n */\nexport function histogramPath(\n histogram: ImageHistogram,\n channel: HistogramChannel,\n width: number,\n height: number,\n options: { scale?: 'linear' | 'sqrt'; max?: number } = {}\n): string {\n const bins = histogram[channel];\n const sqrt = (options.scale ?? 'sqrt') === 'sqrt';\n const f = (v: number) => (sqrt ? Math.sqrt(v) : v);\n const top = f(options.max ?? histogram.max) || 1;\n const dx = width / (HISTOGRAM_BINS - 1);\n let d = `M0 ${height}`;\n for (let k = 0; k < HISTOGRAM_BINS; k++) {\n const y = height - (f(bins[k]) / top) * height;\n d += ` L${round2(k * dx)} ${round2(y)}`;\n }\n return `${d} L${round2(width)} ${height} Z`;\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { SelectionMask } from './mask';\n\n/**\n * The selection's outline at `threshold` (0..255, default 128) as axis-aligned segments on pixel\n * boundaries, `[x0, y0, x1, y1, …]` in natural image pixels, with collinear neighbours merged.\n * This is what marching ants are drawn from. Linear in the pixel count.\n */\nexport function selectionOutline(mask: SelectionMask, threshold = 128): Float32Array {\n const { width: w, height: h, data } = mask;\n const segs: number[] = [];\n if (!mask.bounds) return new Float32Array(0);\n const b = mask.bounds;\n const inside = (x: number, y: number) => x >= 0 && y >= 0 && x < w && y < h && data[y * w + x] >= threshold;\n // Horizontal boundaries: the line y separates rows y − 1 and y.\n for (let y = b.y; y <= b.y + b.height; y++) {\n let start = -1;\n for (let x = b.x; x <= b.x + b.width; x++) {\n const edge = x < b.x + b.width && inside(x, y - 1) !== inside(x, y);\n if (edge && start < 0) start = x;\n else if (!edge && start >= 0) {\n segs.push(start, y, x, y);\n start = -1;\n }\n }\n }\n // Vertical boundaries: the line x separates columns x − 1 and x.\n for (let x = b.x; x <= b.x + b.width; x++) {\n let start = -1;\n for (let y = b.y; y <= b.y + b.height; y++) {\n const edge = y < b.y + b.height && inside(x - 1, y) !== inside(x, y);\n if (edge && start < 0) start = y;\n else if (!edge && start >= 0) {\n segs.push(x, start, x, y);\n start = -1;\n }\n }\n }\n return Float32Array.from(segs);\n}\n\nexport interface QuickMaskStyle {\n /** Overlay colour, `[r, g, b]` 0..255 (default Photoshop's rubylith red). */\n color?: readonly [number, number, number];\n /** 0..1 (default 0.5): opacity over fully unselected pixels. */\n opacity?: number;\n}\n\n/**\n * Quick Mask view: RGBA pixels (natural image size) tinting the *unselected* area, so the\n * selection shows through clear, as Photoshop's rubylith overlay.\n */\nexport function quickMaskPixels(mask: SelectionMask, style: QuickMaskStyle = {}): Uint8ClampedArray {\n const [r, g, b] = style.color ?? [255, 0, 0];\n const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n out[p] = r;\n out[p + 1] = g;\n out[p + 2] = b;\n out[p + 3] = Math.round((255 - mask.data[i]) * k);\n }\n return out;\n}\n","/**\n * Between the old document-level model (schema v1 / v2: base sliders, adjustment layers, blur,\n * depth mask and annotations) and the layer stack:\n *\n * - `layersFromLegacy` lifts a v1 / v2 state into layers: the source becomes the Background\n * pixel layer carrying the base sliders as its `develop` filter and the depth mask as its\n * parametric mask; adjustment layers go above it, annotations on top in their order. Blur\n * becomes a `blur` filter on the Background, or `legacyOutputBlur` when adjustment layers\n * exist (blur used to run after them).\n * - `legacyView` reads the stack back as `adjustments` / `depthMask` / `annotations` (the\n * compatibility views on the editor state) and says whether the stack still has exactly the\n * legacy shape, in which case it serializes as v1 / v2 byte for byte.\n */\n\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport { normalizeAdjustmentLayers } from '../adjustment-layers';\nimport { normalizeProcessVersion } from '../color';\nimport { decodeLayerMask, normalizeLayerMask } from '../selection/layer-mask';\nimport { encodeSelectionRle, resizeSelection } from '../selection/mask';\nimport type {\n AdjustmentLayer,\n AdjustmentLayerMask,\n Annotation,\n DepthMaskOptions,\n ImageAdjustments,\n} from '../types';\nimport {\n blurFilter,\n developFilter,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type ImageLayer,\n type LayerFilter,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { emptySurface, readRegion, surfaceFromPixels, type MaskSurface } from '../../raster/surface';\n\nexport const BACKGROUND_LAYER_ID = 'background';\nexport const BACKGROUND_LAYER_NAME = 'Background';\nexport const DEVELOP_FILTER_ID = 'develop';\nexport const BLUR_FILTER_ID = 'blur';\n\n/** The old document-level parts of the editor state. */\nexport interface LegacyParts {\n adjustments: ImageAdjustments;\n depthMask: DepthMaskOptions;\n annotations: Annotation[];\n}\n\n/** The stack as stored at document level next to the layers. */\nexport interface LayerStack {\n layers: ImageLayer[];\n /** Export alpha (the old `adjustments.opacity`), 0..1. */\n outputOpacity: number;\n /** Migrated blur that must run after the adjustment layers (never created by new edits). */\n legacyOutputBlur?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Masks\n// ---------------------------------------------------------------------------\n\n/**\n * Stored `r1:` encodings of masks lifted from v1 / v2, by surface, so an unchanged mask writes\n * back byte for byte (and a mask of another size than the document keeps its size).\n */\nconst legacyMaskMemo = new WeakMap<MaskSurface, { width: number; height: number; rle: string }>();\n\n/** A raster mask from an old adjustment-layer mask, resampled to the document if needed. */\nexport function rasterMaskFromLegacy(mask: AdjustmentLayerMask, width: number, height: number): RasterMask | null {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const surface = surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);\n legacyMaskMemo.set(surface, { width: mask.width, height: mask.height, rle: mask.rle });\n const out: RasterMask = { surface, defaultValue: 0 };\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n return out;\n}\n\n/**\n * True when an adjustment layer's mask came from a v1 / v2 mask of another size than the canvas\n * (the old renderer resampled it per render; the stack holds a canvas-sized copy).\n */\nexport function hasResampledLegacyMask(layers: readonly ImageLayer[], width: number, height: number): boolean {\n for (const l of layers) {\n if (l.type !== 'adjustment' || !l.mask) continue;\n const memo = legacyMaskMemo.get(l.mask.surface);\n if (memo && (memo.width !== width || memo.height !== height)) return true;\n }\n return false;\n}\n\n/** The old mask shape of a raster mask (its pixels over the canvas, run-length encoded). */\nexport function legacyMaskOf(mask: RasterMask, width: number, height: number): AdjustmentLayerMask {\n let memo = legacyMaskMemo.get(mask.surface);\n if (!memo) {\n const data = readRegion(mask.surface, { x: 0, y: 0, width, height });\n memo = { width, height, rle: encodeSelectionRle(new Uint8Array(data.buffer, data.byteOffset, data.length)) };\n legacyMaskMemo.set(mask.surface, memo);\n }\n return normalizeLayerMask({ ...memo, density: mask.density, feather: mask.feather, enabled: mask.enabled })!;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy → layers\n// ---------------------------------------------------------------------------\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/** True when the depth mask settings are the defaults (no mask at all). */\nexport function isDefaultDepthMask(mask: DepthMaskOptions | undefined): boolean {\n if (!mask) return true;\n return sameJson({ ...DEFAULT_DEPTH_MASK_VALUES, ...mask, depthRange: [...(mask.depthRange ?? [])] }, DEFAULT_DEPTH_MASK_VALUES);\n}\n\nexport function developFromAdjustments(adjustments: ImageAdjustments): DevelopFilter {\n return {\n id: DEVELOP_FILTER_ID,\n kind: 'develop',\n enabled: true,\n brightness: adjustments.brightness ?? 0,\n contrast: adjustments.contrast ?? 0,\n saturation: adjustments.saturation ?? 0,\n exposure: adjustments.exposure ?? 0,\n temperature: adjustments.temperature ?? 0,\n tint: adjustments.tint ?? 0,\n process: normalizeProcessVersion(adjustments.process),\n };\n}\n\n/** The default name of an annotation layer. */\nexport function annotationLayerName(annotation: Annotation): string {\n switch (annotation.type) {\n case 'pen':\n return 'Pen';\n case 'rect':\n return 'Rectangle';\n case 'circle':\n return 'Ellipse';\n case 'arrow':\n return 'Arrow';\n case 'line':\n return 'Line';\n case 'text':\n return annotation.text ? annotation.text.slice(0, 32) : 'Text';\n case 'image':\n return 'Image';\n }\n}\n\nexport function annotationLayerFor(annotation: Annotation): AnnotationLayer {\n return {\n type: 'annotation',\n id: annotation.id,\n name: annotationLayerName(annotation),\n visible: true,\n opacity: 1,\n blend: 'normal',\n annotation,\n };\n}\n\nexport function adjustmentNodeFromLegacy(layer: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const node = {\n type: 'adjustment',\n id: layer.id,\n name: layer.name ?? '',\n visible: layer.enabled !== false,\n opacity: layer.opacity ?? 1,\n blend: layer.blend ?? 'normal',\n kind: layer.kind,\n params: JSON.parse(JSON.stringify(layer.params)),\n } as AdjustmentLayerNode;\n if (layer.mask) {\n const mask = rasterMaskFromLegacy(layer.mask, width, height);\n if (mask) node.mask = mask;\n }\n return node;\n}\n\n/** The Background layer of an opened image (its pixels are the document source). */\nexport function backgroundLayer(filters: LayerFilter[] = [], mask?: RasterMask): PixelLayer {\n const layer: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: emptySurface(4),\n offset: { x: 0, y: 0 },\n source: 'document',\n filters,\n };\n if (mask) layer.mask = mask;\n return layer;\n}\n\n/** A parametric depth mask on a layer. */\nexport function depthRasterMask(source: DepthMaskOptions): RasterMask {\n const out: RasterMask = {\n surface: emptySurface(1, 255),\n defaultValue: 255,\n source: { ...source, depthRange: [...source.depthRange] as [number, number] },\n };\n if (!source.enabled) out.enabled = false;\n return out;\n}\n\n/** Lifts a v1 / v2 state into a layer stack (see the module comment). */\nexport function layersFromLegacy(parts: Partial<LegacyParts>, width: number, height: number): LayerStack {\n // As `normalizeStoredAdjustments`: stored values without `process` keep the legacy rendering.\n const stored = parts.adjustments;\n const adjustments = (\n stored ? { ...DEFAULT_ADJUSTMENTS_VALUES, ...stored, process: normalizeProcessVersion(stored.process) } : { ...DEFAULT_ADJUSTMENTS_VALUES }\n ) as ImageAdjustments;\n const adjLayers = normalizeAdjustmentLayers(adjustments.layers);\n const filters: LayerFilter[] = [developFromAdjustments(adjustments)];\n const blur = typeof adjustments.blur === 'number' && adjustments.blur > 0 ? adjustments.blur : 0;\n let legacyOutputBlur: number | undefined;\n if (blur > 0) {\n if (adjLayers.length) legacyOutputBlur = blur;\n else filters.push({ id: BLUR_FILTER_ID, kind: 'blur', enabled: true, radius: blur });\n }\n const depth = parts.depthMask;\n const mask = depth && !isDefaultDepthMask(depth) ? depthRasterMask({ ...DEFAULT_DEPTH_MASK_VALUES, ...depth }) : undefined;\n const layers: ImageLayer[] = [backgroundLayer(filters, mask)];\n for (const l of adjLayers) layers.push(adjustmentNodeFromLegacy(l, width, height));\n for (const a of parts.annotations ?? []) layers.push(annotationLayerFor(JSON.parse(JSON.stringify(a))));\n const out: LayerStack = { layers, outputOpacity: typeof adjustments.opacity === 'number' ? adjustments.opacity : 1 };\n if (legacyOutputBlur !== undefined) out.legacyOutputBlur = legacyOutputBlur;\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Layers → legacy views\n// ---------------------------------------------------------------------------\n\n/** The old `AdjustmentLayer` shape of a node, keys in the order `normalizeAdjustmentLayer` writes. */\nexport function legacyAdjustmentLayer(node: AdjustmentLayerNode, width: number, height: number): AdjustmentLayer {\n const out: Record<string, unknown> = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n };\n if (node.name) out.name = node.name;\n if (node.mask && !node.mask.source) out.mask = legacyMaskOf(node.mask, width, height);\n return out as unknown as AdjustmentLayer;\n}\n\n/** The pixel layer whose develop filter the legacy API edits: the active one, else the Background. */\nexport function developTarget(layers: readonly ImageLayer[], activeLayerId: string | null): PixelLayer | null {\n const pixels: PixelLayer[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'pixel') pixels.push(l);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (activeLayerId) {\n const active = pixels.find((l) => l.id === activeLayerId);\n if (active) return active;\n }\n return pixels.find((l) => l.source === 'document') ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;\n}\n\nexport interface LegacyViewResult extends LegacyParts {\n /**\n * True when the stack has exactly the shape a v1 / v2 document lifts to (an unpainted,\n * unmoved Background with develop / blur filters and an optional depth mask, then adjustment\n * layers, then plain annotation layers), so it can be written as v1 / v2.\n */\n exact: boolean;\n}\n\nconst isPlain = (l: ImageLayer) =>\n l.visible && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask && !l.locks && !l.effects;\n\n/** Reads the stack back as the old document-level parts (see `LegacyViewResult`). */\nexport function legacyView(stack: LayerStack, width: number, height: number, activeLayerId: string | null = null): LegacyViewResult {\n const { layers } = stack;\n const target = developTarget(layers, activeLayerId);\n const develop = target ? developFilter(target) : undefined;\n const blur = stack.legacyOutputBlur ?? (target ? blurFilter(target)?.radius : undefined) ?? 0;\n const adjustments: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS_VALUES,\n ...(develop\n ? {\n brightness: develop.brightness,\n contrast: develop.contrast,\n saturation: develop.saturation,\n exposure: develop.exposure,\n temperature: develop.temperature,\n tint: develop.tint ?? 0,\n }\n : {}),\n blur,\n opacity: stack.outputOpacity,\n process: develop ? normalizeProcessVersion(develop.process) : DEFAULT_ADJUSTMENTS_VALUES.process,\n };\n const adjNodes: AdjustmentLayerNode[] = [];\n const annotations: Annotation[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'adjustment') adjNodes.push(l);\n else if (l.type === 'annotation') annotations.push(l.annotation);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (adjNodes.length) adjustments.layers = adjNodes.map((n) => legacyAdjustmentLayer(n, width, height));\n const depthSource = target?.mask?.source;\n const depthMask: DepthMaskOptions = depthSource\n ? { ...depthSource, depthRange: [...depthSource.depthRange] as [number, number] }\n : { ...DEFAULT_DEPTH_MASK_VALUES, depthRange: [...DEFAULT_DEPTH_MASK_VALUES.depthRange] as [number, number] };\n return { adjustments, depthMask, annotations, exact: isLegacyShape(stack) };\n}\n\n/** See `LegacyViewResult.exact`. */\nexport function isLegacyShape(stack: LayerStack): boolean {\n const { layers } = stack;\n const bg = layers[0];\n if (!bg || bg.type !== 'pixel' || bg.source !== 'document') return false;\n if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;\n if (!bg.visible || bg.opacity !== 1 || bg.blend !== 'normal' || bg.clipped || bg.locks || bg.effects) return false;\n if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;\n const filters = bg.filters ?? [];\n if (filters.length < 1 || filters.length > 2) return false;\n const [develop, blur] = filters;\n if (develop.kind !== 'develop' || !develop.enabled || develop.id !== DEVELOP_FILTER_ID) return false;\n if (blur && (blur.kind !== 'blur' || !blur.enabled || blur.id !== BLUR_FILTER_ID || !(blur.radius > 0))) return false;\n if (bg.mask) {\n const m = bg.mask;\n if (!m.source || m.density !== undefined || m.feather !== undefined) return false;\n if ((m.enabled !== false) !== m.source.enabled) return false;\n }\n let i = 1;\n let adjustments = 0;\n for (; i < layers.length && layers[i].type === 'adjustment'; i++) {\n const l = layers[i] as AdjustmentLayerNode;\n if (l.clipped || l.locks || l.mask?.source || l.effects) return false;\n adjustments++;\n }\n for (; i < layers.length; i++) {\n const l = layers[i];\n if (l.type !== 'annotation' || !isPlain(l) || l.id !== l.annotation.id || l.name !== annotationLayerName(l.annotation)) return false;\n }\n if (blur && adjustments) return false;\n if (stack.legacyOutputBlur !== undefined && (!adjustments || !(stack.legacyOutputBlur > 0))) return false;\n return true;\n}\n","/**\n * Pure helpers between the compositor and the renderer: the output stage (projection to the\n * cropped, rotated frame, output opacity and the migrated blur), the routing rule that keeps\n * old-shape documents on the exact legacy path where document-space compositing cannot match it,\n * and per-tile histograms. No DOM.\n */\n\nimport { applyBoxBlur } from '../filters';\nimport { normalizeAngle, unprojectCanvasPoint, getRotatedSize, type ImageProjection } from '../geometry';\nimport { HISTOGRAM_BINS, type ImageHistogram } from '../histogram';\nimport type { CropBox, ImageTransform } from '../types';\nimport { blurFilter, type AnnotationLayer, type ImageLayer, type PixelLayer } from './model';\nimport { hasResampledLegacyMask, isLegacyShape, type LayerStack } from './migrate';\nimport { LayerCompositor, type CompositorSources } from './compositor';\n\n/** True for 0, 90, 180 and 270 degrees. */\nexport function isQuarterTurn(rotate: number): boolean {\n return normalizeAngle(rotate) % 90 === 0;\n}\n\nfunction isIntegerRect(r: CropBox): boolean {\n return Number.isInteger(r.x) && Number.isInteger(r.y) && Number.isInteger(r.width) && Number.isInteger(r.height);\n}\n\n/** The projection that draws `region` into a `baseW × baseH` buffer at scale 1 (the renderer's base canvas). */\nexport function baseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/**\n * Projects `src` (RGBA covering `srcRect` in document space) into the base frame of `region`\n * under a quarter-turn `transform`: an exact pixel permutation (crop, flips, 90° steps), the\n * same mapping the canvas draws with. Returns the base buffer and its size.\n */\nexport function projectQuarterTurn(\n src: ArrayLike<number>,\n srcRect: CropBox,\n region: CropBox,\n transform: ImageTransform\n): { width: number; height: number; data: Uint8ClampedArray } {\n const rotated = getRotatedSize(region.width, region.height, transform);\n const w = Math.max(1, Math.round(rotated.width));\n const h = Math.max(1, Math.round(rotated.height));\n const out = new Uint8ClampedArray(w * h * 4);\n const p = baseProjection(region, transform, w, h);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, p);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, p);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, p);\n const ux = Math.round(ox.x - o.x);\n const vx = Math.round(ox.y - o.y);\n const uy = Math.round(oy.x - o.x);\n const vy = Math.round(oy.y - o.y);\n const u0 = Math.floor(o.x) - srcRect.x;\n const v0 = Math.floor(o.y) - srcRect.y;\n const sw = srcRect.width;\n const sh = srcRect.height;\n for (let y = 0; y < h; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n for (let x = 0; x < w; x++, u += ux, v += vx) {\n if (u < 0 || v < 0 || u >= sw || v >= sh) continue;\n const s = (v * sw + u) * 4;\n const d = (y * w + x) * 4;\n out[d] = src[s];\n out[d + 1] = src[s + 1];\n out[d + 2] = src[s + 2];\n out[d + 3] = src[s + 3];\n }\n }\n return { width: w, height: h, data: out };\n}\n\n/** Output opacity as the old base adjustments applied it (`clamp(a · opacity)`), then the migrated blur. */\nexport function applyOutputStage(img: { width: number; height: number; data: Uint8ClampedArray }, opacity: number, legacyOutputBlur?: number): void {\n if (opacity < 1) {\n const d = img.data;\n for (let i = 3; i < d.length; i += 4) d[i] = d[i] * opacity;\n }\n if (legacyOutputBlur && legacyOutputBlur > 0) applyBoxBlur(img, Math.min(Math.round(legacyOutputBlur), 40));\n}\n\nexport interface RouteInput {\n stack: LayerStack;\n region: CropBox;\n canvas: { width: number; height: number };\n transform: ImageTransform;\n /** Whether the source image is fully opaque. */\n sourceOpaque: boolean;\n}\n\n/**\n * Whether a document must render through the legacy base path to look exactly as before. Only\n * documents that still have the v1 / v2 shape qualify, and only where compositing in document\n * space cannot reproduce the old order of operations:\n *\n * - a rotation that is not a quarter turn, or a crop with fractional coordinates (the old path\n * resampled before adjusting);\n * - an enabled depth mask (applied after the blur and resampled with the canvas);\n * - output opacity below 1 (it rounded alpha before the depth mask);\n * - a Background blur filter with a crop or a 90° / 270° turn (the old blur clamped at the crop\n * edge and ran on the rotated buffer);\n * - a migrated output blur over a source with transparency (canvas premultiplication would round\n * colours before blurring);\n * - an adjustment-layer mask stored at another size than the canvas (it was resampled through\n * the projection on every render).\n */\nexport function needsLegacyBase(input: RouteInput): boolean {\n const { stack, region, canvas, transform } = input;\n if (!isLegacyShape(stack)) return false;\n const bg = stack.layers[0] as PixelLayer;\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) return true;\n if (bg.mask?.source?.enabled && bg.mask.source.maskSource) return true;\n if (stack.outputOpacity < 1) return true;\n const blur = blurFilter(bg);\n if (blur && blur.enabled && blur.radius > 0) {\n const full = region.x === 0 && region.y === 0 && region.width === canvas.width && region.height === canvas.height;\n const angle = normalizeAngle(transform.rotate);\n if (!full || angle === 90 || angle === 270) return true;\n }\n if ((stack.legacyOutputBlur ?? 0) > 0 && !input.sourceOpaque) return true;\n if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;\n return false;\n}\n\nconst isPlainAnnotation = (l: ImageLayer): l is AnnotationLayer =>\n l.type === 'annotation' && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask;\n\n/**\n * Splits the stack into what the compositor rasterizes and the annotation layers at the top that\n * draw as crisp vectors over the result (as annotations always did): the trailing run of plain\n * top-level annotation layers (hidden ones are skipped).\n */\nexport function splitOverlayAnnotations(layers: readonly ImageLayer[]): { composite: ImageLayer[]; overlay: AnnotationLayer[] } {\n let i = layers.length;\n while (i > 0) {\n const l = layers[i - 1];\n if (l.type === 'annotation' && (!l.visible || isPlainAnnotation(l))) i--;\n else break;\n }\n return {\n composite: layers.slice(0, i) as ImageLayer[],\n overlay: (layers.slice(i) as AnnotationLayer[]).filter((l) => l.visible),\n };\n}\n\n/** The integer rectangle covering `region`. */\nexport function coveringRect(region: CropBox): CropBox {\n const x = Math.floor(region.x);\n const y = Math.floor(region.y);\n return { x, y, width: Math.ceil(region.x + region.width) - x, height: Math.ceil(region.y + region.height) - y };\n}\n\n/**\n * The base image of a layered document without a canvas: the stack composited over `region`\n * (integer), projected under a quarter-turn `transform`, then output opacity and the migrated\n * blur. Annotation layers need a canvas and are skipped unless `sources` can rasterize them.\n * This is what the renderer's base canvas holds for such documents.\n */\nexport function renderLayeredBase(\n stack: LayerStack,\n region: CropBox,\n transform: ImageTransform,\n sources: CompositorSources,\n compositor = new LayerCompositor()\n): { width: number; height: number; data: Uint8ClampedArray } {\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) {\n throw new Error('renderLayeredBase needs an integer region and a quarter-turn rotation.');\n }\n const composite = compositor.composite(stack.layers, region, sources);\n const out = projectQuarterTurn(composite, region, region, transform);\n applyOutputStage(out, stack.outputOpacity, stack.legacyOutputBlur);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Histograms\n// ---------------------------------------------------------------------------\n\n/** An empty histogram. */\nexport function emptyHistogram(): ImageHistogram {\n return {\n red: new Uint32Array(HISTOGRAM_BINS),\n green: new Uint32Array(HISTOGRAM_BINS),\n blue: new Uint32Array(HISTOGRAM_BINS),\n luminance: new Uint32Array(HISTOGRAM_BINS),\n samples: 0,\n transparent: 0,\n shadows: [0, 0, 0],\n highlights: [0, 0, 0],\n max: 0,\n };\n}\n\n/** Adds the pixels of `rect` (in tile-local coordinates of a `stride`-wide RGBA buffer) to `h`. */\nexport function addToHistogram(h: ImageHistogram, data: ArrayLike<number>, stride: number, rect: CropBox): void {\n for (let y = rect.y; y < rect.y + rect.height; y++) {\n let i = (y * stride + rect.x) * 4;\n for (let x = 0; x < rect.width; x++, i += 4) {\n if (!(data[i + 3] > 0)) {\n h.transparent++;\n continue;\n }\n h.samples++;\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n h.red[r]++;\n h.green[g]++;\n h.blue[b]++;\n h.luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) h.shadows[0]++;\n if (g === 0) h.shadows[1]++;\n if (b === 0) h.shadows[2]++;\n if (r === 255) h.highlights[0]++;\n if (g === 255) h.highlights[1]++;\n if (b === 255) h.highlights[2]++;\n }\n }\n}\n\n/** Sums histograms (and recomputes `max`). */\nexport function sumHistograms(parts: Iterable<ImageHistogram>): ImageHistogram {\n const out = emptyHistogram();\n for (const p of parts) {\n for (let i = 0; i < HISTOGRAM_BINS; i++) {\n out.red[i] += p.red[i];\n out.green[i] += p.green[i];\n out.blue[i] += p.blue[i];\n out.luminance[i] += p.luminance[i];\n }\n out.samples += p.samples;\n out.transparent += p.transparent;\n for (let c = 0; c < 3; c++) {\n out.shadows[c] += p.shadows[c];\n out.highlights[c] += p.highlights[c];\n }\n }\n let max = 0;\n for (let i = 0; i < HISTOGRAM_BINS; i++) max = Math.max(max, out.red[i], out.green[i], out.blue[i], out.luminance[i]);\n out.max = max;\n return out;\n}\n","import type {\n Annotation,\n CropBox,\n ImagePoint,\n ExportOptions,\n ImageEditorFit,\n ImageEditorState,\n ImageTransform,\n PenAnnotation,\n RectAnnotation,\n CircleAnnotation,\n ArrowAnnotation,\n LineAnnotation,\n TextAnnotation,\n ImageAnnotation,\n} from './types';\nimport { applyAdjustments, applyDepthMask } from './filters';\nimport { adjustmentLayersKey } from './adjustment-layers';\nimport { canvasCompositeOperation, blendImageData, normalizeBlendMode } from './blend';\nimport { computeHistogram, type ImageHistogram } from './histogram';\nimport { normalizeProcessVersion } from './color';\nimport { buildLayerMaskPlanes } from './selection/layer-mask';\nimport { selectionOutline, quickMaskPixels } from './selection/overlay';\nimport type { SelectionMask } from './selection/mask';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport { flattenLayers, type ImageLayer, type PixelLayer } from './layers/model';\nimport {\n addToHistogram,\n applyOutputStage,\n baseProjection,\n coveringRect,\n emptyHistogram,\n needsLegacyBase,\n splitOverlayAnnotations,\n sumHistograms,\n} from './layers/render';\nimport { TILE, tileKey, type PixelSurface, type TileKey } from '../raster/surface';\nimport type { GlobalLight } from '../raster/effects/model';\nimport type { EffectPattern } from '../raster/effects/patterns';\nimport { isLegacyShape as isLegacyStack } from './layers/migrate';\nimport {\n applyImageProjectionToContext,\n projectImagePoint,\n clampCropToImage,\n getAnnotationBounds,\n getRotatedSize,\n getViewportMetrics,\n getViewportProjection,\n normalizeAngle,\n type ImageProjection,\n type ViewportMetrics,\n} from './geometry';\n\nexport interface RenderOptions {\n viewportWidth?: number;\n viewportHeight?: number;\n showCropOverlay?: boolean;\n showSelectionOverlay?: boolean;\n interactive?: boolean;\n draftAnnotation?: Annotation | null;\n draftCrop?: CropBox | null;\n /** Viewport fit mode: 'contain' scales to full viewport bounds; 'margin' scales to 85% with 1.0 cap (default). */\n fit?: ImageEditorFit;\n /**\n * How the active selection is shown: marching ants (default when there is a selection),\n * a Quick Mask tint over the unselected area (default when `state.quickMask`), or not at all.\n */\n selectionDisplay?: 'ants' | 'quickMask' | 'none';\n /** Dash offset of the marching ants in screen pixels (animate it to make them march). */\n antsOffset?: number;\n /** A selection being drawn (e.g. a lasso), shown as a dashed outline in natural pixels. */\n draftSelectionPath?: ImagePoint[] | null;\n /** Document rectangles that are the only change since the last render (strokes); recomposites only there. */\n dirty?: CropBox[];\n /** The layer being edited: tiles keep a checkpoint below it so strokes and drags on it start there. */\n checkpointLayerId?: string | null;\n /** The paint tools' cursor: a circle of `diameter` px at (`x`, `y`) in natural pixels. */\n brushCursor?: { x: number; y: number; diameter: number } | null;\n /** Free Transform's frame, handles and reference point in natural pixels (`FreeTransformTool.overlay()`). */\n freeTransform?: {\n outline: ImagePoint[];\n handles: ImagePoint[];\n reference: ImagePoint;\n /** Warp mode: the mesh's grid lines as cubics, its anchors, and handles with their anchors. */\n warp?: { curves: ImagePoint[][]; anchors: ImagePoint[]; handles: Array<{ point: ImagePoint; anchor: ImagePoint }> };\n } | null;\n}\n\n/**\n * Image Cache to avoid refetching HTMLImageElement objects repeatedly during re-renders.\n */\nclass ImageCache {\n private cache = new Map<string, HTMLImageElement>();\n\n public async get(src: string): Promise<HTMLImageElement> {\n const existing = this.cache.get(src);\n if (existing && existing.complete) {\n return existing;\n }\n\n return new Promise((resolve, reject) => {\n if (typeof window === 'undefined' || typeof Image === 'undefined') {\n reject(new Error('Image loading is only supported in browser/DOM environments'));\n return;\n }\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n this.cache.set(src, img);\n resolve(img);\n };\n img.onerror = (e) => reject(new Error(`Failed to load image from: ${src}`));\n img.src = src;\n\n // In JSDOM or test environments where Image resource loading is inactive, resolve promptly\n if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {\n setTimeout(() => {\n try {\n Object.defineProperty(img, 'naturalWidth', { value: 800, configurable: true });\n Object.defineProperty(img, 'naturalHeight', { value: 600, configurable: true });\n Object.defineProperty(img, 'complete', { value: true, configurable: true });\n } catch {\n // Ignore definition errors in restrictive environments\n }\n this.cache.set(src, img);\n resolve(img);\n }, 10);\n }\n });\n }\n\n public set(src: string, img: HTMLImageElement): void {\n this.cache.set(src, img);\n }\n\n /** The loaded image for `src`, or `undefined` while it is not loaded yet. */\n public peek(src: string): HTMLImageElement | undefined {\n const img = this.cache.get(src);\n return img && img.complete ? img : undefined;\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\nexport const globalImageCache = new ImageCache();\n\n/**\n * Renders an image with all its transformations, filters, depth masks, and annotations\n * to a target HTMLCanvasElement.\n */\nexport class ImageEditorRenderer {\n public imageCache = globalImageCache;\n private cachedBaseCanvas: HTMLCanvasElement | null = null;\n private cachedBaseKey = '';\n /**\n * Histogram of the last viewport base render (after adjustments and layers, before\n * annotations); `null` until a render with pixel access has happened.\n */\n public previewHistogram: ImageHistogram | null = null;\n /** Called whenever the viewport base image is re-rendered with the new histogram. */\n public onPreviewHistogram?: (histogram: ImageHistogram | null) => void;\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 0;\n /**\n * The viewport's compositor. It only ever composites the viewport's stack, so its tile cache can\n * trust dirty rectangles; exports use `exportCompositor`.\n */\n public compositor = new LayerCompositor();\n private exportCompositor = new LayerCompositor({ maxCacheBytes: 64 * 1024 * 1024 });\n /** How the host resolves layer pixels (set by the controller). */\n public layerSources?: LayerSourcesHost;\n /**\n * Debug / test switch: `'legacy'` forces the old base path for documents that still have the\n * v1 / v2 shape, `'layers'` forces the compositor. Default: chosen per document.\n */\n public baseRoute: 'auto' | 'legacy' | 'layers' = 'auto';\n /** Which path the last base render took. */\n public lastBaseRoute: 'legacy' | 'layers' | null = null;\n private comp: CompositeCanvas | null = null;\n private depthMaps = new Map<string, { width: number; height: number; data: Uint8ClampedArray }>();\n private assetVersion = 0;\n private scratch: HTMLCanvasElement | null = null;\n private cachedPostCanvas: HTMLCanvasElement | null = null;\n private cachedPostKey = '';\n private cachedPostHistogram: ImageHistogram | null = null;\n\n /**\n * Helper to create a canvas element safely in browser/JSDOM.\n */\n public createCanvas(width: number, height: number): HTMLCanvasElement {\n if (typeof document !== 'undefined' && document.createElement) {\n const c = document.createElement('canvas');\n c.width = Math.max(1, Math.round(width));\n c.height = Math.max(1, Math.round(height));\n return c;\n }\n // Fallback for non-DOM / headless\n return {\n width: Math.max(1, Math.round(width)),\n height: Math.max(1, Math.round(height)),\n getContext: () => null,\n toDataURL: () => '',\n toBlob: () => {},\n } as unknown as HTMLCanvasElement;\n }\n\n /**\n * Renders the editor state onto a canvas (viewport or preview).\n */\n public async renderToCanvas(\n targetCanvas: HTMLCanvasElement,\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: RenderOptions = {}\n ): Promise<void> {\n const ctx = targetCanvas.getContext('2d');\n if (!ctx) return;\n\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const fit = options.fit ?? state.fit ?? 'margin';\n // Geometry helpers read `imageDimensions`; make sure they see the real source size so the\n // viewport drawn here and the hit-testing done by the UI share one projection.\n const geoState: ImageEditorState = {\n ...state,\n fit,\n imageDimensions:\n state.imageDimensions.width === naturalWidth && state.imageDimensions.height === naturalHeight\n ? state.imageDimensions\n : { width: naturalWidth, height: naturalHeight },\n };\n\n const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;\n\n // Viewport dimensions\n const vpWidth = options.viewportWidth || targetCanvas.width;\n const vpHeight = options.viewportHeight || targetCanvas.height;\n\n targetCanvas.width = vpWidth;\n targetCanvas.height = vpHeight;\n\n ctx.clearRect(0, 0, vpWidth, vpHeight);\n\n // If in compare mode, render original image directly and exit\n if (isOriginalCompared) {\n this.drawFittedImage(ctx, img, naturalWidth, naturalHeight, vpWidth, vpHeight, zoom, pan, fit);\n return;\n }\n\n // When actively in crop tool or overlay requested, show full image so user can adjust the crop box\n const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === 'crop');\n const projection = getViewportProjection(geoState, vpWidth, vpHeight, isActivelyCropping);\n const { region } = projection;\n const rotated = getRotatedSize(region.width, region.height, transform);\n\n const route = this.chooseRoute(state, region, naturalWidth, naturalHeight);\n let overlay: Annotation[] = annotations;\n if (route === 'legacy') {\n // 1. Offscreen canvas for Base Image + Transform (Flip/Rotate) + Crop + Adjustments + Depth mask\n const baseKey = [\n this.getSourceId(img),\n state.sourceUrl,\n `${naturalWidth}x${naturalHeight}`,\n `${region.x},${region.y},${region.width},${region.height}`,\n `${projection.angle},${transform.flipH},${transform.flipV}`,\n `${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.tint ?? 0},${adjustments.blur},${adjustments.opacity},p${normalizeProcessVersion(adjustments.process)}`,\n adjustmentLayersKey(adjustments.layers),\n `${depthMask.enabled},${depthMask.maskSource},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`,\n ].join('|');\n\n let baseCanvas = this.cachedBaseCanvas;\n if (!baseCanvas || this.cachedBaseKey !== baseKey) {\n let histogram: ImageHistogram | null = null;\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state, (pixels) => {\n histogram = computeHistogram(pixels);\n });\n this.cachedBaseCanvas = baseCanvas;\n this.cachedBaseKey = baseKey;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n // 2. Draw the base canvas into the viewport (centred, fitted, zoomed, panned)\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(baseCanvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n } else {\n // 1–2. The layer stack, composited per tile in document space, drawn through the projection.\n const split = splitOverlayAnnotations(state.layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const comp = this.updateComposite(split.composite, region, naturalWidth, naturalHeight, options);\n const base = this.layeredBase(comp, state, region, transform);\n if (base) {\n // Drawn exactly as the legacy base canvas is, so both paths look the same at any zoom.\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(base.canvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n this.publishHistogram(base.histogram ?? comp?.histogram() ?? null);\n }\n }\n\n // 3. Annotations live in natural image space: draw them through the same projection,\n // clipped to the visible (cropped) region.\n if (overlay.length > 0 || options.draftAnnotation) {\n ctx.save();\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {\n this.drawAnnotationSelection(ctx, ann);\n }\n }\n // A selected annotation drawn inside the composite still gets its handles.\n if (options.showSelectionOverlay && selectedAnnotationId && !overlay.some((a) => a.id === selectedAnnotationId)) {\n const sel = annotations.find((a) => a.id === selectedAnnotationId);\n if (sel) this.drawAnnotationSelection(ctx, sel);\n }\n\n // Render active live draft annotation\n if (options.draftAnnotation) {\n await this.drawAnnotation(ctx, options.draftAnnotation);\n }\n\n ctx.restore();\n }\n\n // 4. Selection: marching ants or Quick Mask, through the same projection.\n const display = options.selectionDisplay ?? (state.quickMask ? 'quickMask' : 'ants');\n if ((state.selection?.bounds && display !== 'none') || options.draftSelectionPath?.length) {\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n if (state.selection?.bounds && display === 'quickMask') this.drawQuickMask(ctx, state.selection);\n else if (state.selection?.bounds && display === 'ants') this.drawAnts(ctx, state.selection, projection.scale, options.antsOffset ?? 0);\n if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale);\n } catch {\n // Overlays are best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Brush cursor: the tip outline, one screen pixel wide, white over black.\n if (options.brushCursor) {\n const b = options.brushCursor;\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n const px = 1 / (projection.scale || 1);\n ctx.beginPath();\n ctx.arc(b.x, b.y, Math.max(px, b.diameter / 2), 0, Math.PI * 2);\n ctx.lineWidth = 2 * px;\n ctx.strokeStyle = 'rgba(0,0,0,0.6)';\n ctx.stroke();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Free Transform: the frame, square handles and the reference point, a fixed size on screen.\n if (options.freeTransform) {\n const t = options.freeTransform;\n ctx.save();\n try {\n const scr = (p: ImagePoint) => projectImagePoint(p, projection);\n ctx.beginPath();\n if (t.warp) {\n for (const c of t.warp.curves) {\n const [a, b, d, e] = c.map(scr);\n ctx.moveTo(a.x, a.y);\n ctx.bezierCurveTo(b.x, b.y, d.x, d.y, e.x, e.y);\n }\n for (const h of t.warp.handles) {\n const a = scr(h.anchor);\n const q = scr(h.point);\n ctx.moveTo(a.x, a.y);\n ctx.lineTo(q.x, q.y);\n }\n } else {\n const outline = t.outline.map(scr);\n outline.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));\n ctx.closePath();\n }\n ctx.lineWidth = 3;\n ctx.strokeStyle = 'rgba(0,0,0,0.5)';\n ctx.stroke();\n ctx.lineWidth = 1;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n for (const h of [...t.handles, ...(t.warp?.anchors ?? [])].map(scr)) {\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#1e293b';\n ctx.fillRect(h.x - 4, h.y - 4, 8, 8);\n ctx.strokeRect(h.x - 4, h.y - 4, 8, 8);\n }\n for (const h of (t.warp?.handles ?? []).map((w) => scr(w.point))) {\n ctx.beginPath();\n ctx.arc(h.x, h.y, 3, 0, Math.PI * 2);\n ctx.fillStyle = '#ffffff';\n ctx.fill();\n ctx.strokeStyle = '#1e293b';\n ctx.stroke();\n }\n const r = scr(t.reference);\n ctx.beginPath();\n ctx.arc(r.x, r.y, 5, 0, Math.PI * 2);\n ctx.moveTo(r.x - 8, r.y);\n ctx.lineTo(r.x + 8, r.y);\n ctx.moveTo(r.x, r.y - 8);\n ctx.lineTo(r.x, r.y + 8);\n ctx.lineWidth = 3;\n ctx.strokeStyle = 'rgba(0,0,0,0.5)';\n ctx.stroke();\n ctx.lineWidth = 1;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // 5. Draw Crop Grid Overlay if crop overlay is requested\n if (isActivelyCropping) {\n const activeCrop =\n options.draftCrop !== undefined && options.draftCrop !== null\n ? options.draftCrop\n : (crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight });\n this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));\n }\n }\n\n /**\n * Draws `region` of the source image with rotation/flip applied, then pixel adjustments\n * and the depth mask (which receives the identical transform so it stays aligned).\n */\n private async renderBaseImage(\n img: CanvasImageSource,\n naturalWidth: number,\n naturalHeight: number,\n region: CropBox,\n transform: ImageTransform,\n state: Pick<ImageEditorState, 'adjustments' | 'depthMask'>,\n onPixels?: (pixels: ImageData) => void\n ): Promise<HTMLCanvasElement> {\n const { adjustments, depthMask } = state;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const baseCanvas = this.createCanvas(baseW, baseH);\n const baseCtx = baseCanvas.getContext('2d');\n if (!baseCtx) return baseCanvas;\n\n const projection = getBaseProjection(region, transform, baseW, baseH);\n\n baseCtx.save();\n applyImageProjectionToContext(baseCtx, projection);\n baseCtx.drawImage(\n img,\n region.x, region.y, region.width, region.height,\n region.x, region.y, region.width, region.height\n );\n baseCtx.restore();\n\n const imageData = baseCtx.getImageData(0, 0, baseW, baseH);\n if (imageData) {\n // Layer masks live in natural pixels: resample them through the same projection.\n const layerMasks = buildLayerMaskPlanes(adjustments.layers, naturalWidth, naturalHeight, projection, baseW, baseH);\n applyAdjustments(imageData, adjustments, layerMasks ? { layerMasks } : {});\n\n if (depthMask.enabled && depthMask.maskSource) {\n try {\n const maskImg = await this.imageCache.get(depthMask.maskSource);\n const maskSize = getSourceSize(maskImg, naturalWidth, naturalHeight);\n const sx = maskSize.width / naturalWidth;\n const sy = maskSize.height / naturalHeight;\n const maskCanvas = this.createCanvas(baseW, baseH);\n const maskCtx = maskCanvas.getContext('2d');\n if (maskCtx) {\n // The depth map covers the whole source image; scale it to natural pixels and\n // apply the exact same rotate/flip/crop projection as the image itself.\n maskCtx.save();\n applyImageProjectionToContext(maskCtx, projection);\n maskCtx.drawImage(\n maskImg,\n region.x * sx, region.y * sy, region.width * sx, region.height * sy,\n region.x, region.y, region.width, region.height\n );\n maskCtx.restore();\n const maskData = maskCtx.getImageData(0, 0, baseW, baseH);\n if (maskData) {\n applyDepthMask(imageData, maskData, depthMask);\n }\n }\n } catch {\n // Mask failed to load; continue without crashing\n }\n }\n\n onPixels?.(imageData);\n baseCtx.putImageData(imageData, 0, 0);\n }\n\n return baseCanvas;\n }\n\n // --- Layered documents -------------------------------------------------------\n\n /** Which base path renders `state` (see `needsLegacyBase`). */\n private chooseRoute(state: ImageEditorState, region: CropBox, naturalWidth: number, naturalHeight: number): 'legacy' | 'layers' {\n let route: 'legacy' | 'layers';\n if (!state.layers?.length) route = 'legacy';\n else if (this.baseRoute === 'layers') route = 'layers';\n else {\n const stack = { layers: state.layers, outputOpacity: state.outputOpacity ?? 1, legacyOutputBlur: state.legacyOutputBlur };\n const shaped = this.baseRoute === 'legacy' && isLegacyStack(stack);\n route =\n shaped ||\n needsLegacyBase({\n stack,\n region,\n canvas: { width: naturalWidth, height: naturalHeight },\n transform: state.transform,\n sourceOpaque: this.layerSources?.sourceIsOpaque() ?? true,\n })\n ? 'legacy'\n : 'layers';\n }\n this.lastBaseRoute = route;\n return route;\n }\n\n /** Compositor sources for a `width × height` canvas: layer pixels, depth maps and annotation rasterizing. */\n public compositorSources(width: number, height: number): CompositorSources {\n const host = this.layerSources;\n return {\n width,\n height,\n surfaceOf: host ? (l: PixelLayer) => host.surfaceOf(l) : undefined,\n depthMap: (source) => {\n if (!source.enabled || !source.maskSource) return null;\n return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;\n },\n rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),\n version: `${host?.version() ?? ''}:${this.assetVersion}`,\n globalLight: host?.globalLight?.(),\n pattern: host?.pattern ? (id) => host.pattern!(id) : undefined,\n };\n }\n\n /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */\n /** Loads depth maps and annotation images the compositor reads synchronously. */\n public async prepareLayerAssets(layers: readonly ImageLayer[], width: number, height: number): Promise<void> {\n for (const l of flattenLayers(layers)) {\n const source = l.mask?.source;\n if (source?.enabled && source.maskSource) {\n const key = `${source.maskSource}|${width}x${height}`;\n if (this.depthMaps.has(key)) continue;\n try {\n const maskImg = await this.imageCache.get(source.maskSource);\n const canvas = this.createCanvas(width, height);\n const mctx = canvas.getContext('2d');\n const data = mctx ? (mctx.drawImage(maskImg, 0, 0, width, height), mctx.getImageData?.(0, 0, width, height)) : null;\n if (data) {\n this.depthMaps.set(key, { width, height, data: data.data });\n this.assetVersion++;\n }\n } catch {\n // A depth map that fails to load reveals the layer, as before.\n }\n }\n if (l.type === 'annotation' && l.annotation.type === 'image' && !this.imageCache.peek(l.annotation.src)) {\n try {\n await this.imageCache.get(l.annotation.src);\n this.assetVersion++;\n } catch {\n // Skipped, as a watermark that fails to load always was.\n }\n }\n }\n }\n\n /** Whether annotation layers can be rasterized here (a 2D canvas with pixel access). */\n public canRasterize(): boolean {\n const ctx = this.createCanvas(1, 1).getContext('2d');\n return Boolean(ctx && typeof ctx.getImageData === 'function');\n }\n\n /** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */\n private rasterizeAnnotation(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null {\n if (!this.scratch) this.scratch = this.createCanvas(rect.width, rect.height);\n const canvas = this.scratch;\n if (canvas.width !== rect.width || canvas.height !== rect.height) {\n canvas.width = rect.width;\n canvas.height = rect.height;\n }\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n try {\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, rect.width, rect.height);\n ctx.setTransform(1, 0, 0, 1, -rect.x, -rect.y);\n const img = annotation.type === 'image' ? this.imageCache.peek(annotation.src) ?? null : null;\n this.drawAnnotationShape(ctx, annotation, img);\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n return new Uint8ClampedArray(ctx.getImageData(0, 0, rect.width, rect.height).data);\n } catch {\n return null;\n }\n }\n\n private putTile(ctx: CanvasRenderingContext2D, data: Uint8ClampedArray, tx: number, ty: number, rect: CropBox): void {\n const ox = tx * TILE - rect.x;\n const oy = ty * TILE - rect.y;\n const x0 = Math.max(0, -ox);\n const y0 = Math.max(0, -oy);\n const x1 = Math.min(TILE, rect.width - ox);\n const y1 = Math.min(TILE, rect.height - oy);\n if (x1 <= x0 || y1 <= y0) return;\n const img = ctx.createImageData(TILE, TILE);\n img.data.set(data);\n ctx.putImageData(img, ox, oy, x0, y0, x1 - x0, y1 - y0);\n }\n\n /**\n * Brings the viewport's composite canvas (document space, covering `region`) up to date,\n * uploading only the tiles that changed since the last render.\n */\n private updateComposite(\n layers: readonly ImageLayer[],\n region: CropBox,\n width: number,\n height: number,\n options: Pick<RenderOptions, 'dirty' | 'checkpointLayerId'> = {}\n ): CompositeCanvas | null {\n const rect = coveringRect(region);\n let comp = this.comp;\n let fresh = false;\n if (!comp || comp.rect.x !== rect.x || comp.rect.y !== rect.y || comp.rect.width !== rect.width || comp.rect.height !== rect.height) {\n const canvas = this.createCanvas(rect.width, rect.height);\n comp = this.comp = new CompositeCanvas(canvas, rect);\n fresh = true;\n }\n const ctx = comp.canvas.getContext('2d');\n if (!ctx || typeof ctx.createImageData !== 'function') return null;\n let changed = false;\n this.compositor.compositeTiles(layers, rect, this.compositorSources(width, height), (tx, ty, data) => {\n const key = tileKey(tx, ty);\n if (comp!.tiles.get(key) === data) return;\n comp!.tiles.set(key, data);\n comp!.histograms.delete(key);\n this.putTile(ctx, data, tx, ty, rect);\n changed = true;\n }, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? undefined : options.dirty });\n if (changed) comp.version++;\n comp.region = region;\n return comp;\n }\n\n /**\n * The base canvas of a layered document: the composite drawn through the base projection (crop,\n * rotation, flips at scale 1), then output opacity and the migrated blur, which run in the output\n * frame. Cached until the composite or those settings change.\n */\n private layeredBase(\n comp: CompositeCanvas | null,\n state: ImageEditorState,\n region: CropBox,\n transform: ImageTransform\n ): { canvas: HTMLCanvasElement; histogram: ImageHistogram | null } | null {\n if (!comp) return null;\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const key = `${comp.version}|${region.x},${region.y},${region.width},${region.height}|${normalizeAngle(transform.rotate)},${transform.flipH},${transform.flipV}|${opacity}|${blur}`;\n if (this.cachedPostCanvas && this.cachedPostKey === key) return { canvas: this.cachedPostCanvas, histogram: this.cachedPostHistogram };\n const canvas =\n this.cachedPostCanvas && this.cachedPostCanvas.width === baseW && this.cachedPostCanvas.height === baseH\n ? this.cachedPostCanvas\n : this.createCanvas(baseW, baseH);\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n ctx.clearRect(0, 0, baseW, baseH);\n ctx.save();\n applyImageProjectionToContext(ctx, baseProjection(region, transform, baseW, baseH));\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n ctx.drawImage(comp.canvas, comp.rect.x, comp.rect.y);\n ctx.restore();\n const histogram = this.applyPost(ctx, baseW, baseH, state);\n this.cachedPostCanvas = canvas;\n this.cachedPostKey = key;\n this.cachedPostHistogram = histogram;\n return { canvas, histogram };\n }\n\n /** Output opacity then the migrated blur, in place on a base canvas; returns its histogram. */\n private applyPost(ctx: CanvasRenderingContext2D, w: number, h: number, state: ImageEditorState): ImageHistogram | null {\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n if (!(opacity < 1) && !(blur > 0)) return null;\n const data = ctx.getImageData?.(0, 0, w, h);\n if (!data) return null;\n applyOutputStage(data, opacity, blur);\n ctx.putImageData(data, 0, 0);\n return computeHistogram(data);\n }\n\n private publishHistogram(histogram: ImageHistogram | null): void {\n if (histogram === this.previewHistogram) return;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n private quickMaskCache = new WeakMap<SelectionMask, HTMLCanvasElement>();\n private outlineCache = new WeakMap<SelectionMask, Float32Array>();\n\n /** Rubylith over the unselected area, drawn in natural image space. */\n private drawQuickMask(ctx: CanvasRenderingContext2D, selection: SelectionMask): void {\n let canvas = this.quickMaskCache.get(selection);\n if (!canvas) {\n canvas = this.createCanvas(selection.width, selection.height);\n const mctx = canvas.getContext('2d');\n if (!mctx || typeof mctx.createImageData !== 'function') return;\n const img = mctx.createImageData(selection.width, selection.height);\n img.data.set(quickMaskPixels(selection));\n mctx.putImageData(img, 0, 0);\n this.quickMaskCache.set(selection, canvas);\n }\n ctx.drawImage(canvas, 0, 0, selection.width, selection.height);\n }\n\n /** Marching ants: a white line under a black dashed one, one screen pixel wide. */\n private drawAnts(ctx: CanvasRenderingContext2D, selection: SelectionMask, scale: number, offset: number): void {\n let segs = this.outlineCache.get(selection);\n if (!segs) {\n segs = selectionOutline(selection);\n this.outlineCache.set(selection, segs);\n }\n if (!segs.length) return;\n const px = 1 / (scale || 1);\n ctx.beginPath();\n for (let i = 0; i < segs.length; i += 4) {\n ctx.moveTo(segs[i], segs[i + 1]);\n ctx.lineTo(segs[i + 2], segs[i + 3]);\n }\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') {\n ctx.setLineDash([4 * px, 4 * px]);\n ctx.lineDashOffset = -offset * px;\n }\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private drawDraftPath(ctx: CanvasRenderingContext2D, points: ImagePoint[], scale: number): void {\n const px = 1 / (scale || 1);\n ctx.beginPath();\n points.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)));\n ctx.closePath();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([4 * px, 4 * px]);\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private getSourceId(img: CanvasImageSource): number {\n const key = img as unknown as object;\n let id = this.sourceIds.get(key);\n if (id === undefined) {\n id = ++this.nextSourceId;\n this.sourceIds.set(key, id);\n }\n return id;\n }\n\n /**\n * Draws an individual annotation onto the canvas.\n */\n public async drawAnnotation(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n const blend = normalizeBlendMode(ann.blend);\n if (blend === 'normal') {\n await this.drawAnnotationContent(ctx, ann);\n return;\n }\n const op = canvasCompositeOperation(blend);\n if (op) {\n ctx.save();\n ctx.globalCompositeOperation = op;\n await this.drawAnnotationContent(ctx, ann);\n ctx.restore();\n return;\n }\n if (!(await this.drawAnnotationBlended(ctx, ann))) {\n // No pixel access (e.g. a tainted or headless canvas): draw it normally.\n await this.drawAnnotationContent(ctx, ann);\n }\n }\n\n /**\n * Scalar fallback for blend modes the canvas cannot composite (Photoshop soft light, linear /\n * vivid / pin light, hard mix, subtract, divide, darker / lighter colour, linear burn / dodge):\n * draws the annotation on a same-size layer with the same transform, blends it into a copy of\n * the target's pixels with `blendImageData`, and copies the result back inside the current clip.\n */\n private async drawAnnotationBlended(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<boolean> {\n const canvas = ctx.canvas as HTMLCanvasElement | undefined;\n if (!canvas || typeof ctx.getTransform !== 'function' || typeof ctx.getImageData !== 'function') return false;\n const w = canvas.width;\n const h = canvas.height;\n const layer = this.createCanvas(w, h);\n const lctx = layer.getContext('2d');\n if (!lctx) return false;\n try {\n lctx.setTransform(ctx.getTransform());\n lctx.globalAlpha = ctx.globalAlpha;\n await this.drawAnnotationContent(lctx, ann);\n const backdrop = ctx.getImageData(0, 0, w, h);\n const source = lctx.getImageData(0, 0, w, h);\n if (!backdrop || !source) return false;\n blendImageData(backdrop, source, normalizeBlendMode(ann.blend));\n lctx.setTransform(1, 0, 0, 1, 0, 0);\n lctx.putImageData(backdrop, 0, 0);\n } catch {\n return false;\n }\n ctx.save();\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.globalAlpha = 1;\n ctx.globalCompositeOperation = 'copy';\n ctx.drawImage(layer, 0, 0);\n ctx.restore();\n return true;\n }\n\n private async drawAnnotationContent(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n let img: CanvasImageSource | null = null;\n if (ann.type === 'image') {\n try {\n img = await this.imageCache.get(ann.src);\n } catch {\n // Skip if watermark image failed to load\n }\n }\n this.drawAnnotationShape(ctx, ann, img);\n }\n\n /** Draws an annotation synchronously; an image annotation draws `img` (skipped when `null`). */\n public drawAnnotationShape(ctx: CanvasRenderingContext2D, ann: Annotation, img: CanvasImageSource | null): void {\n ctx.save();\n if (ann.opacity !== undefined) {\n ctx.globalAlpha = ann.opacity;\n }\n\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n const pen = ann as PenAnnotation;\n if (pen.points.length > 0) {\n ctx.beginPath();\n ctx.strokeStyle = pen.strokeColor;\n ctx.lineWidth = pen.strokeWidth;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n ctx.moveTo(pen.points[0].x, pen.points[0].y);\n for (let i = 1; i < pen.points.length; i++) {\n ctx.lineTo(pen.points[i].x, pen.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n\n case 'rect': {\n const rect = ann as RectAnnotation;\n ctx.strokeStyle = rect.strokeColor;\n ctx.lineWidth = rect.strokeWidth;\n if (rect.fillColor) {\n ctx.fillStyle = rect.fillColor;\n ctx.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n if (rect.strokeWidth > 0) {\n ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);\n }\n break;\n }\n\n case 'circle': {\n const circle = ann as CircleAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = circle.strokeColor;\n ctx.lineWidth = circle.strokeWidth;\n ctx.ellipse(circle.x, circle.y, Math.abs(circle.radiusX), Math.abs(circle.radiusY), 0, 0, 2 * Math.PI);\n if (circle.fillColor) {\n ctx.fillStyle = circle.fillColor;\n ctx.fill();\n }\n if (circle.strokeWidth > 0) {\n ctx.stroke();\n }\n break;\n }\n\n case 'arrow': {\n const arrow = ann as ArrowAnnotation;\n ctx.strokeStyle = arrow.strokeColor;\n ctx.fillStyle = arrow.strokeColor;\n ctx.lineWidth = arrow.strokeWidth;\n\n // Line body\n ctx.beginPath();\n ctx.moveTo(arrow.x, arrow.y);\n ctx.lineTo(arrow.endX, arrow.endY);\n ctx.stroke();\n\n // Arrow head\n const angle = Math.atan2(arrow.endY - arrow.y, arrow.endX - arrow.x);\n const headLen = Math.max(12, arrow.strokeWidth * 3);\n ctx.beginPath();\n ctx.moveTo(arrow.endX, arrow.endY);\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle - Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle - Math.PI / 6)\n );\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle + Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle + Math.PI / 6)\n );\n ctx.closePath();\n ctx.fill();\n break;\n }\n\n case 'line': {\n const line = ann as LineAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = line.strokeColor;\n ctx.lineWidth = line.strokeWidth;\n ctx.moveTo(line.x, line.y);\n ctx.lineTo(line.endX, line.endY);\n ctx.stroke();\n break;\n }\n\n case 'text': {\n const textAnn = ann as TextAnnotation;\n ctx.font = `${textAnn.fontWeight || 'normal'} ${textAnn.fontSize}px ${textAnn.fontFamily || 'sans-serif'}`;\n ctx.fillStyle = textAnn.color;\n ctx.textBaseline = 'top';\n ctx.fillText(textAnn.text, textAnn.x, textAnn.y);\n break;\n }\n\n case 'image': {\n const imgAnn = ann as ImageAnnotation;\n if (img) ctx.drawImage(img, imgAnn.x, imgAnn.y, imgAnn.width, imgAnn.height);\n break;\n }\n }\n\n ctx.restore();\n }\n\n /**\n * Draws selection box around active annotation.\n */\n private drawAnnotationSelection(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.strokeStyle = '#3b82f6'; // Tailwind blue-500\n ctx.lineWidth = 1.5;\n ctx.setLineDash([4, 4]);\n\n const bounds = getAnnotationBounds(ann);\n const pad = 4;\n ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);\n\n // Draw handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#3b82f6';\n ctx.setLineDash([]);\n const handleSize = 6;\n const corners = [\n { x: bounds.x - pad, y: bounds.y - pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y - pad },\n { x: bounds.x - pad, y: bounds.y + bounds.height + pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y + bounds.height + pad },\n ];\n for (const c of corners) {\n ctx.fillRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n /** @deprecated Use the standalone `getAnnotationBounds` from the geometry module. */\n public getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n return getAnnotationBounds(ann);\n }\n\n /**\n * Draws a rule-of-thirds crop grid overlay. All rectangles come from\n * `getViewportMetrics`, i.e. the crop box projected through rotation/flip.\n */\n private drawCropOverlay(ctx: CanvasRenderingContext2D, metrics: ViewportMetrics): void {\n const { imageRect, cropRect } = metrics;\n if (!cropRect) return;\n\n const imgL = imageRect.x;\n const imgT = imageRect.y;\n const imgR = imageRect.x + imageRect.width;\n const imgB = imageRect.y + imageRect.height;\n const startX = cropRect.x;\n const startY = cropRect.y;\n const cropBoxW = cropRect.width;\n const cropBoxH = cropRect.height;\n\n ctx.save();\n\n // Dark semi-transparent scrim outside crop box (within the image)\n ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';\n // Top\n ctx.fillRect(imgL, imgT, imageRect.width, Math.max(0, startY - imgT));\n // Bottom\n ctx.fillRect(imgL, startY + cropBoxH, imageRect.width, Math.max(0, imgB - (startY + cropBoxH)));\n // Left\n ctx.fillRect(imgL, startY, Math.max(0, startX - imgL), cropBoxH);\n // Right\n ctx.fillRect(startX + cropBoxW, startY, Math.max(0, imgR - (startX + cropBoxW)), cropBoxH);\n\n // Crop border\n ctx.strokeStyle = '#ffffff';\n ctx.lineWidth = 2;\n ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);\n\n // Rule of thirds lines\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';\n ctx.lineWidth = 1;\n ctx.beginPath();\n // Vertical lines\n ctx.moveTo(startX + cropBoxW / 3, startY);\n ctx.lineTo(startX + cropBoxW / 3, startY + cropBoxH);\n ctx.moveTo(startX + (cropBoxW * 2) / 3, startY);\n ctx.lineTo(startX + (cropBoxW * 2) / 3, startY + cropBoxH);\n // Horizontal lines\n ctx.moveTo(startX, startY + cropBoxH / 3);\n ctx.lineTo(startX + cropBoxW, startY + cropBoxH / 3);\n ctx.moveTo(startX, startY + (cropBoxH * 2) / 3);\n ctx.lineTo(startX + cropBoxW, startY + (cropBoxH * 2) / 3);\n ctx.stroke();\n\n // Corner and edge handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#0284c7';\n ctx.lineWidth = 1.5;\n const handleSize = 10;\n const handles = [\n // Corners\n { x: startX, y: startY },\n { x: startX + cropBoxW, y: startY },\n { x: startX, y: startY + cropBoxH },\n { x: startX + cropBoxW, y: startY + cropBoxH },\n // Edges\n { x: startX + cropBoxW / 2, y: startY },\n { x: startX + cropBoxW / 2, y: startY + cropBoxH },\n { x: startX, y: startY + cropBoxH / 2 },\n { x: startX + cropBoxW, y: startY + cropBoxH / 2 },\n ];\n for (const h of handles) {\n ctx.fillRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n private drawFittedImage(\n ctx: CanvasRenderingContext2D,\n img: CanvasImageSource,\n imgW: number,\n imgH: number,\n vpW: number,\n vpH: number,\n zoom: number,\n pan: { x: number; y: number },\n fit: ImageEditorFit = 'margin'\n ): void {\n ctx.save();\n ctx.translate(vpW / 2 + pan.x, vpH / 2 + pan.y);\n ctx.scale(zoom, zoom);\n const fitScale =\n fit === 'contain'\n ? Math.min(vpW / imgW, vpH / imgH)\n : Math.min((vpW * 0.85) / imgW, (vpH * 0.85) / imgH, 1);\n const renderW = imgW * fitScale;\n const renderH = imgH * fitScale;\n ctx.drawImage(img, -renderW / 2, -renderH / 2, renderW, renderH);\n ctx.restore();\n }\n\n /**\n * Performs an export render at exact pixel dimensions without zoom/pan viewport offsets.\n */\n public async renderExport(\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: ExportOptions = {}\n ): Promise<HTMLCanvasElement> {\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const { crop, transform, annotations } = state;\n\n const region = crop\n ? clampCropToImage(crop, naturalWidth, naturalHeight)\n : { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n\n const exportW = options.width || baseW;\n const exportH = options.height || baseH;\n\n const exportCanvas = this.createCanvas(exportW, exportH);\n const ctx = exportCanvas.getContext('2d');\n if (!ctx) return exportCanvas;\n\n let overlay: Annotation[] = annotations;\n let baseCanvas: HTMLCanvasElement;\n // A render of chosen layers (`layerIds`) always composites them; the legacy views describe the whole stack.\n if (!options.layerIds && this.chooseRoute(state, region, naturalWidth, naturalHeight) === 'legacy') {\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state);\n } else {\n const layers = options.includeAnnotations === false ? removeAnnotationLayers(state.layers) : state.layers;\n const split = splitOverlayAnnotations(layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const rect = coveringRect(region);\n const comp = this.createCanvas(rect.width, rect.height);\n const cctx = comp.getContext('2d');\n if (cctx && typeof cctx.createImageData === 'function') {\n this.exportCompositor.compositeTiles(split.composite, rect, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {\n this.putTile(cctx, data, tx, ty, rect);\n });\n }\n baseCanvas = this.createCanvas(baseW, baseH);\n const bctx = baseCanvas.getContext('2d');\n if (bctx) {\n bctx.save();\n applyImageProjectionToContext(bctx, baseProjection(region, transform, baseW, baseH));\n bctx.beginPath();\n bctx.rect(region.x, region.y, region.width, region.height);\n bctx.clip();\n bctx.drawImage(comp, rect.x, rect.y);\n bctx.restore();\n this.applyPost(bctx, baseW, baseH, state);\n }\n }\n\n // Scale to target export dimensions\n ctx.drawImage(baseCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);\n\n // Draw annotations (natural image space) through the same rotate/flip/crop projection\n if (options.includeAnnotations !== false && overlay.length > 0) {\n ctx.save();\n ctx.scale(exportW / baseW, exportH / baseH);\n applyImageProjectionToContext(ctx, getBaseProjection(region, transform, baseW, baseH));\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n }\n ctx.restore();\n }\n\n return exportCanvas;\n }\n\n /**\n * Generates a strictly binary mask canvas (white = area to regenerate, black = keep) in\n * natural image pixel space of the current source image — the same space the crop box\n * and annotations are stored in. Pair it with `toSourceDataURL()` for AI inpainting.\n */\n public async renderMask(\n state: ImageEditorState,\n options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }\n ): Promise<HTMLCanvasElement> {\n const naturalWidth = Math.max(1, Math.round(state.imageDimensions.width));\n const naturalHeight = Math.max(1, Math.round(state.imageDimensions.height));\n const maskCanvas = this.createCanvas(naturalWidth, naturalHeight);\n const ctx = maskCanvas.getContext('2d');\n if (!ctx) return maskCanvas;\n\n // Background: unmasked (black)\n ctx.fillStyle = MASK_BLACK;\n ctx.fillRect(0, 0, naturalWidth, naturalHeight);\n\n const useCrop = options?.useCrop ?? true;\n const useAnnotations = options?.useAnnotations ?? true;\n\n // Crop box mask (if active)\n if (useCrop && state.crop) {\n ctx.fillStyle = MASK_WHITE;\n ctx.fillRect(state.crop.x, state.crop.y, state.crop.width, state.crop.height);\n }\n\n // Annotation masks (drawn in solid white, never in annotation colours)\n if (useAnnotations && state.annotations.length > 0) {\n for (const ann of state.annotations) {\n this.drawAnnotationMask(ctx, ann);\n }\n }\n\n // Anti-aliased edges produce grey pixels: threshold to pure black/white.\n const imageData = ctx.getImageData?.(0, 0, naturalWidth, naturalHeight);\n // The active selection (same natural space) is added to the mask.\n const selection =\n (options?.useSelection ?? true) &&\n state.selection?.bounds &&\n state.selection.width === naturalWidth &&\n state.selection.height === naturalHeight\n ? state.selection\n : null;\n if (imageData) {\n const d = imageData.data;\n for (let i = 0; i < d.length; i += 4) {\n const v = d[i] >= 128 || (selection !== null && selection.data[i >> 2] >= 128) ? 255 : 0;\n d[i] = v;\n d[i + 1] = v;\n d[i + 2] = v;\n d[i + 3] = 255;\n }\n ctx.putImageData(imageData, 0, 0);\n }\n\n return maskCanvas;\n }\n\n private drawAnnotationMask(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.globalAlpha = 1;\n ctx.fillStyle = MASK_WHITE;\n ctx.strokeStyle = MASK_WHITE;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n if (ann.points.length > 0) {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.points[0].x, ann.points[0].y);\n for (let i = 1; i < ann.points.length; i++) {\n ctx.lineTo(ann.points[i].x, ann.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n case 'circle': {\n ctx.beginPath();\n ctx.ellipse(ann.x, ann.y, Math.abs(ann.radiusX), Math.abs(ann.radiusY), 0, 0, Math.PI * 2);\n ctx.fill();\n break;\n }\n case 'arrow':\n case 'line': {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.x, ann.y);\n ctx.lineTo(ann.endX, ann.endY);\n ctx.stroke();\n break;\n }\n case 'rect':\n case 'image':\n case 'text': {\n const b = getAnnotationBounds(ann);\n ctx.fillRect(b.x, b.y, b.width, b.height);\n break;\n }\n }\n ctx.restore();\n }\n}\n\nconst MASK_WHITE = '#ffffff';\nconst MASK_BLACK = '#000000';\n\nfunction getSourceSize(img: CanvasImageSource, fallbackW = 800, fallbackH = 600): { width: number; height: number } {\n const anyImg = img as any;\n return {\n width: anyImg.naturalWidth || anyImg.videoWidth || anyImg.width || fallbackW,\n height: anyImg.naturalHeight || anyImg.videoHeight || anyImg.height || fallbackH,\n };\n}\n\nfunction getBaseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/** What the renderer asks its host (the controller) about layer pixels. */\nexport interface LayerSourcesHost {\n surfaceOf(layer: PixelLayer): PixelSurface;\n sourceIsOpaque(): boolean;\n version(): string;\n /** The document's global light for layer effects. */\n globalLight?(): GlobalLight | undefined;\n /** A document pattern for pattern effects (built-in patterns are found without it). */\n pattern?(id: string): EffectPattern | null;\n}\n\n/** The viewport's composite canvas and what each of its tiles holds. */\nclass CompositeCanvas {\n readonly tiles = new Map<TileKey, Uint8ClampedArray>();\n readonly histograms = new Map<TileKey, ImageHistogram>();\n version = 0;\n region: CropBox;\n constructor(readonly canvas: HTMLCanvasElement, readonly rect: CropBox) {\n this.region = rect;\n }\n\n /** Histogram of the composite over the region (per-tile parts are cached). */\n histogram(): ImageHistogram | null {\n if (!this.tiles.size) return null;\n const r = coveringRect(this.region);\n const parts: ImageHistogram[] = [];\n for (const [key, data] of this.tiles) {\n let h = this.histograms.get(key);\n if (!h) {\n const tx = (key % 65536) - 32768;\n const ty = Math.floor(key / 65536) - 32768;\n const x0 = Math.max(r.x, tx * TILE);\n const y0 = Math.max(r.y, ty * TILE);\n const x1 = Math.min(r.x + r.width, tx * TILE + TILE);\n const y1 = Math.min(r.y + r.height, ty * TILE + TILE);\n h = emptyHistogram();\n if (x1 > x0 && y1 > y0) addToHistogram(h, data, TILE, { x: x0 - tx * TILE, y: y0 - ty * TILE, width: x1 - x0, height: y1 - y0 });\n this.histograms.set(key, h);\n }\n parts.push(h);\n }\n return sumHistograms(parts);\n }\n}\n\nfunction removeAnnotationLayers(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: removeAnnotationLayers(l.children) } : l);\n }\n return out;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox, ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type SelectionMask } from './mask';\n\nexport interface ShapeSelectionOptions {\n /** Partial coverage along the edge (default true). */\n antiAlias?: boolean;\n}\n\ninterface Edge {\n top: number;\n bottom: number;\n ax: number;\n ay: number;\n bx: number;\n by: number;\n}\n\n/**\n * Even-odd polygon coverage over a `width × height` image: exact horizontal coverage on 4\n * sub-rows per pixel row (1 without anti-aliasing, where a pixel is in when its centre is).\n * Each sub-row only tests the edges that cross it (an active edge list over edges sorted by\n * their top), so the cost is about pixels + edges × rows they span.\n */\nexport function polygonCoverage(\n points: readonly ImagePoint[],\n width: number,\n height: number,\n antiAlias = true\n): Float32Array {\n const m = new Float32Array(width * height);\n if (points.length < 3 || width <= 0 || height <= 0) return m;\n const sub = antiAlias ? 4 : 1;\n const edges: Edge[] = [];\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;\n if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n }\n edges.sort((p, q) => p.top - q.top);\n let next = 0;\n let active: Edge[] = [];\n const xs: number[] = [];\n for (let py = 0; py < height; py++) {\n const row = py * width;\n for (let s = 0; s < sub; s++) {\n const y = py + (s + 0.5) / sub;\n while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);\n if (active.length) active = active.filter((e) => e.bottom > y);\n if (!active.length) continue;\n xs.length = 0;\n for (const e of active) {\n if (e.top <= y) xs.push(e.ax + ((y - e.ay) / (e.by - e.ay)) * (e.bx - e.ax));\n }\n xs.sort((a, b) => a - b);\n for (let k = 0; k + 1 < xs.length; k += 2) {\n const x0 = Math.max(0, xs[k]);\n const x1 = Math.min(width, xs[k + 1]);\n if (x1 <= x0) continue;\n const c0 = Math.floor(x0);\n const c1 = Math.min(width, Math.ceil(x1));\n for (let cx = c0; cx < c1; cx++) {\n let cov: number;\n if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);\n else {\n const c = cx + 0.5;\n cov = c >= x0 && c < x1 ? 1 : 0;\n }\n if (cov > 0) m[row + cx] += cov / sub;\n }\n }\n }\n }\n for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;\n return m;\n}\n\nfunction toMask(width: number, height: number, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** Lasso / polygonal lasso: an even-odd polygon in natural image pixels. */\nexport function polygonSelection(\n width: number,\n height: number,\n points: readonly ImagePoint[],\n options: ShapeSelectionOptions = {}\n): SelectionMask {\n return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));\n}\n\n/** Normalises a rectangle with a negative width or height. */\nexport function normalizeRect(rect: CropBox): CropBox {\n const x = rect.width < 0 ? rect.x + rect.width : rect.x;\n const y = rect.height < 0 ? rect.y + rect.height : rect.y;\n return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };\n}\n\n/** Rectangular marquee. Fractional edges get partial coverage when anti-aliased. */\nexport function rectSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n const r = normalizeRect(rect);\n const pts = [\n { x: r.x, y: r.y },\n { x: r.x + r.width, y: r.y },\n { x: r.x + r.width, y: r.y + r.height },\n { x: r.x, y: r.y + r.height },\n ];\n return polygonSelection(width, height, pts, options);\n}\n\n/** The ellipse inscribed in `rect` as a polygon fine enough that its chords stay within 0.05 px. */\nexport function ellipsePoints(rect: CropBox): ImagePoint[] {\n const r = normalizeRect(rect);\n const rx = r.width / 2;\n const ry = r.height / 2;\n const cx = r.x + rx;\n const cy = r.y + ry;\n const rmax = Math.max(rx, ry, 0.5);\n // Sagitta of a chord with angle step θ is r(1 − cos(θ/2)) ≈ rθ²/8 ≤ 0.05.\n const step = Math.sqrt(0.4 / rmax);\n const n = Math.min(4096, Math.max(16, Math.ceil((2 * Math.PI) / step)));\n const pts: ImagePoint[] = [];\n for (let i = 0; i < n; i++) {\n const t = (i / n) * 2 * Math.PI;\n pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });\n }\n return pts;\n}\n\n/** Elliptical marquee inscribed in `rect`. */\nexport function ellipseSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n return polygonSelection(width, height, ellipsePoints(rect), options);\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\n\n/** How the magic wand measures colour difference. */\nexport type ColorDistance = 'rgb' | 'perceptual';\n\nexport interface MagicWandOptions {\n /**\n * 0..255. `rgb`: the largest per-channel difference in levels (alpha included), as\n * Photoshop's wand. `perceptual`: CIE76 ΔE*ab, with 255 ≙ ΔE 100. Default 32.\n */\n tolerance?: number;\n /** Only the 4-connected region around the seed (default true); otherwise every similar pixel. */\n contiguous?: boolean;\n /** Soft one-pixel fringe on the edge (default true). */\n antiAlias?: boolean;\n /** Default `rgb`. */\n distance?: ColorDistance;\n}\n\nfunction checkPixels(p: PixelBuffer): number {\n const n = p.width * p.height;\n if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {\n throw new Error('Invalid pixel buffer size');\n }\n if (p.data.length < n * 4) throw new Error('Pixel data is shorter than width × height × 4');\n return n;\n}\n\nconst SRGB_TO_LINEAR = (() => {\n const t = new Float32Array(256);\n for (let i = 0; i < 256; i++) {\n const v = i / 255;\n t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n }\n return t;\n})();\n\nconst labF = (t: number) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116);\n\n/** CIE Lab (D65) of an 8-bit sRGB colour. */\nexport function srgb8ToLab(r: number, g: number, b: number): [number, number, number] {\n const rl = SRGB_TO_LINEAR[r & 255];\n const gl = SRGB_TO_LINEAR[g & 255];\n const bl = SRGB_TO_LINEAR[b & 255];\n const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;\n const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;\n const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;\n const fx = labF(x);\n const fy = labF(y);\n const fz = labF(z);\n return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];\n}\n\n/** `similar(i)` for the wand: does pixel `i` match the seed colour within the tolerance? */\nfunction similarity(pixels: PixelBuffer, seed: number, tolerance: number, distance: ColorDistance): (i: number) => boolean {\n const d = pixels.data;\n const tr = d[seed * 4];\n const tg = d[seed * 4 + 1];\n const tb = d[seed * 4 + 2];\n const ta = d[seed * 4 + 3];\n const tol = Math.max(0, Math.floor(tolerance + 1e-3));\n if (distance === 'perceptual') {\n const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);\n const de = (Math.max(0, tolerance) * 100) / 255;\n const de2 = de * de + 1e-6;\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n // Copying a selection can leave RGB under zero alpha; hidden colours must not split a\n // transparent region.\n if (ta === 0 && a === 0) return true;\n if (Math.abs(a - ta) > tol) return false;\n const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);\n return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;\n };\n }\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n if (ta === 0 && a === 0) return true;\n return (\n Math.abs(d[p] - tr) <= tol &&\n Math.abs(d[p + 1] - tg) <= tol &&\n Math.abs(d[p + 2] - tb) <= tol &&\n Math.abs(a - ta) <= tol\n );\n };\n}\n\n/**\n * Softens hard edges: an edge pixel (one whose 3×3 neighbourhood differs from it) becomes\n * `½ + ½·avg` if selected, `½·avg` if not (neighbours outside the image count as the pixel).\n * Only the box `x0..x1 × y0..y1` (exclusive ends) is visited.\n */\nfunction antialiasMarks(marks: Uint8Array, w: number, h: number, x0: number, y0: number, x1: number, y1: number): Uint8Array {\n const out = marks.slice();\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const i = y * w + x;\n const v = marks[i];\n let sum = 0;\n let edge = false;\n for (let dy = -1; dy <= 1; dy++) {\n const ny = y + dy;\n for (let dx = -1; dx <= 1; dx++) {\n const nx = x + dx;\n const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];\n if (n !== v) edge = true;\n sum += n;\n }\n }\n if (edge) {\n const avg = sum / (9 * 255);\n out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);\n }\n }\n }\n return out;\n}\n\n/**\n * Magic wand: pixels similar to the seed pixel. Contiguous mode is a scanline span fill with an\n * explicit stack (fill a run, then scan the rows above and below it for new runs), so it is\n * linear in the pixels visited and never recurses. An empty selection when the seed is outside.\n */\nexport function magicWand(pixels: PixelBuffer, seed: ImagePoint, options: MagicWandOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h } = pixels;\n const sx = Math.floor(seed.x);\n const sy = Math.floor(seed.y);\n if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);\n const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? 'rgb');\n const marks = new Uint8Array(n);\n let bx0 = w;\n let by0 = h;\n let bx1 = -1;\n let by1 = -1;\n if (options.contiguous ?? true) {\n // Span fill. Each stack entry is a seed pixel index.\n const stack: number[] = [sy * w + sx];\n while (stack.length) {\n const idx = stack.pop()!;\n if (marks[idx]) continue;\n const y = (idx / w) | 0;\n const row = y * w;\n let l = idx - row;\n let r = l;\n while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;\n while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;\n marks.fill(255, row + l, row + r + 1);\n if (l < bx0) bx0 = l;\n if (r > bx1) bx1 = r;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n for (const ny of [y - 1, y + 1]) {\n if (ny < 0 || ny >= h) continue;\n const nrow = ny * w;\n let inRun = false;\n for (let nx = l; nx <= r; nx++) {\n const ok = !marks[nrow + nx] && similar(nrow + nx);\n if (ok && !inRun) stack.push(nrow + nx);\n inRun = ok;\n }\n }\n }\n } else {\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) {\n if (similar(row + x)) {\n marks[row + x] = 255;\n if (x < bx0) bx0 = x;\n if (x > bx1) bx1 = x;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n }\n }\n }\n }\n if (bx1 < 0) return createSelectionMask(w, h);\n if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);\n return createSelectionMask(\n w,\n h,\n antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))\n );\n}\n\n/** One Color Range sample: a colour (0..255 levels or `#rrggbb`) and, optionally, where it was picked. */\nexport interface ColorRangeSample {\n color: readonly [number, number, number] | string;\n at?: ImagePoint;\n}\n\nexport interface ColorRangeOptions {\n /** 0..200 levels (default 40): coverage falls off linearly to 0 at `fuzziness + 1` levels. */\n fuzziness?: number;\n /**\n * Localized Color Clusters: a sample with a position also fades with distance from it,\n * reaching 0 at this many pixels.\n */\n localized?: number;\n}\n\nfunction sampleColor(c: ColorRangeSample['color']): [number, number, number] {\n if (typeof c === 'string') {\n const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());\n if (!m) throw new Error(`Invalid colour \"${c}\"`);\n const v = parseInt(m[1], 16);\n return [(v >> 16) & 255, (v >> 8) & 255, v & 255];\n }\n return [c[0], c[1], c[2]];\n}\n\n/**\n * Select › Color Range › Sampled Colors: each pixel takes its best match over the samples; a\n * match falls off linearly with the largest per-channel difference, `1 − d / (fuzziness + 1)`.\n * Transparent pixels are never selected.\n */\nexport function colorRange(pixels: PixelBuffer, samples: readonly ColorRangeSample[], options: ColorRangeOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h, data } = pixels;\n const f = Math.max(0, options.fuzziness ?? 40) + 1;\n const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));\n const radius = options.localized !== undefined ? Math.max(1, options.localized) : undefined;\n const out = new Uint8Array(n);\n if (!parsed.length) return createSelectionMask(w, h, out);\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (data[p + 3] <= 0) continue;\n const x = i % w;\n const y = (i - x) / w;\n let best = 0;\n for (const s of parsed) {\n const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));\n let k = Math.min(1, Math.max(0, 1 - d / f));\n if (k > 0 && radius !== undefined && s.at) {\n const dist = Math.hypot(x - s.at.x, y - s.at.y);\n k *= Math.min(1, Math.max(0, 1 - dist / radius));\n }\n if (k > best) best = k;\n }\n out[i] = coverageToByte(best);\n }\n return createSelectionMask(w, h, out);\n}\n\nexport type ToneRangePreset = 'shadows' | 'midtones' | 'highlights';\n\n/** Luma bounds (0..255, Rec. 601) of the tonal presets. */\nexport const TONE_RANGE_PRESETS: Record<ToneRangePreset, [number, number]> = {\n shadows: [0, 65],\n midtones: [105, 150],\n highlights: [190, 255],\n};\n\n/**\n * Select › Color Range › Shadows / Midtones / Highlights: pixels whose luma lies in `lo..hi` are\n * selected; outside, selection falls off linearly to 0 over `falloff` levels.\n */\nexport function toneRange(\n pixels: PixelBuffer,\n range: ToneRangePreset | readonly [number, number],\n falloff = 20\n): SelectionMask {\n const n = checkPixels(pixels);\n const [lo, hi] = typeof range === 'string' ? TONE_RANGE_PRESETS[range] : range;\n const ramp = Math.max(1e-3, falloff);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));\n const outside = l < lo ? lo - l : l > hi ? l - hi : 0;\n out[i] = coverageToByte(1 - outside / ramp);\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n\n/**\n * Select › Color Range › Reds … Magentas (`center` 0, 60, …, 300°): closeness of hue (full at\n * the centre, none 60° away) × chroma × (2 − max), so neutrals are never selected.\n */\nexport function hueRange(pixels: PixelBuffer, center: number): SelectionMask {\n const n = checkPixels(pixels);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const r = d[p] / 255;\n const g = d[p + 1] / 255;\n const b = d[p + 2] / 255;\n const mx = Math.max(r, g, b);\n const mn = Math.min(r, g, b);\n const chroma = mx - mn;\n if (chroma <= 0) continue;\n let hue: number;\n if (mx === r) hue = (((g - b) / chroma) % 6 + 6) % 6;\n else if (mx === g) hue = (b - r) / chroma + 2;\n else hue = (r - g) / chroma + 4;\n hue *= 60;\n let dh = (((hue - center) % 360) + 360) % 360;\n dh = Math.min(dh, 360 - dh);\n out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n","// Portions ported from photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\nimport { distanceTransform, gaussianBlur, greyMorphology, shiftCoverage } from './modify';\n\n/**\n * Refine Edge / Select and Mask parameters (Photoshop's units). Refinement is edge-aware: within\n * `radius` pixels of the mask's edge the coverage is re-estimated from the image with a colour\n * guided filter (He et al.), so a coarse or misaligned mask (e.g. an AI segmentation) follows\n * the real edges of the image.\n */\nexport interface RefineEdgeParams {\n /** Edge-detection band radius in pixels (0 = no edge detection). */\n radius?: number;\n /** Narrow the band where the image edge is sharp (hair stays soft, hard edges stay hard). */\n smartRadius?: boolean;\n /** 0..100: rounds jagged edges. */\n smooth?: number;\n /** Pixels of Gaussian softening. */\n feather?: number;\n /** 0..100: hardens soft edges. */\n contrast?: number;\n /** −100..100: moves the edge out (positive) or in; 100 % is `max(radius, 4)` pixels. */\n shiftEdge?: number;\n}\n\nexport const DEFAULT_REFINE_EDGE_PARAMS: Required<RefineEdgeParams> = {\n radius: 0,\n smartRadius: false,\n smooth: 0,\n feather: 0,\n contrast: 0,\n shiftEdge: 0,\n};\n\n/** Guided-filter regulariser (colour guide, samples in 0..1). */\nexport const GUIDED_FILTER_EPS = 1e-4;\n\nconst fin = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\n/** Clamped, complete parameters. */\nexport function normalizeRefineEdgeParams(p: RefineEdgeParams = {}): Required<RefineEdgeParams> {\n return {\n radius: Math.min(250, Math.max(0, fin(p.radius, 0))),\n smartRadius: p.smartRadius === true,\n smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),\n feather: Math.min(1000, Math.max(0, fin(p.feather, 0))),\n contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),\n shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0))),\n };\n}\n\nconst gfRadius = (p: Required<RefineEdgeParams>) => Math.min(64, Math.max(1, Math.ceil(p.radius)));\nconst smoothSigma = (p: Required<RefineEdgeParams>) => (p.smooth / 100) * 4;\nconst featherSigma = (p: Required<RefineEdgeParams>) => p.feather / 2;\nfunction shiftAmount(p: Required<RefineEdgeParams>): number {\n const px = (p.shiftEdge / 100) * Math.max(p.radius, 4);\n return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;\n}\n\n/** Pixels around the mask's content that refinement can read or write. */\nexport function refineEdgeHalo(params: RefineEdgeParams = {}): number {\n const p = normalizeRefineEdgeParams(params);\n const g = gfRadius(p);\n const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;\n return (\n edge +\n Math.ceil(3 * smoothSigma(p)) +\n Math.ceil(3 * featherSigma(p)) +\n Math.ceil(Math.abs(shiftAmount(p))) +\n 2\n );\n}\n\n/** Box mean of radius `r` (window clipped to the image, divided by its pixel count). O(1) per pixel. */\nexport function boxMean(src: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const tmp = new Float32Array(w * h);\n const pre = new Float64Array(Math.max(w, h) + 1);\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];\n for (let x = 0; x < w; x++) {\n const lo = Math.max(0, x - r);\n const hi = Math.min(w, x + r + 1);\n tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n const out = new Float32Array(w * h);\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];\n for (let y = 0; y < h; y++) {\n const lo = Math.max(0, y - r);\n const hi = Math.min(h, y + r + 1);\n out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n return out;\n}\n\n/**\n * Guided filter with a colour guide (He et al., Eq. 19–21: `a_k = (Σ_k + εU)⁻¹ cov_k(I, p)`):\n * filters `p` so that it is locally a linear function of the guide's colour, which makes the\n * output's edges follow the guide's edges. `guide` is three planes of 0..1 values.\n */\nexport function guidedFilterColor(\n guide: readonly [Float32Array, Float32Array, Float32Array],\n p: Float32Array,\n w: number,\n h: number,\n r: number,\n eps = GUIDED_FILTER_EPS\n): Float32Array {\n const n = w * h;\n const [c0, c1, c2] = guide;\n const prod = (a: ArrayLike<number>, b: ArrayLike<number>) => {\n const o = new Float32Array(n);\n for (let i = 0; i < n; i++) o[i] = a[i] * b[i];\n return o;\n };\n const m0 = boxMean(c0, w, h, r);\n const m1 = boxMean(c1, w, h, r);\n const m2 = boxMean(c2, w, h, r);\n const mp = boxMean(p, w, h, r);\n const mip0 = boxMean(prod(c0, p), w, h, r);\n const mip1 = boxMean(prod(c1, p), w, h, r);\n const mip2 = boxMean(prod(c2, p), w, h, r);\n const s00 = boxMean(prod(c0, c0), w, h, r);\n const s01 = boxMean(prod(c0, c1), w, h, r);\n const s02 = boxMean(prod(c0, c2), w, h, r);\n const s11 = boxMean(prod(c1, c1), w, h, r);\n const s12 = boxMean(prod(c1, c2), w, h, r);\n const s22 = boxMean(prod(c2, c2), w, h, r);\n const a0 = new Float32Array(n);\n const a1 = new Float32Array(n);\n const a2 = new Float32Array(n);\n const b = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];\n const rr = s00[i] - u0 * u0 + eps;\n const rg = s01[i] - u0 * u1;\n const rb = s02[i] - u0 * u2;\n const gg = s11[i] - u1 * u1 + eps;\n const gb = s12[i] - u1 * u2;\n const bb = s22[i] - u2 * u2 + eps;\n const v0 = mip0[i] - u0 * up;\n const v1 = mip1[i] - u1 * up;\n const v2 = mip2[i] - u2 * up;\n // Inverse of the symmetric 3×3 matrix.\n const i00 = gg * bb - gb * gb;\n const i01 = gb * rb - rg * bb;\n const i02 = rg * gb - gg * rb;\n const i11 = rr * bb - rb * rb;\n const i12 = rb * rg - rr * gb;\n const i22 = rr * gg - rg * rg;\n const det = rr * i00 + rg * i01 + rb * i02;\n let x0 = 0, x1 = 0, x2 = 0;\n if (Math.abs(det) >= 1e-30) {\n x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;\n x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;\n x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;\n }\n a0[i] = x0;\n a1[i] = x1;\n a2[i] = x2;\n b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;\n }\n const ma0 = boxMean(a0, w, h, r);\n const ma1 = boxMean(a1, w, h, r);\n const ma2 = boxMean(a2, w, h, r);\n const mb = boxMean(b, w, h, r);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];\n return out;\n}\n\n/**\n * Per-pixel transition width (pixels) of the guide's luminance: local range over local maximum\n * gradient in a window of radius `r` (a sharp step gives about 2, a ramp of width L about L).\n */\nexport function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array {\n const n = w * h;\n const y = new Float32Array(n);\n for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];\n const g = new Float32Array(n);\n for (let yy = 0; yy < h; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;\n const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;\n g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);\n }\n }\n const gm = greyMorphology(g, w, h, r, true);\n const hi = greyMorphology(y, w, h, r, true);\n const lo = greyMorphology(y, w, h, r, false);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);\n return out;\n}\n\n/**\n * Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when\n * `radius > 0`): edge-aware re-estimation in a band around the edge, then smooth, feather,\n * contrast and shift edge, in that order.\n */\nexport function refineCoverage(\n guide: readonly [Float32Array, Float32Array, Float32Array] | null,\n m: Float32Array,\n w: number,\n h: number,\n params: RefineEdgeParams = {}\n): Float32Array {\n const p = normalizeRefineEdgeParams(params);\n let a: Float32Array = Float32Array.from(m);\n if (p.radius > 0 && guide) {\n const n = w * h;\n const boundary = new Uint8Array(n);\n let any = false;\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = y * w + x;\n const v = m[i] >= 0.5;\n const diff =\n (x > 0 && m[i - 1] >= 0.5 !== v) ||\n (x + 1 < w && m[i + 1] >= 0.5 !== v) ||\n (y > 0 && m[i - w] >= 0.5 !== v) ||\n (y + 1 < h && m[i + w] >= 0.5 !== v);\n if (diff) {\n boundary[i] = 1;\n any = true;\n }\n }\n }\n if (any) {\n const dist = distanceTransform(boundary, w, h);\n const r = gfRadius(p);\n const q = guidedFilterColor(guide, m, w, h, r);\n const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;\n for (let i = 0; i < n; i++) {\n const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;\n const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));\n if (wgt > 0) {\n const qi = Math.min(1, Math.max(0, q[i]));\n a[i] = m[i] + (qi - m[i]) * wgt;\n }\n }\n }\n }\n const ss = smoothSigma(p);\n if (ss >= 0.3) {\n a = gaussianBlur(a, w, h, ss, 'clamp');\n const gain = 1 + ss;\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n a = gaussianBlur(a, w, h, featherSigma(p), 'clamp');\n if (p.contrast > 0) {\n const gain = 1 / (1 - (0.99 * p.contrast) / 100);\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n const amount = shiftAmount(p);\n if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);\n return a;\n}\n\n/** Three 0..1 planes of a window of an RGBA buffer. */\nexport function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array] {\n const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };\n const n = box.width * box.height;\n const r = new Float32Array(n);\n const g = new Float32Array(n);\n const b = new Float32Array(n);\n const d = pixels.data;\n for (let y = 0; y < box.height; y++) {\n for (let x = 0; x < box.width; x++) {\n const i = y * box.width + x;\n const p = ((box.y + y) * pixels.width + box.x + x) * 4;\n r[i] = d[p] / 255;\n g[i] = d[p + 1] / 255;\n b[i] = d[p + 2] / 255;\n }\n }\n return [r, g, b];\n}\n\n/**\n * Refine Edge on a selection (or a coarse AI mask loaded as one) against `pixels` (the image,\n * same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small\n * selection on a large image stays cheap.\n */\nexport function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params: RefineEdgeParams = {}): SelectionMask {\n const p = normalizeRefineEdgeParams(params);\n if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {\n throw new Error('Refine Edge needs pixels the size of the selection');\n }\n if (!mask.bounds) return mask;\n const halo = refineEdgeHalo(p);\n // The guided filter reads 2r beyond the band; the window must hold that too.\n const x0 = Math.max(0, mask.bounds.x - halo);\n const y0 = Math.max(0, mask.bounds.y - halo);\n const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);\n const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);\n const ww = x1 - x0;\n const wh = y1 - y0;\n const m = new Float32Array(ww * wh);\n for (let y = 0; y < wh; y++) {\n const src = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;\n }\n const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;\n const refined = refineCoverage(guide, m, ww, wh, p);\n const out = new Uint8Array(mask.width * mask.height);\n for (let y = 0; y < wh; y++) {\n const dst = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);\n }\n return createSelectionMask(mask.width, mask.height, out);\n}\n","// Portions ported from photocraft/crates/engine/src/transform_cmds.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Free Transform sessions: what a transform moves (a layer's pixels with its linked mask, a lone\n * unlinked mask, the selected pixels as a floating piece, a copy of them on a new layer, or just\n * the selection outline), a live preview while the geometry changes, and one undo step when it is\n * committed with full-quality resampling.\n */\n\nimport {\n TiledSurface,\n contentBounds,\n readRegion,\n surfaceFromPixels,\n writeRegion,\n type RasterRect,\n} from '../raster/surface';\nimport {\n compositeSurfaceOver,\n reduceSurface,\n transformSurface,\n TransformTooLargeError,\n warpSurface,\n isInterpolation,\n type Interpolation,\n type ResampleOptions,\n} from '../raster/transform';\nimport { multiplyHomographies, type Homography } from '../geom/homography';\nimport { findLayer, insertLayer, mapLayer, newLayerId, type ImageLayer, type LayerTarget, type PixelLayer, type RasterMask, type SmartObjectLayer } from './layers/model';\nimport { LayerOperationError, requireLayer, requireUnlocked } from './layers/ops';\nimport { contentsBox, isContentsDecoded, placementGeometry, renderSmartObject, setSmartPlacement } from './layers/smart';\nimport { createSelectionMask, type SelectionMask } from './selection/mask';\nimport {\n geometryHomography,\n hasWarp,\n identityTransform,\n isIdentityTransform,\n transformMap,\n type FreeTransformGeometry,\n} from './transform';\nimport type { CropBox } from './types';\n\n/**\n * What a Free Transform moves:\n * - `layer`: the layer's pixels with its linked mask (or, with `channel: 'mask'` on an unlinked\n * mask, the mask alone);\n * - `selection`: the selected pixels (and the same part of a linked mask), lifted and committed\n * back into the layer; the selection outline moves with them;\n * - `selection-copy`: Layer via Copy, then the copy is transformed on a new layer above;\n * - `selection-outline`: Transform Selection, only the outline;\n * - `auto` (default): `selection` when something is selected, else `layer`.\n */\nexport type FreeTransformContent = 'auto' | 'layer' | 'selection' | 'selection-copy' | 'selection-outline';\n\nexport const FREE_TRANSFORM_CONTENTS: readonly FreeTransformContent[] = ['auto', 'layer', 'selection', 'selection-copy', 'selection-outline'];\n\nexport interface FreeTransformInit {\n /** The layer (default the active one); not needed for `selection-outline`. */\n layerId?: string;\n content?: FreeTransformContent;\n /**\n * `pixels` (default) moves the pixels with a linked mask; `mask` moves an unlinked mask alone (a\n * linked one moves with its pixels). Defaults to the active target on the active layer.\n */\n channel?: LayerTarget;\n /** Resampling of the committed result (default bicubic). */\n interpolation?: Interpolation;\n}\n\n/** How a live preview is drawn (the UI knows these, the controller doesn't). */\nexport interface FreeTransformPreview {\n /** Screen pixels per document pixel: below 1 the preview samples a reduced copy in blocks. */\n scale?: number;\n /** Only this part of the document (the visible area) is resampled. */\n clip?: CropBox;\n}\n\n/** What a committed transform changed. */\nexport interface FreeTransformResult {\n /** The layer that holds the result (the new layer for `selection-copy`; `null` for `selection-outline`). */\n layerId: string | null;\n content: Exclude<FreeTransformContent, 'auto'>;\n /** What moved. */\n moved: Array<'pixels' | 'mask' | 'selection'>;\n /** The frame the transform started from (document pixels). */\n box: CropBox;\n /** Where the frame's corners went. */\n corners: FreeTransformGeometry['corners'];\n /** Document bounds of the moved content after the transform (pixels, or the mask or the outline). */\n bounds: CropBox | null;\n /** False when the geometry was the identity and nothing was recorded. */\n changed: boolean;\n}\n\n/** @internal What the session needs from the controller. */\nexport interface FreeTransformHost {\n layers(): ImageLayer[];\n activeLayerId(): string | null;\n activeTarget(): LayerTarget;\n selection(): SelectionMask | null;\n canvas(): { width: number; height: number };\n /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */\n pixelsOf(layer: PixelLayer | SmartObjectLayer): TiledSurface;\n /** Opaque pre-transform snapshot. */\n capture(): unknown;\n /** Shows a live state (no history). */\n show(layers: ImageLayer[], selection: SelectionMask | null, activeLayerId: string | null): void;\n /** Records `before` as the undo step of the state now shown. */\n record(before: unknown): void;\n /** Puts `before` back (no history). */\n restore(before: unknown): void;\n}\n\n/** A surface that moves: in layer space (`offset` to document), composited back or replacing. */\ninterface Part {\n what: 'pixels' | 'mask' | 'selection';\n offset: { x: number; y: number };\n /** RGBA (pixels, or a mask part as grey + alpha) or one channel (a whole mask, the outline). */\n moving: TiledSurface;\n /** What stays: composited under the moved part (`null`: the moved part replaces the surface). */\n base: TiledSurface | null;\n /** Reduced copies for previews, by factor. */\n reduced: Map<number, TiledSurface>;\n}\n\nconst fail = (message: string): never => {\n throw new LayerOperationError(message);\n};\n\nconst T = (x: number, y: number): Homography => [1, 0, x, 0, 1, y, 0, 0, 1];\n\n/** Preview block size for a screen scale: 1 at 100 % and above, then powers of two up to 8. */\nexport function previewStep(scale: number | undefined): number {\n if (!scale || !(scale > 0) || scale >= 1) return 1;\n return Math.min(8, 2 ** Math.floor(Math.log2(1 / scale)));\n}\n\nfunction toDoc(r: RasterRect | null, o: { x: number; y: number }): CropBox | null {\n return r ? { x: r.x + o.x, y: r.y + o.y, width: r.width, height: r.height } : null;\n}\n\nfunction selectionSurface(sel: SelectionMask): TiledSurface {\n return surfaceFromPixels({ width: sel.width, height: sel.height, data: sel.data }, 1, 0, 0, 0);\n}\n\n/**\n * Lifts the selected part of an RGBA surface (layer space): the lifted pixels (alpha × coverage)\n * and the rest (alpha × (1 − coverage); fully lifted pixels become transparent black).\n */\nfunction liftPixels(surface: TiledSurface, sel: SelectionMask, o: { x: number; y: number }): { lifted: TiledSurface; rest: TiledSurface } {\n const sb = sel.bounds!;\n const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, area);\n const kept = new Uint8ClampedArray(px);\n for (let y = 0; y < area.height; y++) {\n for (let x = 0; x < area.width; x++) {\n const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;\n const i = (y * area.width + x) * 4;\n if (k <= 0) {\n px[i] = px[i + 1] = px[i + 2] = px[i + 3] = 0;\n continue;\n }\n px[i + 3] = px[i + 3] * k;\n if (!px[i + 3]) px[i] = px[i + 1] = px[i + 2] = 0;\n kept[i + 3] = kept[i + 3] * (1 - k);\n if (!kept[i + 3]) kept[i] = kept[i + 1] = kept[i + 2] = 0;\n }\n }\n return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: px }, 4, area.x, area.y), rest: writeRegion(surface, area, kept) };\n}\n\n/**\n * Lifts the selected part of a mask (layer space): the values as grey + alpha (alpha = coverage)\n * and the rest (the selected values replaced by the default). Everything selected moves,\n * default-valued parts included, so the mask stays aligned with the moved pixels.\n */\nfunction liftMask(mask: TiledSurface, sel: SelectionMask, o: { x: number; y: number }): { lifted: TiledSurface; rest: TiledSurface } {\n const sb = sel.bounds!;\n const area = { x: sb.x - o.x, y: sb.y - o.y, width: sb.width, height: sb.height };\n const v = readRegion(mask, area);\n const def = mask.defaultValue;\n const lifted = new Uint8ClampedArray(area.width * area.height * 4);\n const rest = new Uint8ClampedArray(v.length);\n for (let y = 0; y < area.height; y++) {\n for (let x = 0; x < area.width; x++) {\n const i = y * area.width + x;\n const k = sel.data[(sb.y + y) * sel.width + sb.x + x] / 255;\n const g = v[i];\n if (k > 0) {\n lifted[i * 4] = lifted[i * 4 + 1] = lifted[i * 4 + 2] = g;\n lifted[i * 4 + 3] = k * 255;\n }\n rest[i] = g * (1 - k) + def * k;\n }\n }\n return { lifted: surfaceFromPixels({ width: area.width, height: area.height, data: lifted }, 4, area.x, area.y), rest: writeRegion(mask, area, rest) };\n}\n\n/**\n * A Free Transform in progress (see `ImageEditorController.beginFreeTransform`). Change the\n * geometry with `update` (each change shows a live preview without history), then `commit()`\n * (one undo step, resampled at full quality) or `cancel()` (everything back as it was).\n */\nexport class FreeTransformSession {\n readonly content: Exclude<FreeTransformContent, 'auto'>;\n /** The layer being transformed (`null` for `selection-outline`). */\n readonly layerId: string | null;\n /** The new layer of a `selection-copy`. */\n readonly copyId: string | null = null;\n /** The frame (document pixels). */\n readonly box: CropBox;\n /** What moves. */\n readonly moves: ReadonlyArray<'pixels' | 'mask' | 'selection'>;\n /**\n * The geometry the transform starts from: the identity over `box`, or for a smart object its\n * placement (`box` is then the contents' rectangle in contents pixels, and the transform edits\n * the placement without resampling anything twice).\n */\n readonly startGeometry: FreeTransformGeometry;\n /** True when the layer is a smart object (the transform sets its placement). */\n readonly smartObject: boolean = false;\n private geometryValue: FreeTransformGeometry;\n private readonly smart: SmartObjectLayer | null = null;\n private readonly smartReduced = new Map<number, TiledSurface>();\n private readonly startInterp: Interpolation;\n private smartMasksMoved = false;\n private interp: Interpolation;\n private readonly parts: Part[] = [];\n private readonly before: unknown;\n private readonly startLayers: ImageLayer[];\n private readonly startSelection: SelectionMask | null;\n private readonly startActive: string | null;\n /** The stack this session last showed: anything else means the document changed under it. */\n private shown: ImageLayer[];\n private closed = false;\n\n constructor(\n private readonly host: FreeTransformHost,\n init: FreeTransformInit = {}\n ) {\n if (init.interpolation !== undefined && !isInterpolation(init.interpolation)) fail(`Unknown interpolation \"${String(init.interpolation)}\"; use nearest, bilinear or bicubic.`);\n this.interp = init.interpolation ?? 'bicubic';\n const sel = host.selection();\n const selected = Boolean(sel?.bounds);\n let content = init.content ?? 'auto';\n if (!(['auto', 'layer', 'selection', 'selection-copy', 'selection-outline'] as const).includes(content)) {\n fail(`Unknown content \"${String(content)}\"; use auto, layer, selection, selection-copy or selection-outline.`);\n }\n const targetId = init.layerId ?? host.activeLayerId();\n const target = targetId && content !== 'selection-outline' ? findLayer(host.layers(), targetId)?.layer : undefined;\n const smartTarget = target?.type === 'smart' && (init.channel ?? (target.id === host.activeLayerId() ? host.activeTarget() : 'pixels')) !== 'mask';\n // A smart object transforms as a whole, selection or not (as in Photoshop).\n if (content === 'auto') content = selected && !smartTarget ? 'selection' : 'layer';\n this.content = content;\n if (content !== 'layer' && !selected) fail(`Nothing is selected; ${content} needs a selection. Select first, or use content layer.`);\n const layers = host.layers();\n this.startLayers = layers;\n this.startSelection = sel;\n this.startActive = host.activeLayerId();\n this.before = host.capture();\n const moves: Array<'pixels' | 'mask' | 'selection'> = [];\n let box: CropBox | null = null;\n\n if (content === 'selection-outline') {\n this.layerId = null;\n this.parts.push({ what: 'selection', offset: { x: 0, y: 0 }, moving: selectionSurface(sel!), base: null, reduced: new Map() });\n moves.push('selection');\n box = { ...sel!.bounds! };\n } else {\n const id = init.layerId ?? host.activeLayerId();\n if (!id) fail('No layer given and none is active; pass layerId (see list_layers).');\n const layer = requireLayer(layers, id!).layer;\n this.layerId = layer.id;\n const mask = layer.mask && !layer.mask.source ? layer.mask : undefined;\n const channel: LayerTarget = init.channel ?? (layer.id === host.activeLayerId() ? host.activeTarget() : 'pixels');\n if (channel !== 'pixels' && channel !== 'mask') fail(`Unknown channel \"${String(channel)}\"; use pixels or mask.`);\n const linked = mask?.linked !== false;\n const maskAlone = channel === 'mask' && (!linked || layer.type !== 'pixel');\n if (channel === 'mask' && !mask) fail(layer.mask?.source ? `Layer \"${layer.name || id}\" has a depth mask, which is generated, not transformed.` : `Layer \"${layer.name || id}\" has no mask to transform; add one with add_layer_mask, or transform its pixels.`);\n if (!maskAlone && layer.type !== 'pixel' && layer.type !== 'smart') fail(`Layer \"${layer.name || id}\" is a ${layer.type} layer; Free Transform works on pixel layers, smart objects, layer masks (channel mask) and selections.`);\n if (!layer.visible && content !== 'selection-copy') fail(`Layer \"${layer.name || id}\" is hidden; show it before you transform it.`);\n if (content !== 'selection-copy') {\n requireUnlocked(layer, 'position', 'transform it');\n if (!maskAlone) requireUnlocked(layer, 'pixels', 'transform it');\n }\n const o = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const pixels = layer.type === 'pixel' ? host.pixelsOf(layer) : null;\n if (layer.type === 'smart' && !maskAlone) {\n if (content !== 'layer') fail(`Layer \"${layer.name || id}\" is a smart object, which transforms as a whole; use content layer (or rasterize it to transform part of it).`);\n if (!isContentsDecoded(layer.contents)) fail(`The contents of \"${layer.name || id}\" are not decoded here, so it cannot be transformed; run prepareSmartObjects() first.`);\n this.smart = layer;\n this.smartObject = true;\n this.startGeometry = placementGeometry(layer);\n if (init.interpolation === undefined && layer.placement.interpolation) this.interp = layer.placement.interpolation;\n this.startInterp = this.interp;\n this.box = contentsBox(layer.contents);\n this.moves = mask && linked ? ['pixels', 'mask'] : ['pixels'];\n this.geometryValue = this.startGeometry;\n this.shown = this.startLayers;\n return;\n }\n if (maskAlone) {\n if (content === 'selection-copy') fail('Layer via Copy copies pixels; transform a mask with content layer or selection.');\n if (content === 'selection') {\n const { lifted, rest } = liftMask(mask!.surface, sel!, o);\n this.parts.push({ what: 'mask', offset: o, moving: lifted, base: rest, reduced: new Map() });\n box = toDoc(contentBounds(lifted), o);\n } else {\n this.parts.push({ what: 'mask', offset: o, moving: mask!.surface, base: null, reduced: new Map() });\n box = toDoc(contentBounds(mask!.surface), o);\n }\n moves.push('mask');\n } else if (content === 'layer') {\n this.parts.push({ what: 'pixels', offset: o, moving: pixels!, base: null, reduced: new Map() });\n moves.push('pixels');\n box = toDoc(contentBounds(pixels!), o);\n if (mask && linked) {\n this.parts.push({ what: 'mask', offset: o, moving: mask.surface, base: null, reduced: new Map() });\n moves.push('mask');\n if (!box) box = toDoc(contentBounds(mask.surface), o);\n }\n } else if (content === 'selection') {\n const { lifted, rest } = liftPixels(pixels!, sel!, o);\n this.parts.push({ what: 'pixels', offset: o, moving: lifted, base: rest, reduced: new Map() });\n moves.push('pixels');\n box = toDoc(contentBounds(lifted), o);\n if (mask && linked) {\n const m = liftMask(mask.surface, sel!, o);\n this.parts.push({ what: 'mask', offset: o, moving: m.lifted, base: m.rest, reduced: new Map() });\n moves.push('mask');\n }\n } else {\n // selection-copy: the copy goes on a new layer above, with the source layer's offset.\n const { lifted } = liftPixels(pixels!, sel!, o);\n box = toDoc(contentBounds(lifted), o);\n if (box) {\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: `${layer.name || 'Layer'} copy`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: lifted,\n offset: { ...o },\n };\n (this as { copyId: string | null }).copyId = copy.id;\n const loc = findLayer(layers, layer.id)!;\n this.startLayers = insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1);\n this.parts.push({ what: 'pixels', offset: o, moving: lifted, base: null, reduced: new Map() });\n moves.push('pixels');\n }\n }\n if (content !== 'layer') {\n this.parts.push({ what: 'selection', offset: { x: 0, y: 0 }, moving: selectionSurface(sel!), base: null, reduced: new Map() });\n moves.push('selection');\n }\n }\n if (!box && moves[0] === 'mask') fail('The mask holds nothing but its default value, so there is nothing to transform; paint it first, or transform the pixels (channel pixels).');\n if (!box) fail(content === 'layer' ? 'The layer has no pixels to transform.' : 'The selection holds no pixels of this layer; select some, or transform the outline (content selection-outline).');\n this.box = box!;\n this.moves = moves;\n this.startGeometry = identityTransform(box!);\n this.startInterp = this.interp;\n this.geometryValue = this.startGeometry;\n this.shown = this.startLayers;\n if (this.copyId) host.show(this.startLayers, sel, this.copyId);\n this.shown = host.layers();\n }\n\n /** The current geometry. */\n get geometry(): FreeTransformGeometry {\n return this.geometryValue;\n }\n\n get interpolation(): Interpolation {\n return this.interp;\n }\n\n get isOpen(): boolean {\n return !this.closed;\n }\n\n private check(): void {\n if (this.closed) fail('This transform has already been committed or cancelled; begin a new one.');\n if (this.host.layers() !== this.shown) {\n this.closed = true;\n fail('The document changed while the transform was open, so it was abandoned; begin a new one.');\n }\n }\n\n /**\n * Changes the geometry (`corners`, `warp`) or the interpolation and, unless `preview` is `false`,\n * shows the result live (no history). Returns the geometry.\n */\n update(changes: { corners?: FreeTransformGeometry['corners']; warp?: FreeTransformGeometry['warp']; interpolation?: Interpolation }, preview: FreeTransformPreview | false = {}): FreeTransformGeometry {\n this.check();\n if (changes.interpolation !== undefined) {\n if (!isInterpolation(changes.interpolation)) fail(`Unknown interpolation \"${String(changes.interpolation)}\"; use nearest, bilinear or bicubic.`);\n this.interp = changes.interpolation;\n }\n const next: FreeTransformGeometry = {\n box: this.box,\n corners: (changes.corners ?? this.geometryValue.corners).map((p) => ({ x: p.x, y: p.y })) as unknown as FreeTransformGeometry['corners'],\n warp: changes.warp === undefined ? this.geometryValue.warp ?? null : changes.warp,\n };\n if (next.corners.length !== 4 || !next.corners.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) fail('corners must be four finite points: top-left, top-right, bottom-right, bottom-left.');\n if (!transformMap(next)) fail('These corners fold the frame over itself or through the horizon; keep the quad convex.');\n this.geometryValue = next;\n if (preview !== false) this.render(preview);\n return next;\n }\n\n /** Resamples every part for the current geometry and shows it (or returns it, for commit). */\n private resample(final: boolean, preview: FreeTransformPreview = {}): { layers: ImageLayer[]; selection: SelectionMask | null } {\n if (this.smart) return { layers: this.resampleSmart(final, preview), selection: this.startSelection };\n const g = this.geometryValue;\n const identity = isIdentityTransform(g);\n const warp = hasWarp(g);\n const h = warp ? null : geometryHomography(g);\n const f = warp ? transformMap(g) : null;\n const step = final ? 1 : previewStep(preview.scale);\n const interpolation: Interpolation = final ? this.interp : this.interp === 'nearest' ? 'nearest' : 'bilinear';\n let layers = this.startLayers;\n let selection = this.startSelection;\n const targetId = this.copyId ?? this.layerId;\n for (const part of this.parts) {\n const o = part.offset;\n let moved: TiledSurface;\n if (identity) moved = part.moving;\n else {\n let src = part.moving;\n let k = 1;\n if (step > 1) {\n k = step;\n let r = part.reduced.get(k);\n if (!r) {\n r = reduceSurface(part.moving, k);\n part.reduced.set(k, r);\n }\n src = r;\n }\n const opts: ResampleOptions = { interpolation, step: step > 1 ? step : undefined };\n if (preview.clip && !final) opts.clip = { x: Math.floor(preview.clip.x - o.x), y: Math.floor(preview.clip.y - o.y), width: Math.ceil(preview.clip.width) + 1, height: Math.ceil(preview.clip.height) + 1 };\n try {\n if (f) {\n moved = warpSurface(src, (p) => {\n const q = f({ x: p.x * k + o.x, y: p.y * k + o.y });\n return q && { x: q.x - o.x, y: q.y - o.y };\n }, opts);\n } else {\n // Layer space → document → transform → layer space, from the reduced copy's pixels.\n const hl = multiplyHomographies(multiplyHomographies(multiplyHomographies([k, 0, 0, 0, k, 0, 0, 0, 1], T(o.x, o.y)), h!), T(-o.x, -o.y));\n moved = transformSurface(src, hl, opts);\n }\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;\n if (part.what === 'selection') {\n const { width, height } = this.host.canvas();\n const data = new Uint8Array(readRegion(result, { x: 0, y: 0, width, height }).buffer);\n const next = createSelectionMask(width, height, data);\n selection = next.bounds ? next : null;\n } else if (part.what === 'pixels') {\n layers = mapLayer(layers, targetId!, (l) => {\n const next = { ...(l as PixelLayer), surface: result };\n delete next.source;\n return next;\n });\n } else {\n layers = mapLayer(layers, targetId!, (l) => ({ ...l, mask: { ...(l.mask as RasterMask), surface: result } }) as ImageLayer);\n }\n }\n return { layers, selection };\n }\n\n /** A smart object re-rendered from its contents for the current geometry (previews leave the smart filters out). */\n private resampleSmart(final: boolean, preview: FreeTransformPreview): ImageLayer[] {\n const layer = this.smart!;\n const g = this.geometryValue;\n const placement = { corners: g.corners, warp: g.warp ?? null, interpolation: this.interp };\n const canvas = this.host.canvas();\n try {\n if (final) {\n const res = setSmartPlacement(this.startLayers, layer.id, placement, canvas);\n this.smartMasksMoved = res.masksMoved;\n return res.layers;\n }\n const step = previewStep(preview.scale);\n const surface = renderSmartObject({ ...layer, placement }, canvas, { step, clip: preview.clip, skipFilters: true, reduced: this.smartReduced });\n return mapLayer(this.startLayers, layer.id, (l) => ({ ...(l as SmartObjectLayer), placement, surface }));\n } catch (e) {\n if (e instanceof TransformTooLargeError) fail(e.message);\n throw e;\n }\n }\n\n /** Whether the geometry (or the interpolation of a smart object) differs from where it started. */\n private isChanged(): boolean {\n const g = this.geometryValue;\n if (!this.smart) return !isIdentityTransform(g);\n const a = this.startGeometry;\n const same = g.corners.every((c, i) => Math.abs(c.x - a.corners[i].x) < 1e-9 && Math.abs(c.y - a.corners[i].y) < 1e-9);\n const warp = (w: FreeTransformGeometry['warp'], box: CropBox) => (w && hasWarp({ box, corners: a.corners, warp: w }) ? JSON.stringify(w) : '');\n return !same || warp(g.warp, g.box) !== warp(a.warp, a.box) || this.interp !== this.startInterp;\n }\n\n private render(preview: FreeTransformPreview): void {\n const { layers, selection } = this.resample(false, preview);\n this.host.show(layers, selection, this.copyId ?? this.startActive);\n this.shown = this.host.layers();\n }\n\n /** Finishes the transform with full-quality resampling, as one undo step. */\n commit(): FreeTransformResult {\n this.check();\n const changed = this.isChanged() || Boolean(this.copyId);\n if (!changed) {\n this.closed = true;\n this.host.restore(this.before);\n return this.result(false, this.startLayers, this.startSelection);\n }\n const { layers, selection } = this.resample(true);\n this.closed = true;\n this.host.show(layers, selection, this.copyId ?? this.startActive);\n this.host.record(this.before);\n return this.result(true, layers, selection);\n }\n\n /** Abandons the transform and puts everything back (no history). */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.host.restore(this.before);\n }\n\n private result(changed: boolean, layers: ImageLayer[], selection: SelectionMask | null): FreeTransformResult {\n let bounds: CropBox | null = null;\n const id = this.copyId ?? this.layerId;\n const layer = id ? findLayer(layers, id)?.layer : undefined;\n if (this.moves.includes('pixels') && layer?.type === 'pixel') bounds = toDoc(contentBounds(this.host.pixelsOf(layer)), layer.offset);\n else if (this.moves.includes('pixels') && layer?.type === 'smart') bounds = contentBounds(layer.surface);\n else if (this.moves.includes('mask') && layer?.mask) bounds = toDoc(contentBounds(layer.mask.surface), layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 });\n else if (this.moves.includes('selection')) bounds = selection?.bounds ? { ...selection.bounds } : null;\n return {\n layerId: this.copyId ?? this.layerId,\n content: this.content,\n moved: this.smart ? this.moves.filter((m) => m !== 'mask' || this.smartMasksMoved) : [...this.moves],\n box: { ...this.box },\n corners: this.geometryValue.corners,\n bounds,\n changed,\n };\n }\n}\n","import {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentParamsMap,\n Annotation,\n CreateAnnotationPayload,\n AspectRatio,\n CropBox,\n DepthMaskOptions,\n ExportOptions,\n ImageAdjustments,\n ImageDimensions,\n ImageEditorFit,\n ImageEditorOptions,\n ImageEditorState,\n ImageEditorTool,\n ImageTransform,\n ImagePoint,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { ImageEditorRenderer, RenderOptions } from './renderer';\nimport { clampCropToImage, isQuarterTurnSwapped } from './geometry';\nimport { LATEST_PROCESS_VERSION, LEGACY_PROCESS_VERSION, normalizeProcessVersion } from './color';\nimport { createAdjustmentLayer, normalizeAdjustmentLayer, normalizeAdjustmentLayers } from './adjustment-layers';\nimport type { BlendMode } from './blend';\nimport type { ImageHistogram } from './histogram';\nimport { applyAdjustments } from './filters';\nimport {\n combineSelections,\n createSelectionMask,\n deserializeSelection,\n fullSelection,\n invertSelection as invertSelectionMask,\n resizeSelection,\n selectionFromPixels,\n serializeSelection,\n type PixelBuffer,\n type SelectionChannel,\n type SelectionMask,\n type SelectionMode,\n} from './selection/mask';\nimport { ellipseSelection, polygonSelection, rectSelection, type ShapeSelectionOptions } from './selection/shapes';\nimport { colorRange, magicWand, type ColorRangeOptions, type ColorRangeSample, type MagicWandOptions } from './selection/wand';\nimport { modifySelection as modifySelectionMask, type SelectionModifyOp } from './selection/modify';\nimport { refineSelection as refineSelectionMask, type RefineEdgeParams } from './selection/refine';\nimport type { LayerMaskProps } from './selection/layer-mask';\nimport type { AdjustmentLayerMask } from './types';\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES, DEFAULT_TRANSFORM_VALUES } from './layers/defaults';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isActiveFilter,\n mapLayer,\n newLayerId,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type GroupLayer,\n type LayerBlendMode,\n type ImageLayer,\n type LayerFilter,\n type LayerTarget,\n type PixelLayer,\n type RasterMask,\n type SmartContents,\n type SmartFilter,\n type SmartObjectLayer,\n type SmartPlacement,\n} from './layers/model';\nimport {\n addSmartFilter as addSmartFilterOp,\n contentsSurface,\n convertToSmartObject as convertToSmartObjectOp,\n copySmartObject as copySmartObjectOp,\n documentContents,\n instancesOf,\n isContentsDecoded,\n moveSmartFilter as moveSmartFilterOp,\n normalizeSmartFilter,\n offsetSmartObject,\n placementGeometry,\n rasterContents,\n rasterizeSmartObject as rasterizeSmartObjectOp,\n removeSmartFilter as removeSmartFilterOp,\n replaceContents as replaceContentsOp,\n requireSmartObject,\n setSmartFilterMask as setSmartFilterMaskOp,\n setSmartFiltersEnabled as setSmartFiltersEnabledOp,\n setSmartPlacement,\n updateSmartFilter as updateSmartFilterOp,\n withFilterMaskSurface,\n} from './layers/smart';\nimport {\n BACKGROUND_LAYER_ID,\n BACKGROUND_LAYER_NAME,\n BLUR_FILTER_ID,\n DEVELOP_FILTER_ID,\n adjustmentNodeFromLegacy,\n annotationLayerFor,\n annotationLayerName,\n depthRasterMask,\n developTarget,\n isDefaultDepthMask,\n layersFromLegacy,\n legacyMaskOf,\n legacyView,\n rasterMaskFromLegacy,\n type LayerStack,\n} from './layers/migrate';\nimport {\n LAYERED_SCHEMA_VERSION,\n deserializeLayers,\n deserializePatterns,\n serializePatterns,\n resolveExternalAssets,\n serializeLayers,\n serializeLayersExternal,\n type ImageEditorAssetStore,\n type SerializedLayeredImageEditorState,\n} from './layers/serialize';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport {\n SurfaceDraft,\n TILE,\n collectTiles,\n contentBounds,\n emptySurface,\n packRgba,\n readPixel,\n readRegion,\n solidSurface,\n surfaceFromPixels,\n tilesInRect,\n translateSurface,\n writeRegion,\n type PixelSurface,\n type Tile,\n type TiledSurface,\n type TileKey,\n TiledSurface as TiledSurfaceClass,\n finishTile,\n tileKey,\n} from '../raster/surface';\nimport {\n LayerOperationError,\n addLayerEffect as addLayerEffectOp,\n addLayerFilter as addLayerFilterOp,\n layerEffectsOf,\n removeLayerEffect as removeLayerEffectOp,\n setLayerEffects as setLayerEffectsOp,\n setLayerEffectsEnabled as setLayerEffectsEnabledOp,\n updateLayerEffect as updateLayerEffectOp,\n duplicateLayer as duplicateLayerOp,\n groupLayers as groupLayersOp,\n moveLayer as moveLayerOp,\n normalizeFilter,\n offsetLayer as offsetLayerOp,\n parseBlend,\n placeLayer,\n removeLayer as removeLayerOp,\n removeLayerFilter as removeLayerFilterOp,\n requireLayer,\n requireUnlocked,\n setLayerMask as setLayerMaskOp,\n setLayerProperties as setLayerPropertiesOp,\n ungroupLayer as ungroupLayerOp,\n unionRect,\n updateLayerFilter as updateLayerFilterOp,\n type LayerPlacement,\n type LayerProperties,\n} from './layers/ops';\nimport { coveringRect } from './layers/render';\nimport {\n CoverageMap,\n DEFAULT_BRUSH,\n DEFAULT_MIXER,\n EMPTY_MIXER_LOAD,\n MixerStroke,\n StrokeCoverage,\n applyStrokeToTile,\n cleanMixer,\n loadMixer,\n normalizeBrush,\n normalizeMixer,\n type BrushSettings,\n type MixerLoad,\n type MixerSettings,\n type MixerTarget,\n type StrokeApply,\n type StrokePoint,\n} from '../paint';\nimport { base64ToBytes, bytesToBase64, decodePng, encodePng, isPng, parseDataUrl } from '../raster/png';\nimport type { Interpolation } from '../raster/transform';\nimport { getAnnotationBounds } from './geometry';\nimport { importPsd, type PsdImportResult } from './psd/import';\nimport { exportPsd } from './psd/export';\nimport { FreeTransformSession, type FreeTransformHost, type FreeTransformInit, type FreeTransformResult } from './free-transform';\nimport {\n anchorPoint,\n applyParams,\n cornersFromMatrix,\n describeTransform,\n flipCorners,\n identityTransform,\n matrixFromParams,\n transformedBox,\n isValidWarp,\n presetWarp,\n type AffineMatrix,\n type Quad,\n type TransformParams,\n type WarpMesh,\n type WarpPreset,\n} from './transform';\nimport { WARP_STYLES } from '../geom/warp';\nimport { applyMatrix, multiplyMatrices, translateMatrix } from '../geom/affine';\nimport type { EffectPattern } from '../raster/effects/patterns';\nimport { DEFAULT_GLOBAL_LIGHT, effectsMargin, type GlobalLight, type LayerEffect, type LayerEffectKind, type LayerEffects } from '../raster/effects/model';\n\n/** What `getSmartObject` reports. */\nexport interface SmartObjectInfo {\n layerId: string;\n contents: {\n /** Shared by linked instances. */\n id: string;\n name: string;\n kind: 'raster' | 'document';\n /** Pixel size of the contents. */\n width: number;\n height: number;\n /** False for a file that could not be decoded here (the layer shows its stored pixels). */\n decoded: boolean;\n /** The type of the file the contents came from (`psb`, `png`, `jpg`, …), or `null`. */\n fileType: string | null;\n /** The external file of linked contents, or `null` for embedded ones. */\n linkedPath: string | null;\n /** Every smart object placed from these contents (this one included), bottom first. */\n instances: string[];\n /** Layers in a nested document (0 for raster contents). */\n layerCount: number;\n };\n placement: {\n /** Where the contents' corners land: top-left, top-right, bottom-right, bottom-left (document pixels). */\n corners: Quad;\n /** The warp mesh in contents pixels, or `null`. */\n warp: WarpMesh | null;\n interpolation: Interpolation;\n /** False for distort, perspective and warp (then only corners and bounds describe it). */\n affine: boolean;\n /** Percent of the contents' own size. */\n scaleX: number;\n scaleY: number;\n rotate: number;\n skewX: number;\n /** Where the contents' centre is (document pixels). */\n center: { x: number; y: number };\n /** Bounds of the placed outline. */\n bounds: CropBox | null;\n };\n smartFilters: {\n enabled: boolean;\n /** Bottom (applied first) to top. Photoshop filters we keep but do not draw have kind `psd`. */\n filters: Array<Record<string, unknown>>;\n mask: { enabled: boolean; linked: boolean; density: number; feather: number; bounds: CropBox | null; defaultValue: number } | null;\n };\n /** Document bounds of the rendered pixels. */\n bounds: CropBox | null;\n}\n\n/** The document set aside while smart object contents are open. */\ninterface SavedDocument {\n state: ImageEditorState;\n undo: HistoryEntry[];\n redo: HistoryEntry[];\n initial: HistoryEntry;\n element: CanvasImageSource | null;\n samplePixels: PixelBuffer | null;\n samplePixelsOverride: boolean;\n psdResolution: number | undefined;\n}\n\n/** A nested document's layers as `exportPsd` takes them (filters baked, smart objects and annotations as their pixels). */\nfunction prepareNestedForPsd(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n let next: ImageLayer = l;\n if (l.type === 'group') next = { ...l, children: prepareNestedForPsd(l.children) };\n else if (l.type === 'smart') next = rasterizeSmartObjectOp([l], l.id).layer;\n else if (l.type === 'pixel' && l.filters?.length) {\n const p: PixelLayer = { ...l };\n if (l.filters.some(isActiveFilter)) {\n const b = l.surface.bounds;\n if (b) {\n const r = { x: b.x + l.offset.x - 64, y: b.y + l.offset.y - 64, width: b.width + 128, height: b.height + 128 };\n const plain: PixelLayer = { ...l, visible: true, opacity: 1, blend: 'normal' };\n delete plain.mask;\n delete plain.clipped;\n delete plain.effects;\n delete plain.fillOpacity;\n const data = new LayerCompositor().composite([plain], r, { width: r.x + r.width, height: r.y + r.height });\n p.surface = surfaceFromPixels({ width: r.width, height: r.height, data }, 4, r.x - l.offset.x, r.y - l.offset.y);\n }\n }\n delete p.filters;\n next = p;\n }\n if (next.mask?.source) {\n next = { ...next } as ImageLayer;\n delete next.mask;\n }\n out.push(next);\n }\n return out;\n}\n\n/** Latest serialized schema version (`3`: layers). Documents with the old shape still write 1 or 2. */\nexport const IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;\n\n/** Changes to an adjustment layer; `params` is merged into the current parameters. */\nexport interface AdjustmentLayerUpdate<K extends AdjustmentKind = AdjustmentKind> {\n params?: Partial<AdjustmentParamsMap[K]>;\n enabled?: boolean;\n opacity?: number;\n blend?: BlendMode;\n name?: string;\n /** A layer mask, or `null` to remove it. */\n mask?: AdjustmentLayerMask | null;\n}\n\n/** Options shared by the selection commands. */\nexport interface SelectionCommandOptions {\n /** How the result combines with the current selection (default `replace`). */\n mode?: SelectionMode;\n /** Feather the new shape by this many pixels before combining (default 0). */\n feather?: number;\n}\n\n/** Where an AI result goes (see `applyInpaintedImage`, `applyBackgroundRemovedImage`). */\nexport interface AIResultOptions {\n /** `new-layer`: a new layer (inpainting) or a mask (background removal); `replace`: the Background's pixels. Default: by `isMultiLayer`. */\n target?: 'new-layer' | 'replace';\n}\n\n/** Where the selection tools read colours from. */\nexport interface SampleOptions {\n /**\n * `composite` (default): the image, as before (with several layers: the composite of the pixel\n * layers, or with `sampleMerged` of every layer); `active-layer`: only the active pixel layer.\n */\n sample?: 'composite' | 'active-layer';\n /** Sample the adjusted image (base adjustments and layers) instead of the source pixels. */\n sampleMerged?: boolean;\n /** Pixels to use instead of reading the source image (natural size, RGBA). */\n pixels?: PixelBuffer;\n}\n\n/** A new adjustment layer: its kind plus optional initial values and position. */\nexport interface AdjustmentLayerInit<K extends AdjustmentKind = AdjustmentKind> extends AdjustmentLayerUpdate<K> {\n kind: K;\n id?: string;\n /** Put the new layer directly above this layer (in its group). */\n aboveId?: string;\n /** Put the new layer at the top of this group. */\n parentId?: string;\n}\n\n/** A new pixel layer: empty, a solid colour over the canvas, or given pixels, plus its properties and position. */\nexport interface PixelLayerInit extends LayerPlacement {\n id?: string;\n name?: string;\n /** Fill the canvas with this colour (`#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` 0..255). */\n color?: string | number[];\n /** RGBA pixels placed at `x`, `y` (document pixels). */\n pixels?: PixelBuffer;\n x?: number;\n y?: number;\n visible?: boolean;\n /** 0..1 (default 1). */\n opacity?: number;\n fillOpacity?: number;\n blend?: BlendMode;\n}\n\n/** The paint tools' options (view state). */\nexport interface PaintOptions {\n brush: BrushSettings;\n /** `#rrggbb`: what the brush and Fill paint. */\n foreground: string;\n /** `#rrggbb`: what the eraser paints on transparency-locked layers and masks. */\n background: string;\n /** Mixer Brush options (wet, load, mix, load / clean each stroke, RGB or pigment mixing). */\n mixer: MixerSettings;\n /** What the Mixer Brush carries now: its reservoir, how much is left and the picked-up colour. */\n mixerLoad: MixerLoad;\n}\n\n/** Paint tools a stroke can use. */\nexport type StrokeTool = 'brush' | 'eraser' | 'mixer';\n\n/** A stroke: tool, target (default the active layer and target) and brush / colour overrides. */\nexport interface StrokeInit {\n tool?: StrokeTool;\n layerId?: string;\n channel?: LayerTarget;\n /** Overrides of the paint options' brush. */\n brush?: Partial<BrushSettings>;\n /**\n * Paint colour (default the foreground). Masks take its grey value. For the mixer, the colour\n * the reservoir is loaded with: given, it loads the brush whatever `loadEachStroke` says.\n */\n color?: string | number[];\n /** Overrides of the paint options' Mixer Brush settings (tool `mixer`). */\n mixer?: Partial<MixerSettings>;\n /** Eraser colour on locked transparency / masks (default the background). */\n background?: string | number[];\n /** Kept with the stroke for replays (v1 brushes have no jitter). */\n seed?: number;\n}\n\nexport interface FillOptions {\n layerId?: string;\n channel?: LayerTarget;\n /** Default the foreground colour. */\n color?: string | number[];\n /** 0..1 (default 1). */\n opacity?: number;\n blend?: BlendMode;\n /** Limit to the selection when there is one (default true). */\n useSelection?: boolean;\n}\n\n/** What a stroke, fill or clear changed. */\nexport interface PaintStrokeResult {\n layerId: string;\n channel: LayerTarget;\n /** Document rectangle that changed (null when nothing did). */\n bounds: CropBox | null;\n dabs: number;\n /** The Mixer Brush's load after the stroke (tool `mixer` only). */\n mixerLoad?: MixerLoad;\n}\n\n/** An opaque pre-stroke history snapshot. */\nexport type HistoryEntryHandle = { readonly __historyEntry: unique symbol };\n\n/** Composites `coverage` into the tiles of `base` under `rects` (document space), returning the new surface. */\nfunction paintCoverageInto(base: TiledSurface, coverage: CoverageMap, apply: StrokeApply, rects: CropBox[]): TiledSurface {\n const tiles = new Map(base.tiles);\n const done = new Set<TileKey>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - apply.offset.x, y: r.y - apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n if (done.has(key)) continue;\n done.add(key);\n const src = base.tiles.get(key)?.data ?? defaultTile(base);\n const data = applyStrokeToTile(coverage, src, base.channels, t.tx, t.ty, apply);\n const tile = finishTile(data, base.channels, base.defaultValue);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n }\n return new TiledSurfaceClass(base.channels, tiles, base.defaultValue);\n}\n\nfunction defaultTile(s: TiledSurface): Uint8ClampedArray {\n const d = new Uint8ClampedArray(TILE * TILE * s.channels);\n if (s.defaultValue) d.fill(s.defaultValue);\n return d;\n}\n\n/** How a Mixer Brush stroke starts (see `PaintStrokeSession`). */\nexport interface MixerStrokeInit {\n /** Makes the stroke over the session's live pixels. */\n create(pixels: MixerTarget): MixerStroke;\n /** Stores the brush load when the stroke ends and returns it (after any clean). */\n end(load: MixerLoad): MixerLoad;\n}\n\n/**\n * A stroke in progress (see `beginStroke`): `push` input points as they arrive, then `end` (one\n * undo step) or `cancel`. Brush and eraser strokes build up a coverage map composited over the\n * pre-stroke pixels; Mixer Brush strokes paint dab by dab into the live pixels, so each dab picks\n * up what the dabs before it laid down.\n */\nexport class PaintStrokeSession {\n private readonly before: HistoryEntryHandle;\n private readonly base: TiledSurface;\n private readonly live: Map<TileKey, Tile>;\n private readonly touched = new Set<TileKey>();\n private readonly stroke: StrokeCoverage | null;\n private readonly mixer: MixerStroke | null;\n private readonly mixerInit: MixerStrokeInit | null = null;\n /** Mixer tiles written since the last update: they get new tile objects so the compositor sees them change. */\n private readonly fresh = new Set<TileKey>();\n private last: StrokePoint | null = null;\n private closed = false;\n\n constructor(\n private readonly controller: ImageEditorController,\n readonly target: { id: string; channel: LayerTarget; surface: TiledSurface },\n stroke: StrokeCoverage | MixerStrokeInit,\n private readonly apply: StrokeApply\n ) {\n this.before = controller._captureForStroke();\n this.base = target.surface;\n this.live = new Map(this.base.tiles);\n if (stroke instanceof StrokeCoverage) {\n this.stroke = stroke;\n this.mixer = null;\n } else {\n this.stroke = null;\n this.mixer = stroke.create(this.mixerPixels());\n this.mixerInit = stroke;\n }\n }\n\n /** The live pixels a mixer stroke reads and writes; base tiles are copied on first write. */\n private mixerPixels(): MixerTarget {\n const a = this.apply;\n return {\n offset: a.offset,\n lockTransparency: a.lockTransparency,\n selection: a.selection,\n readTile: (tx, ty) => this.live.get(tileKey(tx, ty))?.data ?? (this.base.defaultValue ? defaultTile(this.base) : null),\n writeTile: (tx, ty) => {\n const key = tileKey(tx, ty);\n if (!this.touched.has(key)) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n this.live.set(key, { data: new Uint8ClampedArray(src), uniform: null });\n this.touched.add(key);\n }\n this.fresh.add(key);\n return this.live.get(key)!.data;\n },\n };\n }\n\n get layerId(): string {\n return this.target.id;\n }\n\n /** Adds input points (document pixels); the stack shows the stroke so far. */\n push(points: readonly StrokePoint[]): void {\n if (this.closed || !points.length) return;\n this.last = points[points.length - 1];\n this.update(this.mixer ? this.mixer.push(points) : this.stroke!.push(points));\n }\n\n private update(rects: CropBox[]): void {\n if (!rects.length) return;\n const ch = this.base.channels;\n if (this.mixer) {\n for (const key of this.fresh) this.live.set(key, { data: this.live.get(key)!.data, uniform: null });\n this.fresh.clear();\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n return;\n }\n const stroke = this.stroke!;\n // Per target tile, the union of the new dabs inside it: only that part is recomputed.\n const regions = new Map<TileKey, { tx: number; ty: number; r: CropBox }>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n const cur = regions.get(key);\n regions.set(key, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r)! : r });\n }\n }\n for (const [key, { tx, ty, r }] of regions) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n const prev = this.touched.has(key) ? this.live.get(key)?.data : undefined;\n this.touched.add(key);\n const data = applyStrokeToTile(stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});\n this.live.set(key, { data, uniform: null });\n }\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n }\n\n /** Finishes the stroke (one undo step) and returns what changed. */\n end(): PaintStrokeResult {\n if (this.closed) throw new LayerOperationError('This stroke has already ended.');\n const last = this.last ?? undefined;\n this.update(this.mixer ? this.mixer.finish(last) : this.stroke!.finish(last));\n this.closed = true;\n const ch = this.base.channels;\n const tiles = new Map(this.base.tiles);\n let changed = false;\n for (const key of this.touched) {\n const live = this.live.get(key)!;\n const tile = finishTile(live.data, ch, this.base.defaultValue);\n const prev = this.base.tiles.get(key);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n if (!sameTile(prev, tile)) changed = true;\n }\n const surface = changed ? new TiledSurfaceClass(ch, tiles, this.base.defaultValue) : null;\n const mixerLoad = this.mixer ? this.mixerInit!.end(this.mixer.load) : undefined;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);\n const b = this.mixer ? this.mixer.bounds : this.stroke!.coverage.bounds;\n const result: PaintStrokeResult = { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: (this.mixer ?? this.stroke!).dabCount };\n if (mixerLoad) result.mixerLoad = mixerLoad;\n return result;\n }\n\n /** Abandons the stroke and restores the layer. */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);\n }\n}\n\nfunction sameTile(a: Tile | undefined, b: Tile | null): boolean {\n if (!a || !b) return !a && !b;\n if (a === b) return true;\n if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;\n for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;\n return true;\n}\n\n/** Where a new annotation goes. */\nexport interface AnnotationPlacement {\n /** Put the annotation layer directly above this layer (default: the top of the stack). */\n aboveId?: string;\n}\n\n/** The image a history entry was recorded against. */\ninterface SourceSnapshot {\n sourceUrl: string | null;\n element: CanvasImageSource | null;\n dimensions: ImageDimensions;\n}\n\n/**\n * Undo/redo entry: the document as immutable objects. Layers, surfaces, tiles and selections are\n * shared with the other entries and the current state, so an entry costs about the tiles it changed.\n */\ninterface HistoryEntry {\n layers: ImageLayer[];\n activeLayerId: string | null;\n activeTarget: LayerTarget;\n crop: CropBox | null;\n transform: ImageTransform;\n outputOpacity: number;\n legacyOutputBlur?: number;\n globalLight?: GlobalLight;\n patterns?: EffectPattern[];\n selection: SelectionMask | null;\n source: SourceSnapshot;\n}\n\nexport const DEFAULT_ADJUSTMENTS: ImageAdjustments = DEFAULT_ADJUSTMENTS_VALUES;\n\n/**\n * Fills in defaults for stored adjustments. A stored object without `process` was saved before\n * process versions existed and keeps the legacy rendering.\n */\nexport function normalizeStoredAdjustments(stored: Partial<ImageAdjustments> | undefined | null): ImageAdjustments {\n if (!stored) return { ...DEFAULT_ADJUSTMENTS };\n const out: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS,\n ...stored,\n process: normalizeProcessVersion(stored.process),\n };\n // Layers are deep-copied and cleaned; an empty or missing list is left out entirely.\n const layers = normalizeAdjustmentLayers(stored.layers);\n if (layers.length) out.layers = layers;\n else delete out.layers;\n return out;\n}\n\nconst isLayered = (data: { version?: unknown } | null | undefined): data is SerializedLayeredImageEditorState =>\n Boolean(data && data.version === LAYERED_SCHEMA_VERSION && Array.isArray((data as { layers?: unknown }).layers));\n\n/**\n * Brings a stored editor state of any schema version to a clean state of the same family:\n * version 1 (and a missing version) has no adjustment layers, version 2 has them, and both stay\n * in the document-level schema so old states load and render exactly as before. Version 3\n * (layers) is returned with its document fields cleaned. To lift a v1 / v2 state into layers,\n * use {@link migrateToLayeredState}.\n */\nexport function migrateSerializedImageEditorState(\n data: Partial<SerializedImageEditorState> & { version?: number }\n): SerializedImageEditorState {\n if (isLayered(data)) {\n const out: SerializedLayeredImageEditorState = {\n ...data,\n crop: data.crop ? { ...data.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...data.transform },\n opacity: typeof data.opacity === 'number' ? Math.min(1, Math.max(0, data.opacity)) : 1,\n assets: { ...(data.assets ?? {}) },\n activeLayerId: typeof data.activeLayerId === 'string' ? data.activeLayerId : null,\n };\n return out;\n }\n const legacy = data as Partial<LegacySerializedImageEditorState>;\n const adjustments = normalizeStoredAdjustments(legacy.adjustments);\n return {\n version: adjustments.layers?.length ? 2 : 1,\n crop: legacy.crop ? { ...legacy.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...legacy.transform },\n adjustments: serializeAdjustments(adjustments),\n depthMask: {\n ...DEFAULT_DEPTH_MASK,\n ...legacy.depthMask,\n depthRange: [...(legacy.depthMask?.depthRange ?? DEFAULT_DEPTH_MASK.depthRange)] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(legacy.annotations ?? [])),\n ...(legacy.selection ? { selection: { ...legacy.selection } } : {}),\n };\n}\n\n/**\n * Lifts a stored state of any version into the layered schema (v3) for a canvas of\n * `width × height`: the source becomes the Background layer with the base sliders as its\n * `develop` filter and the depth mask as its parametric mask, adjustment layers go above it and\n * annotations on top (see `layersFromLegacy`).\n */\nexport function migrateToLayeredState(\n data: Partial<SerializedImageEditorState> & { version?: number },\n size: ImageDimensions\n): SerializedLayeredImageEditorState {\n const clean = migrateSerializedImageEditorState(data);\n if (isLayered(clean)) return clean;\n const stack = layersFromLegacy(clean, Math.round(size.width), Math.round(size.height));\n const { layers, assets } = serializeLayers(stack.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width: Math.round(size.width),\n height: Math.round(size.height),\n layers,\n assets,\n activeLayerId: null,\n crop: clean.crop,\n transform: clean.transform,\n opacity: stack.outputOpacity,\n };\n if (stack.legacyOutputBlur !== undefined) out.legacyOutputBlur = stack.legacyOutputBlur;\n if (clean.selection) out.selection = clean.selection;\n return out;\n}\n\nexport const DEFAULT_TRANSFORM: ImageTransform = DEFAULT_TRANSFORM_VALUES;\n\nexport const DEFAULT_DEPTH_MASK: DepthMaskOptions = DEFAULT_DEPTH_MASK_VALUES;\n\nconst DEVELOP_KEYS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/**\n * ImageEditorController manages the complete lifecycle, canvas rendering pipeline,\n * tool interaction states, undo/redo history, serialization, and image export.\n */\nexport class ImageEditorController {\n private state: ImageEditorState;\n private renderer: ImageEditorRenderer;\n private imageElement: CanvasImageSource | null = null;\n private listeners = new Set<(state: ImageEditorState) => void>();\n private histogramListeners = new Set<(histogram: ImageHistogram | null) => void>();\n private onStateChange?: (state: ImageEditorState) => void;\n\n // History stacks\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private maxHistoryDepth: number;\n private maxHistoryBytes: number;\n private isApplyingHistory = false;\n private initialSnapshot: HistoryEntry;\n /** Pixels the selection tools sample (host-provided or read back from the source). */\n private samplePixels: PixelBuffer | null = null;\n private samplePixelsOverride = false;\n /** Decoded source pixels (the Background's surface), by the buffer or element they came from. */\n private sourceSurfaces = new WeakMap<object, PixelSurface>();\n private viewKey: unknown[] = [];\n /** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */\n private compositor = new LayerCompositor();\n\n constructor(options: ImageEditorOptions = {}) {\n this.maxHistoryDepth = options.maxHistoryDepth ?? 50;\n this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;\n this.onStateChange = options.onStateChange;\n this.renderer = new ImageEditorRenderer();\n this.renderer.onPreviewHistogram = (histogram) => {\n this.histogramListeners.forEach((l) => l(histogram));\n };\n this.renderer.layerSources = {\n surfaceOf: (layer) => this.surfaceOf(layer),\n sourceIsOpaque: () => this.sourceIsOpaque(),\n version: () => `${this.sourceVersion()}:${this.patternsVersion()}`,\n globalLight: () => this.state.globalLight,\n pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null,\n };\n\n this.state = {\n sourceUrl: typeof options.image === 'string' ? options.image : null,\n imageDimensions: { width: 800, height: 600 },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM },\n adjustments: { ...DEFAULT_ADJUSTMENTS },\n depthMask: { ...DEFAULT_DEPTH_MASK },\n annotations: [],\n selectedAnnotationId: null,\n activeTool: 'select',\n zoom: 1,\n pan: { x: 0, y: 0 },\n isOriginalCompared: false,\n selection: null,\n quickMask: false,\n layers: [],\n activeLayerId: null,\n activeTarget: 'pixels',\n outputOpacity: 1,\n paint: { brush: { ...DEFAULT_BRUSH }, foreground: '#000000', background: '#ffffff', mixer: { ...DEFAULT_MIXER }, mixerLoad: EMPTY_MIXER_LOAD },\n fit: options.fit ?? 'margin',\n };\n\n if (options.image instanceof Object && 'width' in options.image) {\n this.imageElement = options.image as CanvasImageSource;\n this.state.sourceUrl = (options.image as any).src || null;\n this.state.imageDimensions = {\n width: (options.image as any).naturalWidth || (options.image as any).width || 800,\n height: (options.image as any).naturalHeight || (options.image as any).height || 600,\n };\n }\n\n this.applyStored(options.initialState ?? {});\n if (options.initialState?.selection) {\n try {\n this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));\n } catch {\n // A damaged selection is dropped; the rest of the state still loads.\n }\n }\n\n this.initialSnapshot = this.captureHistoryEntry();\n\n if (typeof options.image === 'string') {\n this.loadImage(options.image);\n }\n }\n\n // --- Image Loading ---\n\n /**\n * Loads a new source image and makes it the baseline that `reset()` returns to.\n * Does not record history; use `applyInpaintedImage` / `applyBackgroundRemovedImage`\n * for undoable source replacement.\n */\n public async loadImage(src: string | HTMLImageElement): Promise<void> {\n // Keep the URL visible synchronously (UI bindings compare against it while loading).\n this.abandonContents();\n if (typeof src === 'string') this.state.sourceUrl = src;\n const source = await this.resolveSource(src);\n const prev = this.state.imageDimensions;\n this.imageElement = source.element;\n this.state.sourceUrl = source.sourceUrl;\n this.state.imageDimensions = source.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (prev.width !== source.dimensions.width || prev.height !== source.dimensions.height) {\n // Masks lifted from v1 / v2 documents follow the image, as they did when they were resampled at render.\n this.state.layers = resizeLegacyMasks(this.state.layers, prev, source.dimensions);\n }\n this.dropMismatchedSelection();\n\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n }\n\n private async resolveSource(src: string | HTMLImageElement): Promise<SourceSnapshot> {\n if (typeof src === 'string') {\n try {\n const img = await this.renderer.imageCache.get(src);\n return {\n sourceUrl: src,\n element: img,\n dimensions: {\n width: img.naturalWidth || img.width || 800,\n height: img.naturalHeight || img.height || 600,\n },\n };\n } catch {\n // In SSR / non-DOM environments, retain current dimensions\n return { sourceUrl: src, element: null, dimensions: { ...this.state.imageDimensions } };\n }\n }\n return {\n sourceUrl: (src as any).src || null,\n element: src,\n dimensions: {\n width: src.naturalWidth || src.width || 800,\n height: src.naturalHeight || src.height || 600,\n },\n };\n }\n\n public getImageElement(): CanvasImageSource | null {\n return this.imageElement;\n }\n\n // --- State & Subscription ---\n\n public getState(): ImageEditorState {\n this.syncViews();\n return this.state;\n }\n\n public subscribe(listener: (state: ImageEditorState) => void): () => void {\n this.syncViews();\n this.listeners.add(listener);\n listener(this.state);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Where layer pixels changed since the last viewport render, when only strokes changed them\n * (the renderer then recomposites just those rectangles); `'all'` after any other change.\n */\n private pendingDirty: CropBox[] | 'all' = 'all';\n\n private notify(strokeRects?: CropBox[]): void {\n if (strokeRects && this.pendingDirty !== 'all') this.pendingDirty.push(...strokeRects);\n else if (!strokeRects) this.pendingDirty = 'all';\n this.syncViews();\n const currentState = { ...this.state };\n this.onStateChange?.(currentState);\n this.listeners.forEach((l) => l(currentState));\n }\n\n /** Recomputes the compatibility views (`adjustments`, `depthMask`, `annotations`) when the stack changed. */\n private syncViews(): void {\n const s = this.state;\n const key = [s.layers, s.outputOpacity, s.legacyOutputBlur, s.activeLayerId, s.imageDimensions.width, s.imageDimensions.height];\n if (key.length === this.viewKey.length && key.every((v, i) => v === this.viewKey[i])) return;\n this.viewKey = key;\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, s.activeLayerId);\n s.adjustments = view.adjustments;\n s.depthMask = view.depthMask;\n s.annotations = view.annotations;\n }\n\n private stack(): LayerStack {\n const out: LayerStack = { layers: this.state.layers, outputOpacity: this.state.outputOpacity };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n return out;\n }\n\n // --- History (Undo / Redo / Reset) ---\n\n private captureHistoryEntry(): HistoryEntry {\n const s = this.state;\n const entry: HistoryEntry = {\n layers: s.layers,\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n crop: s.crop ? { ...s.crop } : null,\n transform: { ...s.transform },\n outputOpacity: s.outputOpacity,\n selection: s.selection ?? null,\n source: {\n sourceUrl: s.sourceUrl,\n element: this.imageElement,\n dimensions: { ...s.imageDimensions },\n },\n };\n if (s.legacyOutputBlur !== undefined) entry.legacyOutputBlur = s.legacyOutputBlur;\n if (s.globalLight) entry.globalLight = { ...s.globalLight };\n if (s.patterns) entry.patterns = s.patterns;\n return entry;\n }\n\n private restoreHistoryEntry(entry: HistoryEntry): void {\n const s = this.state;\n s.layers = entry.layers;\n s.activeLayerId = entry.activeLayerId;\n s.activeTarget = entry.activeTarget;\n s.crop = entry.crop ? { ...entry.crop } : null;\n s.transform = { ...entry.transform };\n s.outputOpacity = entry.outputOpacity;\n if (entry.legacyOutputBlur !== undefined) s.legacyOutputBlur = entry.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n if (entry.globalLight) s.globalLight = { ...entry.globalLight };\n else delete s.globalLight;\n if (entry.patterns) s.patterns = entry.patterns;\n else delete s.patterns;\n s.selection = entry.selection;\n if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;\n this.imageElement = entry.source.element;\n s.sourceUrl = entry.source.sourceUrl;\n s.imageDimensions = { ...entry.source.dimensions };\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) {\n s.selectedAnnotationId = null;\n }\n }\n\n private recordHistory(): void {\n if (this.isApplyingHistory) return;\n this.undoStack.push(this.captureHistoryEntry());\n if (this.undoStack.length > this.maxHistoryDepth) {\n this.undoStack.shift();\n }\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n\n /**\n * Bytes of pixel tiles held only by undo / redo steps (not by the current state). The oldest\n * undo steps are dropped while it exceeds `maxHistoryBytes`.\n */\n public get historyBytes(): number {\n const live = new Set<Tile>();\n collectStateTiles(this.state.layers, live);\n const held = new Set<Tile>();\n for (const e of [...this.undoStack, ...this.redoStack]) collectStateTiles(e.layers, held);\n let n = 0;\n for (const t of held) if (!live.has(t)) n += t.data.byteLength;\n return n;\n }\n\n private trimHistoryBytes(): void {\n if (!(this.maxHistoryBytes > 0) || this.undoStack.length <= 1) return;\n while (this.undoStack.length > 1 && this.historyBytes > this.maxHistoryBytes) this.undoStack.shift();\n }\n\n public get canUndo(): boolean {\n return this.undoStack.length > 0;\n }\n\n public get canRedo(): boolean {\n return this.redoStack.length > 0;\n }\n\n public undo(): boolean {\n if (!this.canUndo) return false;\n this.isApplyingHistory = true;\n this.redoStack.push(this.captureHistoryEntry());\n\n const prev = this.undoStack.pop()!;\n this.restoreHistoryEntry(prev);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public redo(): boolean {\n if (!this.canRedo) return false;\n this.isApplyingHistory = true;\n this.undoStack.push(this.captureHistoryEntry());\n\n const next = this.redoStack.pop()!;\n this.restoreHistoryEntry(next);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public reset(): void {\n this.recordHistory();\n this.restoreHistoryEntry(this.initialSnapshot);\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.state.activeTool = 'select';\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n // --- Active Tool ---\n\n public setTool(tool: ImageEditorTool): void {\n if (this.state.activeTool === tool) return;\n this.state.activeTool = tool;\n if (tool !== 'select') {\n this.state.selectedAnnotationId = null;\n }\n // If switching to crop tool and no crop is defined, initialize crop to full image\n if (tool === 'crop' && !this.state.crop) {\n this.state.crop = {\n x: 0,\n y: 0,\n width: this.state.imageDimensions.width,\n height: this.state.imageDimensions.height,\n };\n }\n this.notify();\n }\n\n // --- Crop ---\n\n public setCrop(crop: CropBox | null, addToHistory = true): void {\n if (addToHistory) {\n this.recordHistory();\n }\n this.state.crop = crop;\n this.notify();\n }\n\n public applyCropPreset(ratio: AspectRatio): void {\n this.recordHistory();\n const { width, height } = this.state.imageDimensions;\n // `ratio` is the on-screen aspect; the crop box lives in natural image space.\n if (ratio && isQuarterTurnSwapped(this.state.transform)) {\n ratio = 1 / ratio;\n }\n if (!ratio) {\n // Freeform: full image\n this.state.crop = { x: 0, y: 0, width, height };\n } else {\n let targetW = width;\n let targetH = targetW / ratio;\n\n if (targetH > height) {\n targetH = height;\n targetW = targetH * ratio;\n }\n\n const x = Math.max(0, Math.round((width - targetW) / 2));\n const y = Math.max(0, Math.round((height - targetH) / 2));\n this.state.crop = {\n x,\n y,\n width: Math.round(targetW),\n height: Math.round(targetH),\n };\n }\n this.notify();\n }\n\n public resetCrop(): void {\n this.recordHistory();\n this.state.crop = null;\n this.notify();\n }\n\n // --- Transformations (Rotate / Flip) ---\n\n public rotate(degrees: number): void {\n this.recordHistory();\n const current = this.state.transform.rotate;\n this.state.transform = { ...this.state.transform, rotate: (current + degrees) % 360 };\n this.notify();\n }\n\n public setRotation(degrees: number): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, rotate: degrees % 360 };\n this.notify();\n }\n\n public flipHorizontal(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipH: !this.state.transform.flipH };\n this.notify();\n }\n\n public flipVertical(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipV: !this.state.transform.flipV };\n this.notify();\n }\n\n // --- Layers (read) ---\n\n /** The layer stack, bottom to top (immutable objects). */\n public getLayers(): ImageLayer[] {\n return this.state.layers;\n }\n\n public getLayer(id: string): ImageLayer | undefined {\n return findLayer(this.state.layers, id)?.layer;\n }\n\n /** The active layer, or `undefined`. */\n public getActiveLayer(): ImageLayer | undefined {\n return this.state.activeLayerId ? this.getLayer(this.state.activeLayerId) : undefined;\n }\n\n /**\n * Makes a layer the active one (or none) and chooses whether painting targets its pixels or\n * its mask. View state: no undo step. Returns false for an unknown id.\n */\n public setActiveLayer(id: string | null, target: LayerTarget = 'pixels'): boolean {\n if (id !== null && !this.getLayer(id)) return false;\n const layer = id ? this.getLayer(id) : undefined;\n const t: LayerTarget = target === 'mask' && layer?.mask ? 'mask' : target === 'filter-mask' && layer?.type === 'smart' && layer.smartFilters?.mask ? 'filter-mask' : 'pixels';\n if (this.state.activeLayerId === id && this.state.activeTarget === t) return true;\n this.state.activeLayerId = id;\n this.state.activeTarget = t;\n this.notify();\n return true;\n }\n\n /**\n * The pixels of a pixel layer, in document space (`x`, `y` is the top-left of the returned\n * buffer), or `null` for an unknown or empty layer. Resolves the Background's source pixels.\n */\n public getLayerPixels(id: string): (PixelBuffer & { x: number; y: number }) | null {\n const layer = this.getLayer(id);\n if (!layer || (layer.type !== 'pixel' && layer.type !== 'smart')) return null;\n const surface = this.surfaceOf(layer);\n const b = contentBounds(surface);\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height, data: readRegion(surface, b) };\n }\n\n private boundsCache = new WeakMap<object, CropBox | null>();\n\n /**\n * Document bounds of a layer's non-transparent pixels (pixel layers; the Background's source\n * included), or `null` for an empty or non-pixel layer.\n */\n public getLayerBounds(id: string): CropBox | null {\n const layer = this.getLayer(id);\n if (!layer || (layer.type !== 'pixel' && layer.type !== 'smart')) return null;\n const surface = this.surfaceOf(layer);\n let b = this.boundsCache.get(surface);\n if (b === undefined) {\n b = contentBounds(surface);\n this.boundsCache.set(surface, b);\n }\n return b ? { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height } : null;\n }\n\n /**\n * Composites the layer stack in document space without a canvas (annotation layers need one\n * and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and\n * output opacity are not applied. With `layerIds`, only those layers are composited.\n */\n public compositePixels(rect?: CropBox, options: { layerIds?: string[] } = {}): PixelBuffer {\n const { width, height } = this.naturalSize;\n const r = rect ?? { x: 0, y: 0, width, height };\n let layers = this.state.layers;\n if (options.layerIds) {\n const keep = new Set(options.layerIds);\n layers = flattenLayers(layers).filter((l) => keep.has(l.id) && l.type !== 'group');\n }\n const data = this.compositor.composite(layers, r, this.compositorSources());\n return { width: r.width, height: r.height, data };\n }\n\n private compositorSources(): CompositorSources {\n const { width, height } = this.naturalSize;\n return {\n width,\n height,\n surfaceOf: (l) => this.surfaceOf(l),\n version: `${this.sourceVersion()}:${this.patternsVersion()}`,\n globalLight: this.state.globalLight,\n pattern: (id) => this.state.patterns?.find((p) => p.id === id) ?? null,\n };\n }\n\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 1;\n\n /** Changes whenever the Background's source pixels can have changed. */\n private sourceVersion(): string {\n const key = (this.samplePixelsOverride ? this.samplePixels : this.imageElement ?? this.samplePixels) as object | null;\n if (!key) return '0';\n let id = this.sourceIds.get(key);\n if (!id) {\n id = this.nextSourceId++;\n this.sourceIds.set(key, id);\n }\n return String(id);\n }\n\n /** The pixels a pixel layer or smart object composites (the decoded source for the Background). */\n private surfaceOf(layer: PixelLayer | SmartObjectLayer): PixelSurface {\n if (layer.type === 'smart' || layer.source !== 'document') return layer.surface;\n return this.sourceSurface() ?? layer.surface;\n }\n\n /**\n * The source image as a surface (cached): host-supplied sample pixels first, then the decoded\n * element, then pixels read back for the selection tools.\n */\n private sourceSurface(): PixelSurface | null {\n const { width, height } = this.naturalSize;\n const fromPixels = (px: PixelBuffer) => {\n let s = this.sourceSurfaces.get(px as object);\n if (!s) {\n s = surfaceFromPixels(px, 4);\n this.sourceSurfaces.set(px as object, s);\n }\n return s;\n };\n const px = this.samplePixels;\n const fits = Boolean(px && px.width === width && px.height === height);\n if (this.samplePixelsOverride && fits) return fromPixels(px!);\n const el = this.imageElement as unknown as object | null;\n if (el) {\n const cached = this.sourceSurfaces.get(el);\n if (cached) return cached;\n const decoded = this.decodeSource();\n if (decoded) {\n if (!this.samplePixelsOverride) this.samplePixels = decoded;\n const s = surfaceFromPixels(decoded, 4);\n this.sourceSurfaces.set(el, s);\n this.sourceSurfaces.set(decoded as object, s);\n return s;\n }\n }\n return fits ? fromPixels(px!) : null;\n }\n\n private opaqueCache = new WeakMap<object, boolean>();\n\n /** Whether every source pixel is opaque (some legacy renders depend on it). */\n private sourceIsOpaque(): boolean {\n const s = this.sourceSurface();\n if (!s) return true;\n let v = this.opaqueCache.get(s);\n if (v === undefined) {\n const { width, height } = this.naturalSize;\n v = isOpaqueInside(s, width, height);\n this.opaqueCache.set(s, v);\n }\n return v;\n }\n\n /** Synchronously reads the source element's pixels through a canvas, or `null`. */\n private decodeSource(): PixelBuffer | null {\n const el = this.imageElement;\n if (!el) return null;\n const { width, height } = this.naturalSize;\n try {\n const canvas = this.renderer.createCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n ctx.drawImage(el, 0, 0, width, height);\n const data = ctx.getImageData(0, 0, width, height);\n if (!data?.data || data.data.length < width * height * 4) return null;\n return { width, height, data: data.data };\n } catch {\n return null;\n }\n }\n\n // --- Layer operations ---\n\n /** Records one undo step and applies a new stack (and any other state change in `extra`). */\n private commitLayers(next: ImageLayer[], extra?: () => void, commitToHistory = true): void {\n if (commitToHistory) this.recordHistory();\n this.state.layers = next;\n extra?.();\n if (this.state.activeLayerId && !findLayer(next, this.state.activeLayerId)) {\n this.state.activeLayerId = null;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n }\n\n /** The default position of a new layer: above the active layer, else the top of the stack. */\n private newLayerPlacement(placement: LayerPlacement): LayerPlacement {\n if (placement.aboveId || placement.belowId || placement.parentId || placement.index !== undefined) return placement;\n const active = this.state.activeLayerId ? findLayer(this.state.layers, this.state.activeLayerId) : null;\n return active ? { aboveId: active.layer.id } : {};\n }\n\n /**\n * Adds a pixel layer (one undo step) and makes it active: empty, filled with `color` over the\n * canvas, or holding `pixels` placed at `x`, `y`. Goes above the active layer unless placed.\n * Returns its id.\n */\n public addPixelLayer(init: PixelLayerInit = {}): string {\n const { width, height } = this.naturalSize;\n let surface = emptySurface(4);\n if (init.pixels) {\n const px = init.pixels;\n if (!(px.width > 0 && px.height > 0) || px.data.length < px.width * px.height * 4) {\n throw new LayerOperationError('pixels must be RGBA with width × height × 4 bytes.');\n }\n surface = surfaceFromPixels(px, 4);\n } else if (init.color !== undefined) {\n const c = parseColor(init.color);\n surface = solidSurface(4, { x: 0, y: 0, width, height }, packRgba(c[0], c[1], c[2], c[3]));\n }\n const blend = parseBlend(init.blend) as BlendMode | undefined;\n if (init.opacity !== undefined && !(init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer: PixelLayer = {\n type: 'pixel',\n id: init.id && !ids.has(init.id) ? init.id : newLayerId(),\n name: init.name?.trim() || this.nextLayerName(),\n visible: init.visible ?? true,\n opacity: init.opacity ?? 1,\n blend: blend ?? 'normal',\n surface,\n offset: { x: Math.round(init.x ?? 0), y: Math.round(init.y ?? 0) },\n };\n if (init.fillOpacity !== undefined && init.fillOpacity !== 1) layer.fillOpacity = init.fillOpacity;\n const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = layer.id;\n this.state.activeTarget = 'pixels';\n });\n return layer.id;\n }\n\n /**\n * `addPixelLayer` from an image: a PNG data URL or base64 PNG is decoded without a canvas;\n * other formats and URLs need a browser. Returns the new layer id.\n */\n public async addPixelLayerFromImage(image: string, init: Omit<PixelLayerInit, 'pixels' | 'color'> = {}): Promise<string> {\n const pixels = await this.decodeImage(image);\n return this.addPixelLayer({ ...init, pixels });\n }\n\n /** RGBA pixels of an image given as a data URL, base64 PNG or URL. */\n public async decodeImage(image: string): Promise<PixelBuffer> {\n const trimmed = image.trim();\n let png: Uint8Array | null = null;\n if (/^data:image\\/png/i.test(trimmed)) png = parseDataUrl(trimmed)?.bytes ?? null;\n else if (/^[A-Za-z0-9+/=\\s]+$/.test(trimmed) && trimmed.length > 16) {\n try {\n const bytes = base64ToBytes(trimmed);\n if (isPng(bytes)) png = bytes;\n } catch {\n png = null;\n }\n }\n if (png) {\n try {\n const d = decodePng(png);\n return { width: d.width, height: d.height, data: d.data };\n } catch {\n // Fall back to the browser decoder below (interlaced PNGs).\n }\n }\n const src = png ? `data:image/png;base64,${bytesToBase64(png)}` : trimmed;\n let img: HTMLImageElement;\n try {\n img = await this.renderer.imageCache.get(src);\n } catch {\n throw new LayerOperationError('The image could not be decoded here. Pass a PNG (data URL or base64), or run in a browser for other formats and URLs.');\n }\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const ctx = this.renderer.createCanvas(w, h).getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') throw new LayerOperationError('Decoding this image needs a canvas; pass a PNG instead.');\n ctx.drawImage(img, 0, 0, w, h);\n const data = ctx.getImageData(0, 0, w, h);\n return { width: w, height: h, data: data.data };\n }\n\n private nextLayerName(): string {\n const names = new Set(flattenLayers(this.state.layers).map((l) => l.name));\n let n = 1;\n while (names.has(`Layer ${n}`)) n++;\n return `Layer ${n}`;\n }\n\n /** Duplicates a layer (with its children) directly above it, as one undo step. Returns the copy's id. */\n public duplicateLayer(id: string, name?: string): string {\n const res = duplicateLayerOp(this.state.layers, id, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Deletes a layer (refused when locked), as one undo step. */\n public removeLayer(id: string): void {\n const next = removeLayerOp(this.state.layers, id);\n this.commitLayers(next, () => {\n if (this.state.selectedAnnotationId && !flattenLayers(next).some((l) => l.type === 'annotation' && l.annotation.id === this.state.selectedAnnotationId)) {\n this.state.selectedAnnotationId = null;\n }\n });\n }\n\n /** Moves a layer above / below another, or into a group, as one undo step. Returns its new parent and index. */\n public moveLayer(id: string, placement: LayerPlacement): { parentId: string | null; index: number } {\n const next = moveLayerOp(this.state.layers, id, placement);\n const loc = findLayer(next, id)!;\n if (next === this.state.layers) return { parentId: loc.parent?.id ?? null, index: loc.index };\n this.commitLayers(next);\n return { parentId: loc.parent?.id ?? null, index: loc.index };\n }\n\n /**\n * Changes name, visibility, opacity, fill, blend, clipping, locks or (groups) collapsed state, as\n * one undo step (`commitToHistory = false` for the moves of a slider drag). Returns the layer.\n */\n public setLayerProperties(id: string, props: LayerProperties, commitToHistory = true): ImageLayer {\n const next = setLayerPropertiesOp(this.state.layers, id, props);\n this.commitLayers(next, undefined, commitToHistory);\n return findLayer(next, id)!.layer;\n }\n\n /** Adds an empty pass-through group above the active layer (or at `placement`) and makes it active. Returns its id. */\n public addGroup(init: LayerPlacement & { name?: string; blend?: LayerBlendMode } = {}): string {\n const blend = init.blend === undefined ? 'pass-through' : (parseBlend(init.blend, true) as LayerBlendMode);\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name: init.name?.trim() || 'Group',\n visible: true,\n opacity: 1,\n blend,\n children: [],\n };\n const next = placeLayer(this.state.layers, group, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = group.id;\n this.state.activeTarget = 'pixels';\n });\n return group.id;\n }\n\n /** Puts sibling layers into a new group (one undo step). Returns the group id. */\n public groupLayers(ids: string[], name?: string): string {\n const res = groupLayersOp(this.state.layers, ids, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Replaces a group by its children (one undo step). Returns the child ids, bottom to top. */\n public ungroupLayer(id: string): string[] {\n const res = ungroupLayerOp(this.state.layers, id);\n this.commitLayers(res.layers);\n return res.ids;\n }\n\n /**\n * Moves a pixel layer to an absolute offset (`x`, `y`) or by `dx`, `dy`, as one undo step\n * (`commitToHistory = false` for the moves of a drag). Returns the offset and document bounds.\n */\n public offsetLayer(id: string, move: { x?: number; y?: number; dx?: number; dy?: number }, commitToHistory = true): { offset: { x: number; y: number }; bounds: CropBox | null } {\n const layer = requireLayer(this.state.layers, id).layer;\n if (layer.type === 'smart') {\n // A smart object moves through its placement: x, y is where its top-left corner goes.\n const tl = layer.placement.corners[0];\n const dx = move.x !== undefined ? move.x - tl.x : (move.dx ?? 0);\n const dy = move.y !== undefined ? move.y - tl.y : (move.dy ?? 0);\n if (!Number.isFinite(dx) || !Number.isFinite(dy)) throw new LayerOperationError('x and y must be numbers.');\n this.commitLayers(offsetSmartObject(this.state.layers, id, dx, dy, this.canvasSize), undefined, commitToHistory);\n const moved = findLayer(this.state.layers, id)!.layer as SmartObjectLayer;\n return { offset: { x: Math.round(moved.placement.corners[0].x), y: Math.round(moved.placement.corners[0].y) }, bounds: this.getLayerBounds(id) };\n }\n const current = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const x = move.x ?? current.x + (move.dx ?? 0);\n const y = move.y ?? current.y + (move.dy ?? 0);\n const next = offsetLayerOp(this.state.layers, id, { x, y });\n this.commitLayers(next, undefined, commitToHistory);\n return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };\n }\n\n /**\n * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black) or\n * `selection` (the selection's coverage). Replaces an existing mask. Makes the mask the paint\n * target when the layer is active.\n */\n public addLayerMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' = 'reveal-all', props: LayerMaskProps = {}): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n let mask: RasterMask;\n if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n if (layer.type === 'pixel' && (layer.offset.x || layer.offset.y)) {\n // Masks live in the layer's space (they move with it).\n mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };\n }\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const next = setLayerMaskOp(this.state.layers, id, mask);\n this.commitLayers(next, () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'mask';\n });\n return mask;\n }\n\n /** Removes a layer's mask (one undo step). */\n public removeLayerMask(id: string): void {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, null), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /** Changes any layer mask's density, feather, enabled or linked flag (pixels kept). Returns the mask. */\n public setLayerMaskProps(id: string, props: LayerMaskProps, commitToHistory = true): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n const mask = withMaskProps(layer.mask, props);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, mask), undefined, commitToHistory);\n return mask;\n }\n\n /**\n * Bakes a pixel layer's mask into its pixels (alpha × coverage, including density, feather or\n * the depth map) and removes the mask, as one undo step.\n */\n public async applyLayerMask(id: string): Promise<void> {\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Only a pixel layer's mask can be applied to its pixels; \"${id}\" is a ${layer.type} layer. Remove the mask or keep it.`);\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n requireUnlocked(layer, 'pixels', 'apply its mask');\n const { width, height } = this.naturalSize;\n await this.renderer.prepareLayerAssets([layer], width, height);\n const sources = this.renderer.compositorSources(width, height);\n const surface = this.surfaceOf(layer);\n let out = surface;\n const b = surface.bounds;\n if (b) {\n const draft = new SurfaceDraft(surface);\n for (const t of tilesInRect(b)) {\n const data = draft.peek(t.tx, t.ty);\n if (!data) continue;\n const r = { x: t.tx * TILE + layer.offset.x, y: t.ty * TILE + layer.offset.y, width: TILE, height: TILE };\n const cov = this.compositor.maskCoverage(layer, r, sources);\n if (!cov) continue;\n const d = draft.tile(t.tx, t.ty);\n for (let p = 0, i = 3; p < cov.length; p++, i += 4) d[i] = Math.round(d[i] * cov[p]);\n }\n out = draft.commit();\n }\n const next: PixelLayer = { ...layer, surface: out };\n delete next.mask;\n delete next.source;\n this.commitLayers(mapLayer(this.state.layers, id, () => next), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /**\n * Layer via Copy (or via Cut): a new pixel layer with the selected pixels of a pixel layer (the\n * active one by default), directly above it, as one undo step. Returns the new id.\n */\n public layerViaCopy(options: { layerId?: string; cut?: boolean; name?: string } = {}): string {\n const id = options.layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError('No layer given and none is active; pass layerId (see list_layers).');\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer via copy needs a pixel layer; \"${id}\" is a ${layer.type} layer.`);\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select the pixels to copy first.');\n if (options.cut) requireUnlocked(layer, 'pixels', 'cut from it');\n const surface = this.surfaceOf(layer);\n const sb = sel.bounds;\n const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, src);\n const kept = options.cut ? new Uint8ClampedArray(px) : null;\n for (let y = 0; y < sb.height; y++) {\n for (let x = 0; x < sb.width; x++) {\n const c = sel.data[(sb.y + y) * sel.width + sb.x + x];\n const i = (y * sb.width + x) * 4 + 3;\n if (kept) kept[i] = Math.round(kept[i] * (1 - c / 255));\n px[i] = Math.round(px[i] * (c / 255));\n }\n }\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: options.name?.trim() || `${layer.name || 'Layer'} ${options.cut ? 'cut' : 'copy'}`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width: src.width, height: src.height, data: px }, 4, src.x, src.y),\n offset: { ...layer.offset },\n };\n let next = insertLayer(this.state.layers, copy, loc.parent?.id ?? null, loc.index + 1);\n if (kept) {\n const cutLayer: PixelLayer = { ...layer, surface: writeRegion(surface, src, kept) };\n delete cutLayer.source;\n next = mapLayer(next, id, () => cutLayer);\n }\n this.commitLayers(next, () => {\n this.state.activeLayerId = copy.id;\n this.state.activeTarget = 'pixels';\n });\n return copy.id;\n }\n\n // --- Free Transform ---\n\n private openTransform: FreeTransformSession | null = null;\n\n /** The Free Transform in progress (see `beginFreeTransform`), or `null`. */\n public getFreeTransform(): FreeTransformSession | null {\n if (this.openTransform && !this.openTransform.isOpen) this.openTransform = null;\n return this.openTransform;\n }\n\n private transformHost(): FreeTransformHost {\n return {\n layers: () => this.state.layers,\n activeLayerId: () => this.state.activeLayerId,\n activeTarget: () => this.state.activeTarget,\n selection: () => (this.state.selection?.bounds ? this.state.selection : null),\n canvas: () => this.naturalSize,\n pixelsOf: (layer) => this.surfaceOf(layer),\n capture: () => this.captureHistoryEntry(),\n show: (layers, selection, active) => {\n this.state.layers = layers;\n this.state.selection = selection;\n if (active !== this.state.activeLayerId) {\n this.state.activeLayerId = active;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n },\n record: (before) => {\n if (this.isApplyingHistory) return;\n this.undoStack.push(before as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n this.notify();\n },\n restore: (before) => {\n const e = before as HistoryEntry;\n this.state.layers = e.layers;\n this.state.selection = e.selection;\n this.state.activeLayerId = e.activeLayerId;\n this.state.activeTarget = e.activeTarget;\n this.notify();\n },\n };\n }\n\n /**\n * Starts a Free Transform (Edit › Free Transform, Transform Selection, or a transform of a Layer\n * via Copy): of the layer's pixels with its linked mask, an unlinked mask alone, the selected\n * pixels or just the selection outline (see `FreeTransformInit`). Change the geometry with\n * `session.update` (live preview, no history), then `commit()` (one undo step) or `cancel()`.\n * Refused while another transform is open.\n */\n public beginFreeTransform(init: FreeTransformInit = {}): FreeTransformSession {\n if (this.getFreeTransform()) throw new LayerOperationError('A Free Transform is already open; commit or cancel it first.');\n const session = new FreeTransformSession(this.transformHost(), init);\n this.openTransform = session;\n this.notify();\n return session;\n }\n\n /**\n * Transforms in one step (agents, scripts): begins a Free Transform for `spec`, sets its corners\n * from exactly one of `matrix` (document-space affine), `corners` (where the frame's corners go:\n * distort / perspective) or `params` (scale, rotate, skew and move about a reference point), and\n * commits it as one undo step. Returns what changed and the new bounds.\n */\n public transformLayer(spec: FreeTransformInit & { matrix?: AffineMatrix; corners?: Quad; params?: TransformParams; warp?: WarpMesh | WarpPreset | null }): FreeTransformResult {\n const given = [spec.matrix, spec.corners, spec.params].filter((v) => v !== undefined).length;\n if (given > 1) throw new LayerOperationError('Give only one of matrix, corners and params.');\n if (!given && spec.warp === undefined) throw new LayerOperationError('Give matrix, corners or params (or a warp).');\n if (spec.matrix && !(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) {\n throw new LayerOperationError('matrix must be six finite numbers [a, b, c, d, e, f] (x\\' = a·x + c·y + e, y\\' = b·x + d·y + f).');\n }\n if (spec.matrix) {\n const [a, b, c, d] = spec.matrix;\n if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError('matrix is singular (it collapses the layer to a line); its determinant a·d − b·c must not be 0.');\n }\n const session = this.beginFreeTransform(spec);\n try {\n // Relative to where the content is now (a smart object's current placement).\n const g = session.startGeometry;\n const corners = spec.matrix ? (g.corners.map((p) => applyMatrix(spec.matrix!, p)) as unknown as Quad) : spec.corners ? spec.corners : spec.params ? applyParams(g, spec.params) : g.corners;\n let warp: WarpMesh | null = spec.warp === undefined ? (g.warp ?? null) : null;\n if (spec.warp) {\n if ('style' in spec.warp) {\n if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style \"${String(spec.warp.style)}\". Styles: ${WARP_STYLES.join(', ')}.`);\n warp = presetWarp(session.box, spec.warp);\n } else {\n if (!isValidWarp(spec.warp)) throw new LayerOperationError('mesh must have us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points, row by row.');\n warp = spec.warp;\n }\n }\n session.update({ corners, warp }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /** Flips the layer (or its selected pixels, or the outline) about the frame's centre, exactly (one undo step). */\n public flipLayer(axis: 'horizontal' | 'vertical', init: FreeTransformInit = {}): FreeTransformResult {\n if (axis !== 'horizontal' && axis !== 'vertical') throw new LayerOperationError(`axis must be horizontal or vertical, got \"${String(axis)}\".`);\n const session = this.beginFreeTransform({ interpolation: 'nearest', ...init });\n try {\n session.update({ corners: flipCorners(session.startGeometry, axis) }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /**\n * Rotates by a quarter turn (90 clockwise, −90 counter-clockwise) or 180 about the frame's\n * centre, snapped so pixels land on whole pixels: exact, no resampling (one undo step).\n */\n public rotateLayer90(turn: 90 | -90 | 180, init: FreeTransformInit = {}): FreeTransformResult {\n if (turn !== 90 && turn !== -90 && turn !== 180) throw new LayerOperationError(`turn must be 90, -90 or 180, got ${String(turn)}.`);\n const session = this.beginFreeTransform({ interpolation: 'nearest', ...init });\n try {\n // The frame in the document (a smart object's placed outline).\n const b = session.smartObject ? (transformedBox(session.startGeometry) ?? session.box) : session.box;\n // About the centre, rounded so an odd-by-even box still lands on the pixel grid.\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const ox = turn === 180 ? 0 : Math.round(cx - b.height / 2) - (cx - b.height / 2);\n const oy = turn === 180 ? 0 : Math.round(cy - b.width / 2) - (cy - b.width / 2);\n const m: AffineMatrix =\n turn === 90 ? [0, 1, -1, 0, cx + cy + ox, cy - cx + oy] : turn === -90 ? [0, -1, 1, 0, cx - cy + ox, cy + cx + oy] : [-1, 0, 0, -1, 2 * cx, 2 * cy];\n session.update({ corners: session.startGeometry.corners.map((p) => applyMatrix(m, p)) as unknown as Quad }, false);\n return session.commit();\n } catch (e) {\n session.cancel();\n throw e;\n }\n }\n\n /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */\n public addLayerFilter(id: string, filter: Partial<LayerFilter> & { kind: LayerFilter['kind'] }): LayerFilter[] {\n const f = normalizeFilter(filter);\n const next = addLayerFilterOp(this.state.layers, id, f);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */\n public updateLayerFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): LayerFilter[] {\n const next = updateLayerFilterOp(this.state.layers, id, filterId, changes);\n this.commitLayers(next, undefined, commitToHistory);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Removes one filter (one undo step). Returns the remaining filters. */\n public removeLayerFilter(id: string, filterId: string): LayerFilter[] {\n const next = removeLayerFilterOp(this.state.layers, id, filterId);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n // --- Smart objects ---\n\n private get canvasSize(): { width: number; height: number } {\n return this.naturalSize;\n }\n\n /**\n * Layer › Smart Objects › Convert to Smart Object (one undo step): sibling layers (default the\n * active one) become one smart object at the topmost one's position. A single pixel layer keeps\n * its id and properties and its filters become smart filters (it renders exactly as before);\n * anything else nests as a layer document. Returns the smart object's id.\n */\n public convertToSmartObject(ids?: string[], options: { name?: string } = {}): string {\n const list = ids?.length ? ids : this.state.activeLayerId ? [this.state.activeLayerId] : [];\n if (!list.length) throw new LayerOperationError('No layers given and none is active; pass layerIds (see list_layers).');\n const res = convertToSmartObjectOp(this.state.layers, list, this.canvasSize, { surfaceOf: (l) => this.surfaceOf(l), name: options.name });\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.layer.id;\n this.state.activeTarget = 'pixels';\n });\n return res.layer.id;\n }\n\n /** Layer › Smart Objects › Rasterize (one undo step): the smart object becomes a pixel layer holding what it renders. */\n public rasterizeSmartObject(id: string): string {\n const res = rasterizeSmartObjectOp(this.state.layers, id);\n this.commitLayers(res.layers, () => {\n if (this.state.activeLayerId === id && this.state.activeTarget === 'filter-mask') this.state.activeTarget = 'pixels';\n });\n return id;\n }\n\n /** A smart object's contents, placement and smart filters as plain data (see `SmartObjectInfo`). */\n public getSmartObject(id: string): SmartObjectInfo {\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n const g = placementGeometry(layer);\n const readout = describeTransform(g);\n const stack = layer.smartFilters;\n return {\n layerId: layer.id,\n contents: {\n id: c.id,\n name: c.name,\n kind: c.kind,\n width: c.width,\n height: c.height,\n decoded: isContentsDecoded(c),\n fileType: c.file?.type ?? null,\n linkedPath: c.linked?.path ?? null,\n instances: instancesOf(this.state.layers, c.id),\n layerCount: c.document ? flattenLayers(c.document.layers).length : 0,\n },\n placement: {\n corners: g.corners.map((p) => ({ x: p.x, y: p.y })) as unknown as Quad,\n warp: layer.placement.warp ?? null,\n interpolation: layer.placement.interpolation ?? 'bicubic',\n affine: readout.affine,\n scaleX: readout.scaleX,\n scaleY: readout.scaleY,\n rotate: readout.rotate,\n skewX: readout.skewX,\n center: anchorPoint(g, 'center'),\n bounds: transformedBox(g),\n },\n smartFilters: {\n enabled: stack?.enabled ?? true,\n filters: (stack?.filters ?? []).map((f) => (f.kind === 'psd' ? { id: f.id, kind: 'psd', name: f.name, enabled: f.enabled, blend: f.blend, opacity: f.opacity } : { ...f })),\n mask: stack?.mask\n ? { enabled: stack.mask.enabled !== false, linked: stack.mask.linked !== false, density: stack.mask.density ?? 1, feather: stack.mask.feather ?? 0, bounds: contentBounds(stack.mask.surface), defaultValue: stack.mask.defaultValue }\n : null,\n },\n bounds: this.getLayerBounds(id),\n };\n }\n\n /**\n * Sets a smart object's placement (one undo step, or none with `commitToHistory = false` during a\n * drag): where the contents' corners land, an optional warp in contents pixels and the\n * interpolation. The contents render again from the source; a linked mask follows a warp-free\n * change. Returns the new placement summary.\n */\n public setSmartObjectPlacement(id: string, placement: SmartPlacement, commitToHistory = true): SmartObjectInfo['placement'] & { masksMoved: boolean } {\n const res = setSmartPlacement(this.state.layers, id, placement, this.canvasSize);\n this.commitLayers(res.layers, undefined, commitToHistory);\n return { ...this.getSmartObject(id).placement, masksMoved: res.masksMoved };\n }\n\n /**\n * Sets a smart object's transform absolutely (one undo step): exactly one of `corners` (where the\n * contents' corners land), `matrix` (contents pixels → document) or the numbers (`scaleX`,\n * `scaleY` in percent of the contents' own size, `rotate`, `skewX`, `skewY`, and `x`, `y` where\n * the contents' centre goes, default where it is now); plus, optionally, a warp (a preset, a mesh\n * in contents pixels, or `null` to remove it) and the interpolation. Unlike `transformLayer`,\n * nothing is relative to the current transform.\n */\n public setSmartObjectTransform(\n id: string,\n spec: {\n corners?: Quad;\n matrix?: AffineMatrix;\n scaleX?: number;\n scaleY?: number;\n rotate?: number;\n skewX?: number;\n skewY?: number;\n x?: number;\n y?: number;\n warp?: WarpMesh | WarpPreset | null;\n interpolation?: Interpolation;\n }\n ): SmartObjectInfo['placement'] & { masksMoved: boolean } {\n const layer = requireSmartObject(this.state.layers, id);\n const g = placementGeometry(layer);\n const numeric = (['scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'x', 'y'] as const).filter((k) => spec[k] !== undefined);\n const ways = [spec.corners !== undefined, spec.matrix !== undefined, numeric.length > 0].filter(Boolean).length;\n if (ways > 1) throw new LayerOperationError('Give the transform only one way: corners, matrix, or the numbers (scaleX, scaleY, rotate, skewX, skewY, x, y).');\n let corners: Quad = g.corners;\n if (spec.corners) corners = spec.corners;\n else if (spec.matrix) {\n if (!(Array.isArray(spec.matrix) && spec.matrix.length === 6 && spec.matrix.every(Number.isFinite))) throw new LayerOperationError(\"matrix must be six finite numbers [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f).\");\n const [a, b, c, d] = spec.matrix;\n if (Math.abs(a * d - b * c) < 1e-9) throw new LayerOperationError('matrix is singular (it collapses the contents to a line); a·d − b·c must not be 0.');\n corners = cornersFromMatrix(g.box, spec.matrix);\n } else if (numeric.length) {\n if (spec.scaleX === 0 || spec.scaleY === 0) throw new LayerOperationError('scaleX and scaleY must not be 0 (that collapses the contents to a line).');\n for (const k of numeric) if (!Number.isFinite(spec[k] as number)) throw new LayerOperationError(`${k} must be a finite number.`);\n const now = describeTransform(g);\n const centre = anchorPoint(g, 'center');\n const params = {\n scaleX: spec.scaleX ?? (now.affine ? now.scaleX : 100),\n scaleY: spec.scaleY ?? (now.affine ? now.scaleY : 100),\n rotate: spec.rotate ?? (now.affine ? now.rotate : 0),\n skewX: spec.skewX ?? (now.affine ? now.skewX : 0),\n skewY: spec.skewY ?? 0,\n };\n const m = matrixFromParams(g.box, { ...params, reference: 'center' });\n const cx = g.box.width / 2;\n const cy = g.box.height / 2;\n const to = { x: spec.x ?? centre.x, y: spec.y ?? centre.y };\n corners = cornersFromMatrix(g.box, multiplyMatrices(m, translateMatrix(to.x - cx, to.y - cy)));\n }\n let warp: WarpMesh | null | undefined = layer.placement.warp ?? null;\n if (spec.warp === null) warp = null;\n else if (spec.warp) {\n if ('style' in spec.warp) {\n if (!WARP_STYLES.includes(spec.warp.style)) throw new LayerOperationError(`Unknown warp style \"${String(spec.warp.style)}\". Styles: ${WARP_STYLES.join(', ')}.`);\n warp = presetWarp(g.box, spec.warp);\n } else {\n if (!isValidWarp(spec.warp)) throw new LayerOperationError('mesh must have us and vs (knots from 0 to 1, increasing) and (3·columns + 1) × (3·rows + 1) finite points in contents pixels, row by row.');\n warp = spec.warp;\n }\n }\n const next: SmartPlacement = { corners, warp, interpolation: spec.interpolation ?? layer.placement.interpolation };\n return this.setSmartObjectPlacement(id, next);\n }\n\n /**\n * Layer › Smart Objects › Replace Contents (one undo step): new contents for the smart object and\n * every instance linked to it, from RGBA pixels, PSD bytes (a layer document) or contents. Each\n * instance keeps its scale and rotation about the contents' centre. Returns the contents id and\n * the layers that changed.\n */\n public replaceSmartObjectContents(\n id: string,\n source: { pixels: PixelBuffer; name?: string } | { psd: Uint8Array; name?: string } | { contents: SmartContents }\n ): { contentsId: string; width: number; height: number; layerIds: string[] } {\n const layer = requireSmartObject(this.state.layers, id);\n let contents: SmartContents;\n if ('contents' in source) contents = source.contents;\n else if ('pixels' in source) contents = rasterContents(source.pixels, source.name ?? layer.contents.name.replace(/\\.\\w+$/, '.png'));\n else {\n const psd = importPsd(source.psd, { newId: () => newLayerId() });\n contents = documentContents({ layers: psd.layers, globalLight: psd.globalLight, patterns: psd.patterns.length ? psd.patterns : undefined }, psd.width, psd.height, source.name ?? `${layer.name || 'Contents'}.psb`);\n contents.file = { bytes: source.psd.slice(), type: psd.info.psb ? 'psb' : 'psd' };\n }\n const res = replaceContentsOp(this.state.layers, layer.contents.id, contents, this.canvasSize);\n this.commitLayers(res.layers);\n return { contentsId: layer.contents.id, width: contents.width, height: contents.height, layerIds: res.ids };\n }\n\n /** `replaceSmartObjectContents` from an image (PNG data URL or base64 decodes anywhere; other formats need a browser). */\n public async replaceSmartObjectContentsFromImage(id: string, image: string, name?: string): Promise<{ contentsId: string; width: number; height: number; layerIds: string[] }> {\n const pixels = await this.decodeImage(image);\n return this.replaceSmartObjectContents(id, { pixels, name });\n }\n\n /**\n * Layer › Smart Objects › Export Contents: the file the contents came from while unedited, else a\n * PNG (raster contents) or a PSD / PSB of the nested document. Returns the file name, MIME type\n * and bytes.\n */\n public exportSmartObjectContents(id: string): { name: string; mimeType: string; bytes: Uint8Array } {\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n const base = c.name.replace(/\\.\\w+$/, '') || 'Contents';\n if (c.file) {\n const mime: Record<string, string> = { psd: 'image/vnd.adobe.photoshop', psb: 'image/vnd.adobe.photoshop', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', tif: 'image/tiff', tiff: 'image/tiff', gif: 'image/gif' };\n const ext = c.file.type.toLowerCase();\n return { name: /\\.\\w+$/.test(c.name) ? c.name : `${base}.${ext}`, mimeType: mime[ext] ?? 'application/octet-stream', bytes: c.file.bytes };\n }\n if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are not available here.`);\n if (c.kind === 'raster') {\n const data = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });\n return { name: `${base}.png`, mimeType: 'image/png', bytes: encodePng(c.width, c.height, data) };\n }\n const doc = c.document!;\n const merged = readRegion(contentsSurface(c), { x: 0, y: 0, width: c.width, height: c.height });\n const prepared = prepareNestedForPsd(doc.layers);\n const res = exportPsd({ width: c.width, height: c.height, layers: prepared, merged, patterns: doc.patterns, globalLight: doc.globalLight });\n return { name: `${base}.${res.psb ? 'psb' : 'psd'}`, mimeType: 'image/vnd.adobe.photoshop', bytes: res.bytes };\n }\n\n /**\n * Layer › Smart Objects › New Smart Object via Copy (`linked: false`: independent contents) or a\n * linked duplicate (`linked: true`: edits to the contents show in both), directly above, as one\n * undo step. Returns the copy's id.\n */\n public copySmartObject(id: string, linked = false, name?: string): string {\n const res = copySmartObjectOp(this.state.layers, id, linked, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.layer.id;\n this.state.activeTarget = 'pixels';\n });\n return res.layer.id;\n }\n\n /** Adds a smart filter (`develop` or `blur`, with `blend` and `opacity`) at `index` (default the top), as one undo step. */\n public addSmartFilter(id: string, filter: Record<string, unknown> & { kind: string }, index?: number): SmartFilter {\n const res = addSmartFilterOp(this.state.layers, id, normalizeSmartFilter(filter), this.canvasSize, index);\n this.commitLayers(res.layers);\n return res.filter;\n }\n\n /** Changes one smart filter (one undo step, or none with `commitToHistory = false` during a drag). */\n public updateSmartFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): SmartFilter {\n const res = updateSmartFilterOp(this.state.layers, id, filterId, changes, this.canvasSize);\n this.commitLayers(res.layers, undefined, commitToHistory);\n return res.filter;\n }\n\n /** Removes one smart filter (one undo step). Returns the remaining filters. */\n public removeSmartFilter(id: string, filterId: string): SmartFilter[] {\n const res = removeSmartFilterOp(this.state.layers, id, filterId, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.filters ?? [];\n }\n\n /** Moves one smart filter to `index` (0 = bottom, applied first), as one undo step. Returns the filters. */\n public moveSmartFilter(id: string, filterId: string, index: number): SmartFilter[] {\n const res = moveSmartFilterOp(this.state.layers, id, filterId, index, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.filters ?? [];\n }\n\n /** Turns a smart object's whole filter stack on or off (one undo step). */\n public setSmartFiltersEnabled(id: string, enabled: boolean): boolean {\n const res = setSmartFiltersEnabledOp(this.state.layers, id, enabled, this.canvasSize);\n this.commitLayers(res.layers);\n return res.layer.smartFilters?.enabled ?? true;\n }\n\n /**\n * Sets the smart filter mask (one undo step): `reveal-all`, `hide-all`, `selection` (the\n * selection's coverage), or `none` to remove it; `props` sets density, feather, enabled and linked.\n * Painting it targets channel `filter-mask`. Returns the mask, or `null` when removed.\n */\n public setSmartFilterMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' | 'none' | 'keep', props: LayerMaskProps = {}, commitToHistory = true): RasterMask | null {\n const layer = requireSmartObject(this.state.layers, id);\n let mask: RasterMask | null;\n if (from === 'none') mask = null;\n else if (from === 'keep') {\n if (!layer.smartFilters?.mask) throw new LayerOperationError(`Smart object \"${layer.name || id}\" has no filter mask; add one with from reveal-all, hide-all or selection.`);\n mask = withMaskProps(layer.smartFilters.mask, props);\n } else if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const res = setSmartFilterMaskOp(this.state.layers, id, mask, this.canvasSize);\n this.commitLayers(res.layers, () => {\n if (this.state.activeLayerId !== id) return;\n if (mask && from !== 'keep') this.state.activeTarget = 'filter-mask';\n else if (!mask && this.state.activeTarget === 'filter-mask') this.state.activeTarget = 'pixels';\n }, commitToHistory);\n return res.layer.smartFilters?.mask ?? null;\n }\n\n /**\n * Decodes smart object contents that came as files the synchronous decoders cannot read (a JPEG\n * or TIFF embedded in a PSD), through the browser's image decoder. No undo step: the pixels on\n * screen do not change. Returns the contents ids still undecoded.\n */\n public async prepareSmartObjects(): Promise<string[]> {\n const pending = new Map<string, SmartContents>();\n for (const l of flattenLayers(this.state.layers)) if (l.type === 'smart' && !isContentsDecoded(l.contents) && l.contents.file) pending.set(l.contents.id, l.contents);\n const left: string[] = [];\n for (const [cid, c] of pending) {\n try {\n const url = `data:${c.file!.type === 'png' ? 'image/png' : c.file!.type === 'gif' ? 'image/gif' : c.file!.type === 'tif' || c.file!.type === 'tiff' ? 'image/tiff' : 'image/jpeg'};base64,${bytesToBase64(c.file!.bytes)}`;\n const px = await this.decodeImage(url);\n const decoded: SmartContents = { ...c, kind: 'raster', raster: rasterContents(px, c.name, cid).raster, width: px.width, height: px.height };\n const swap = (layers: ImageLayer[]) => {\n let out = layers;\n for (const l of flattenLayers(layers)) if (l.type === 'smart' && l.contents.id === cid) out = mapLayer(out, l.id, (x) => ({ ...(x as SmartObjectLayer), contents: decoded }));\n return out;\n };\n this.state.layers = swap(this.state.layers);\n for (const e of [...this.undoStack, ...this.redoStack]) e.layers = swap(e.layers);\n } catch {\n left.push(cid);\n }\n }\n if (pending.size) this.notify();\n return left;\n }\n\n // --- Smart object contents (Edit Contents) ---\n\n private contentsStack: Array<{ layerId: string; contentsId: string; name: string; saved: SavedDocument }> = [];\n\n /** The smart objects whose contents are open (outermost first); empty when the document itself is shown. */\n public getEditingContents(): Array<{ layerId: string; name: string }> {\n return this.contentsStack.map((e) => ({ layerId: e.layerId, name: e.name }));\n }\n\n /**\n * Layer › Smart Objects › Edit Contents: shows the smart object's contents as a document of their\n * own (a layer document, or raster contents as one pixel layer), with their own undo history.\n * Every editor operation and tool then works on the contents until `closeSmartObjectContents`.\n * Contents can nest. Returns the contents' size and layer ids.\n */\n public editSmartObjectContents(id: string): { layerId: string; depth: number; width: number; height: number; layerIds: string[] } {\n if (this.getFreeTransform()) throw new LayerOperationError('A Free Transform is open; commit or cancel it first.');\n const layer = requireSmartObject(this.state.layers, id);\n const c = layer.contents;\n if (!isContentsDecoded(c)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are a ${c.file?.type ?? 'file'} that could not be decoded here; run prepareSmartObjects() in a browser first.`);\n const saved: SavedDocument = {\n state: { ...this.state },\n undo: this.undoStack,\n redo: this.redoStack,\n initial: this.initialSnapshot,\n element: this.imageElement,\n samplePixels: this.samplePixels,\n samplePixelsOverride: this.samplePixelsOverride,\n psdResolution: this.psdResolution,\n };\n const layers: ImageLayer[] =\n c.kind === 'document'\n ? c.document!.layers\n : [{ type: 'pixel', id: newLayerId(), name: 'Layer 0', visible: true, opacity: 1, blend: 'normal', surface: c.raster!, offset: { x: 0, y: 0 } }];\n this.contentsStack.push({ layerId: layer.id, contentsId: c.id, name: layer.name || c.name, saved });\n const s = this.state;\n this.state = {\n ...s,\n sourceUrl: null,\n imageDimensions: { width: c.width, height: c.height },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM_VALUES },\n layers,\n activeLayerId: layers.length ? layers[layers.length - 1].id : null,\n activeTarget: 'pixels',\n selection: null,\n selectedAnnotationId: null,\n outputOpacity: 1,\n zoom: 1,\n pan: { x: 0, y: 0 },\n editingContents: this.getEditingContents(),\n };\n delete this.state.legacyOutputBlur;\n if (c.document?.globalLight) this.state.globalLight = c.document.globalLight;\n else delete this.state.globalLight;\n if (c.document?.patterns?.length) this.state.patterns = c.document.patterns;\n else delete this.state.patterns;\n this.imageElement = null;\n this.samplePixels = null;\n this.samplePixelsOverride = false;\n this.undoStack = [];\n this.redoStack = [];\n this.viewKey = [];\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n return { layerId: layer.id, depth: this.contentsStack.length, width: c.width, height: c.height, layerIds: flattenLayers(layers).map((l) => l.id) };\n }\n\n /**\n * Closes the innermost open contents: with `save` (default) the edited layers become the smart\n * object's contents, and it and every linked instance render again (one undo step in the\n * document); without, the edits are dropped. Returns the smart object and the layers that changed.\n */\n public closeSmartObjectContents(save = true): { layerId: string; changed: boolean; layerIds: string[] } {\n const top = this.contentsStack.pop();\n if (!top) throw new LayerOperationError('No smart object contents are open; open them with edit_smart_object_contents.');\n if (this.openTransform?.isOpen) this.openTransform.cancel();\n this.openTransform = null;\n const edited = this.state;\n const changed = save && this.undoStack.length > 0;\n let next: SmartContents | null = null;\n if (changed) {\n const layer = findLayer(top.saved.state.layers, top.layerId)?.layer as SmartObjectLayer | undefined;\n const old = layer?.contents;\n const { width, height } = edited.imageDimensions;\n const only = edited.layers.length === 1 ? edited.layers[0] : null;\n const plain =\n only?.type === 'pixel' && only.visible && only.opacity === 1 && only.blend === 'normal' && !only.mask && !only.effects && !only.filters?.length && !only.clipped && (only.fillOpacity ?? 1) === 1 && only.source !== 'document';\n if (old?.kind === 'raster' && plain) {\n const px = readRegion(only.surface, { x: -only.offset.x, y: -only.offset.y, width, height });\n next = rasterContents({ width, height, data: px }, old.name, old.id);\n } else {\n const doc = { layers: edited.layers, globalLight: edited.globalLight, patterns: edited.patterns };\n next = documentContents(doc, width, height, old?.kind === 'document' ? old.name : `${(old?.name ?? 'Contents').replace(/\\.\\w+$/, '')}.psb`, top.contentsId);\n }\n }\n const saved = top.saved;\n this.state = saved.state;\n this.undoStack = saved.undo;\n this.redoStack = saved.redo;\n this.initialSnapshot = saved.initial;\n this.imageElement = saved.element;\n this.samplePixels = saved.samplePixels;\n this.samplePixelsOverride = saved.samplePixelsOverride;\n this.psdResolution = saved.psdResolution;\n this.viewKey = [];\n if (this.contentsStack.length) this.state.editingContents = this.getEditingContents();\n else delete this.state.editingContents;\n if (!next) {\n this.notify();\n return { layerId: top.layerId, changed: false, layerIds: [] };\n }\n const res = replaceContentsOp(this.state.layers, top.contentsId, next, this.canvasSize);\n this.commitLayers(res.layers);\n return { layerId: top.layerId, changed: true, layerIds: res.ids };\n }\n\n /** The document's own layers (not the contents open for editing). */\n private rootLayers(): ImageLayer[] {\n return this.contentsStack[0]?.saved.state.layers ?? this.state.layers;\n }\n\n /** Drops every open contents edit (loading another document). */\n private abandonContents(): void {\n while (this.contentsStack.length) this.closeSmartObjectContents(false);\n }\n\n // --- Layer effects ---\n\n /** A layer's effects (an empty, enabled list when it has none). */\n public getLayerEffects(id: string): LayerEffects {\n return layerEffectsOf(requireLayer(this.state.layers, id).layer);\n }\n\n /**\n * Adds an effect of `kind` with Photoshop's defaults and `params` applied (one undo step), at\n * `index` in the layer's effect list (default the end). Returns the new effect and the effects.\n */\n public addLayerEffect(id: string, kind: LayerEffectKind, params: Record<string, unknown> = {}, index?: number): { effect: LayerEffect; effects: LayerEffects } {\n const res = addLayerEffectOp(this.state.layers, id, kind, params, index);\n this.commitLayers(res.layers);\n return { effect: res.effect, effects: this.getLayerEffects(id) };\n }\n\n /** Changes one effect (one undo step, or none with `commitToHistory = false` during a drag). Returns the effects. */\n public updateLayerEffect(id: string, effectId: string, changes: Record<string, unknown>, commitToHistory = true): LayerEffects {\n this.commitLayers(updateLayerEffectOp(this.state.layers, id, effectId, changes), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n /** Removes one effect (one undo step). Returns the remaining effects. */\n public removeLayerEffect(id: string, effectId: string): LayerEffects {\n this.commitLayers(removeLayerEffectOp(this.state.layers, id, effectId));\n return this.getLayerEffects(id);\n }\n\n /** Turns a layer's effects on or off as a whole and, when given, sets Scale Effects (one undo step). */\n public setLayerEffectsEnabled(id: string, enabled: boolean, scale?: number, commitToHistory = true): LayerEffects {\n this.commitLayers(setLayerEffectsEnabledOp(this.state.layers, id, enabled, scale), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n /** Replaces a layer's effects (one undo step; `null` removes them). Returns the effects. */\n public setLayerEffects(id: string, effects: LayerEffects | null, commitToHistory = true): LayerEffects {\n this.commitLayers(setLayerEffectsOp(this.state.layers, id, effects), undefined, commitToHistory);\n return this.getLayerEffects(id);\n }\n\n private patternIds = new WeakMap<object, number>();\n\n /** Changes whenever the document's pattern list is replaced (the compositor then drops its caches). */\n private patternsVersion(): number {\n const list = this.state.patterns;\n if (!list) return 0;\n let id = this.patternIds.get(list);\n if (id === undefined) {\n id = this.nextSourceId++;\n this.patternIds.set(list, id);\n }\n return id;\n }\n\n /** The document's patterns (from an opened PSD), besides the built-in ones. */\n public getDocumentPatterns(): EffectPattern[] {\n return this.state.patterns ?? [];\n }\n\n /** The light effects with \"use global light\" follow. */\n public getGlobalLight(): GlobalLight {\n return { ...(this.state.globalLight ?? DEFAULT_GLOBAL_LIGHT) };\n }\n\n /** Sets the global light (one undo step). `angle` -360..360°, `altitude` 0..90°. Returns it. */\n public setGlobalLight(light: Partial<GlobalLight>, commitToHistory = true): GlobalLight {\n const next = { ...this.getGlobalLight(), ...light };\n 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}.`);\n 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}.`);\n if (commitToHistory) this.recordHistory();\n if (next.angle === DEFAULT_GLOBAL_LIGHT.angle && next.altitude === DEFAULT_GLOBAL_LIGHT.altitude) delete this.state.globalLight;\n else this.state.globalLight = next;\n this.notify();\n return this.getGlobalLight();\n }\n\n /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */\n private async compositeToSurface(layers: ImageLayer[], rect: CropBox): Promise<PixelSurface> {\n const { width, height } = this.naturalSize;\n if (flattenLayers(layers).some((l) => l.type === 'annotation' && l.visible) && !this.renderer.canRasterize()) {\n throw new LayerOperationError('Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.');\n }\n await this.renderer.prepareLayerAssets(layers, width, height);\n const data = new LayerCompositor().composite(layers, rect, this.renderer.compositorSources(width, height));\n return surfaceFromPixels({ width: rect.width, height: rect.height, data }, 4, rect.x, rect.y);\n }\n\n /** Document bounds a layer can paint into (pixel content, annotations, or the canvas for the rest). */\n private paintBounds(layer: ImageLayer): CropBox {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n if (layer.type === 'pixel') return this.getLayerBounds(layer.id) ?? { x: 0, y: 0, width: 0, height: 0 };\n if (layer.type === 'annotation') {\n const b = getAnnotationBounds(layer.annotation);\n const pad = 4 + ('strokeWidth' in layer.annotation ? (layer.annotation as { strokeWidth: number }).strokeWidth : 0);\n return coveringRect({ x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad });\n }\n if (layer.type === 'group') {\n let r: CropBox | null = null;\n for (const c of layer.children) r = unionRect(r, this.paintBounds(c));\n return r ?? canvas;\n }\n return canvas;\n }\n\n /**\n * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,\n * opacity and blend; masks, fill, filters and both layers' effects are baked), as one undo step.\n * Returns the merged id.\n */\n public async mergeDown(id: string): Promise<string> {\n const loc = requireLayer(this.state.layers, id);\n const upper = loc.layer;\n const siblings = loc.parent ? loc.parent.children : this.state.layers;\n const lower = siblings[loc.index - 1];\n if (!lower) throw new LayerOperationError(`Layer \"${upper.name || id}\" is the bottom of its group; there is nothing below to merge into.`);\n if (lower.type !== 'pixel') throw new LayerOperationError(`Merge Down needs a pixel layer below; \"${lower.name || lower.id}\" is a ${lower.type} layer. Move the layer or merge into another one.`);\n if (!upper.visible) throw new LayerOperationError(`Layer \"${upper.name || id}\" is hidden; show it or delete it instead of merging.`);\n requireUnlocked(upper, 'all', 'merge it');\n requireUnlocked(lower, 'pixels', 'merge into it');\n let rect = unionRect(this.paintBounds(lower), upper.type === 'adjustment' ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };\n // Effects paint beyond the layers' pixels: merge as far as they reach.\n const reach = Math.max(effectsMargin(lower.effects), upper.type === 'adjustment' ? 0 : effectsMargin(upper.effects));\n if (reach) rect = { x: rect.x - reach, y: rect.y - reach, width: rect.width + 2 * reach, height: rect.height + 2 * reach };\n const base: PixelLayer = { ...lower, blend: 'normal', opacity: 1 };\n delete base.clipped;\n const surface = await this.compositeToSurface([base, upper], rect);\n const merged: PixelLayer = { ...lower, surface, offset: { x: 0, y: 0 } };\n delete merged.mask;\n delete merged.fillOpacity;\n delete merged.filters;\n delete merged.source;\n delete merged.effects;\n let next = mapLayer(this.state.layers, id, () => null);\n next = mapLayer(next, lower.id, () => merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n });\n return merged.id;\n }\n\n /**\n * Merge Visible: the visible top-level layers become one normal pixel layer (at the position of\n * the lowest of them); hidden layers stay. One undo step. Returns the merged id.\n */\n public async mergeVisible(): Promise<string> {\n const visible = this.state.layers.filter((l) => l.visible);\n if (!visible.length) throw new LayerOperationError('No layer is visible.');\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(visible, { x: 0, y: 0, width, height });\n const merged: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: 'Merged',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n const at = this.state.layers.indexOf(visible[0]);\n const next = this.state.layers.filter((l) => !l.visible);\n next.splice(this.state.layers.slice(0, at).filter((l) => !l.visible).length, 0, merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return merged.id;\n }\n\n /** Flatten Image: every visible layer composited into one Background layer; hidden layers are discarded. One undo step. */\n public async flattenImage(): Promise<string> {\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(this.state.layers, { x: 0, y: 0, width, height });\n const bg: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n this.commitLayers([bg], () => {\n this.state.activeLayerId = bg.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return bg.id;\n }\n\n private thumbCache = new WeakMap<object, Map<string, PixelBuffer>>();\n\n /**\n * A small RGBA preview of a pixel layer over the canvas (or of its mask with `mask: true`),\n * longest side `maxSide` px, point-sampled; `null` for layers without pixels.\n */\n public getLayerThumbnail(id: string, options: { maxSide?: number; mask?: boolean } = {}): PixelBuffer | null {\n const layer = this.getLayer(id);\n if (!layer) return null;\n const { width, height } = this.naturalSize;\n const side = Math.max(4, Math.min(256, Math.round(options.maxSide ?? 48)));\n const k = Math.min(1, side / Math.max(width, height));\n const tw = Math.max(1, Math.round(width * k));\n const th = Math.max(1, Math.round(height * k));\n const surface = options.mask ? (layer.mask && !layer.mask.source ? layer.mask.surface : null) : layer.type === 'pixel' || layer.type === 'smart' ? this.surfaceOf(layer) : null;\n if (!surface) return null;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const key = `${tw}x${th}:${ox},${oy}`;\n let byKey = this.thumbCache.get(surface);\n const hit = byKey?.get(key);\n if (hit) return hit;\n const out = new Uint8ClampedArray(tw * th * 4);\n for (let y = 0; y < th; y++) {\n const dy = Math.min(height - 1, Math.floor(((y + 0.5) * height) / th)) - oy;\n for (let x = 0; x < tw; x++) {\n const dx = Math.min(width - 1, Math.floor(((x + 0.5) * width) / tw)) - ox;\n const v = readPixel(surface, dx, dy);\n const i = (y * tw + x) * 4;\n if (surface.channels === 1) {\n out[i] = out[i + 1] = out[i + 2] = v[0];\n out[i + 3] = 255;\n } else out.set(v, i);\n }\n }\n const thumb = { width: tw, height: th, data: out };\n if (!byKey) {\n byKey = new Map();\n this.thumbCache.set(surface, byKey);\n }\n byKey.set(key, thumb);\n return thumb;\n }\n\n // --- Painting ---\n\n /** Brush settings, colours and tool options of the paint tools (view state, no undo). */\n public setPaintOptions(\n options: Partial<Omit<PaintOptions, 'brush' | 'mixer' | 'mixerLoad'>> & { brush?: Partial<BrushSettings>; mixer?: Partial<MixerSettings> }\n ): PaintOptions {\n const cur = this.state.paint;\n const next: PaintOptions = {\n brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,\n foreground: options.foreground !== undefined ? toHex(parseColor(options.foreground)) : cur.foreground,\n background: options.background !== undefined ? toHex(parseColor(options.background)) : cur.background,\n mixer: options.mixer ? normalizeMixer({ ...cur.mixer, ...options.mixer }) : cur.mixer,\n mixerLoad: cur.mixerLoad,\n };\n this.state.paint = next;\n this.notify();\n return next;\n }\n\n /**\n * Loads the Mixer Brush's reservoir full with `color` (default the foreground); the picked-up\n * colour is kept. View state, no undo. Returns the brush load.\n */\n public loadMixerBrush(color?: string | number[]): MixerLoad {\n const c = parseColor(color ?? this.state.paint.foreground);\n const load = loadMixer(this.state.paint.mixerLoad, [c[0] / 255, c[1] / 255, c[2] / 255]);\n this.state.paint = { ...this.state.paint, mixerLoad: load };\n this.notify();\n return load;\n }\n\n /** Cleans the Mixer Brush: empties the pickup well (the reservoir is kept). Returns the brush load. */\n public cleanMixerBrush(): MixerLoad {\n const load = cleanMixer(this.state.paint.mixerLoad);\n this.state.paint = { ...this.state.paint, mixerLoad: load };\n this.notify();\n return load;\n }\n\n /** Empties the Mixer Brush entirely (no reservoir, nothing picked up). Returns the brush load. */\n public emptyMixerBrush(): MixerLoad {\n this.state.paint = { ...this.state.paint, mixerLoad: EMPTY_MIXER_LOAD };\n this.notify();\n return EMPTY_MIXER_LOAD;\n }\n\n /** D: black foreground, white background. */\n public resetPaintColors(): void {\n this.setPaintOptions({ foreground: '#000000', background: '#ffffff' });\n }\n\n /** X: swaps the foreground and background colours. */\n public swapPaintColors(): void {\n const { foreground, background } = this.state.paint;\n this.setPaintOptions({ foreground: background, background: foreground });\n }\n\n /** Resolves and validates what a stroke, fill or clear paints into. */\n private paintTarget(layerId: string | undefined, channel: LayerTarget | undefined, action: string) {\n const id = layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`);\n const layer = requireLayer(this.state.layers, id).layer;\n const ch: LayerTarget = channel ?? (id === this.state.activeLayerId ? this.state.activeTarget : 'pixels');\n if (ch === 'mask') {\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask to ${action}; add one with add_layer_mask, or target pixels.`);\n if (layer.mask.source) throw new LayerOperationError(`Layer \"${layer.name || id}\" has a depth mask, which is generated, not painted.`);\n requireUnlocked(layer, 'all', action);\n } else if (ch === 'filter-mask') {\n if (layer.type !== 'smart' || !layer.smartFilters?.mask) {\n throw new LayerOperationError(`Layer \"${layer.name || id}\" has no smart filter mask to ${action}; add one with set_smart_filter_mask, or target pixels or mask.`);\n }\n if (!isContentsDecoded(layer.contents)) throw new LayerOperationError(`The contents of \"${layer.name || id}\" are not decoded here, so its filter mask cannot be painted; run prepareSmartObjects() first.`);\n requireUnlocked(layer, 'all', action);\n } else {\n if (layer.type === 'smart') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a smart object; its pixels come from its contents. Rasterize it (rasterize_smart_object), edit its contents (edit_smart_object_contents), or ${action} on its mask or filter mask.`);\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);\n requireUnlocked(layer, 'pixels', action);\n }\n if (!layer.visible) throw new LayerOperationError(`Layer \"${layer.name || id}\" is hidden; show it before you ${action}.`);\n const offset = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const surface = ch === 'mask' ? layer.mask!.surface : ch === 'filter-mask' ? (layer as SmartObjectLayer).smartFilters!.mask!.surface : this.surfaceOf(layer as PixelLayer);\n const lockTransparency = ch === 'pixels' && Boolean(layer.locks?.transparency);\n return { id, layer, channel: ch, offset, surface, lockTransparency };\n }\n\n /** The layer with its pixels (or mask) replaced by `surface`. */\n private withSurface(layer: ImageLayer, channel: LayerTarget, surface: TiledSurface): ImageLayer {\n if (channel === 'mask') return { ...layer, mask: { ...layer.mask!, surface } } as ImageLayer;\n if (channel === 'filter-mask') return withFilterMaskSurface(layer as SmartObjectLayer, surface, this.canvasSize);\n const next = { ...(layer as PixelLayer), surface };\n delete next.source;\n return next;\n }\n\n /**\n * Starts a brush or eraser stroke on a layer's pixels or mask (default: the active layer and\n * target, the paint options' brush and colours). Feed points with `push`, finish with `end()`\n * (one undo step for the whole stroke) or `cancel()`. While it runs, the stack shows the stroke\n * without recording history.\n */\n public beginStroke(init: StrokeInit = {}): PaintStrokeSession {\n const tool = init.tool ?? 'brush';\n if (tool !== 'brush' && tool !== 'eraser' && tool !== 'mixer') throw new LayerOperationError(`Unknown paint tool \"${String(tool)}\"; use brush, eraser or mixer.`);\n const target = this.paintTarget(init.layerId, init.channel, tool === 'eraser' ? 'erase' : 'paint');\n if (tool === 'mixer' && target.channel !== 'pixels') {\n throw new LayerOperationError('The Mixer Brush paints colour on pixels, not on masks; target pixels (channel pixels), or paint the mask with the brush.');\n }\n const brush = normalizeBrush({ ...this.state.paint.brush, ...(init.brush ?? {}) });\n const color = parseColor(init.color ?? this.state.paint.foreground);\n const background = parseColor(init.background ?? this.state.paint.background);\n const toMaskGrey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode: tool === 'eraser' ? 'erase' : 'paint',\n color: target.channel !== 'pixels' ? toMaskGrey(color) : color,\n background: target.channel !== 'pixels' ? toMaskGrey(background) : background,\n opacity: brush.opacity,\n blend: target.channel !== 'pixels' ? 'normal' : brush.blend,\n lockTransparency: target.lockTransparency,\n selection: this.state.selection?.bounds ? this.state.selection : null,\n offset: target.offset,\n };\n if (tool === 'mixer') return new PaintStrokeSession(this, target, this.mixerStroke(init, brush), apply);\n return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);\n }\n\n /** A Mixer Brush stroke's start: the settings, and the load (refilled when asked or set to). */\n private mixerStroke(init: StrokeInit, brush: BrushSettings): MixerStrokeInit {\n const settings = normalizeMixer({ ...this.state.paint.mixer, ...(init.mixer ?? {}) });\n let load = this.state.paint.mixerLoad;\n if (init.color !== undefined || settings.loadEachStroke) {\n const c = parseColor(init.color ?? this.state.paint.foreground);\n load = loadMixer(load, [c[0] / 255, c[1] / 255, c[2] / 255]);\n }\n return {\n create: (pixels) => new MixerStroke(brush, settings, load, pixels),\n end: (after) => {\n const next = settings.cleanEachStroke ? cleanMixer(after) : after;\n this.state.paint = { ...this.state.paint, mixerLoad: next };\n return next;\n },\n };\n }\n\n /** @internal Shows a stroke's live surface (no history). */\n public _strokeUpdate(layerId: string, channel: LayerTarget, surface: TiledSurface, rects: CropBox[]): void {\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n // A filter mask re-renders the smart object (and feathering spreads the change): redraw it all.\n this.notify(channel === 'filter-mask' ? undefined : rects);\n }\n\n /** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */\n public _strokeCommit(before: HistoryEntryHandle, layerId: string, channel: LayerTarget, surface: TiledSurface | null): void {\n if (surface) this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n else this.state.layers = (before as unknown as HistoryEntry).layers;\n if (surface && !this.isApplyingHistory) {\n this.undoStack.push(before as unknown as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n this.notify();\n }\n\n /** @internal The pre-stroke snapshot. */\n public _captureForStroke(): HistoryEntryHandle {\n return this.captureHistoryEntry() as unknown as HistoryEntryHandle;\n }\n\n /**\n * Paints a whole stroke in one call (agents, replays): `points` in document pixels with optional\n * pressure, through the brush, eraser or Mixer Brush. One undo step. Returns the layer, channel, the changed\n * document bounds and the number of dabs.\n */\n public paintStroke(init: StrokeInit & { points: StrokePoint[] }): PaintStrokeResult {\n if (!Array.isArray(init.points) || !init.points.length) throw new LayerOperationError('points must hold at least one { x, y } point.');\n for (const p of init.points) {\n if (!Number.isFinite(p?.x) || !Number.isFinite(p?.y)) throw new LayerOperationError('Every point needs finite x and y (document pixels).');\n }\n const session = this.beginStroke(init);\n session.push(init.points);\n return session.end();\n }\n\n /**\n * Edit › Fill: fills the selection (or, without one or with `useSelection: false`, the whole\n * canvas) on a layer's pixels or mask with a colour (default the foreground), at `opacity` with\n * `blend`. One undo step. Returns the changed bounds.\n */\n public fillPixels(options: FillOptions = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'paint');\n }\n\n /**\n * Edit › Clear: removes the selected pixels (alpha to 0; on a transparency-locked layer or a mask\n * it fills with the background colour). Without a selection (or with `useSelection: false`) the\n * whole canvas. One undo step. Returns the changed bounds.\n */\n public clearPixels(options: Omit<FillOptions, 'color' | 'blend'> = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'erase');\n }\n\n private fillOrClear(options: FillOptions, mode: 'paint' | 'erase'): PaintStrokeResult {\n const target = this.paintTarget(options.layerId, options.channel, mode === 'paint' ? 'fill' : 'clear');\n const { width, height } = this.naturalSize;\n const opacity = options.opacity ?? 1;\n if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const blend = (parseBlend(options.blend) as BlendMode | undefined) ?? 'normal';\n const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;\n const region = sel?.bounds ?? { x: 0, y: 0, width, height };\n const coverage = new CoverageMap();\n const ones = new Float32Array(region.width * region.height);\n if (sel) {\n for (let y = 0; y < region.height; y++) {\n for (let x = 0; x < region.width; x++) ones[y * region.width + x] = sel.data[(region.y + y) * sel.width + region.x + x] / 255;\n }\n } else ones.fill(1);\n coverage.accumulate(region, ones, 1);\n const color = parseColor(options.color ?? this.state.paint.foreground);\n const background = parseColor(this.state.paint.background);\n const grey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode,\n color: target.channel !== 'pixels' ? grey(color) : color,\n background: target.channel !== 'pixels' ? grey(background) : background,\n opacity,\n blend: target.channel !== 'pixels' ? 'normal' : blend,\n lockTransparency: target.lockTransparency,\n selection: null,\n offset: target.offset,\n };\n const surface = paintCoverageInto(target.surface, coverage, apply, [region]);\n const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));\n this.commitLayers(next);\n return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };\n }\n\n // --- Adjustments ---\n\n /** The pixel layer the base sliders edit: the active pixel layer, else the Background. */\n private developLayer(): PixelLayer | null {\n return developTarget(this.state.layers, this.state.activeLayerId);\n }\n\n /**\n * Sets base adjustment values as one undo step (unless `commitToHistory` is false). The base\n * sliders edit the `develop` filter of the active pixel layer (or the Background), `blur` its\n * blur filter (or `legacyOutputBlur` in migrated documents), `opacity` the output opacity, and\n * `layers` replaces the adjustment layers.\n */\n public setAdjustments(partial: Partial<ImageAdjustments>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const target = this.developLayer();\n let layers = this.state.layers;\n if (target && (DEVELOP_KEYS.some((k) => k in partial) || 'process' in partial)) {\n layers = mapLayer(layers, target.id, (l) => withDevelop(l as PixelLayer, partial));\n }\n if ('blur' in partial && typeof partial.blur === 'number') {\n const blur = Math.max(0, partial.blur);\n if (this.state.legacyOutputBlur !== undefined) this.state.legacyOutputBlur = blur;\n else if (target) layers = mapLayer(layers, target.id, (l) => withBlur(l as PixelLayer, blur));\n }\n if ('opacity' in partial && typeof partial.opacity === 'number') {\n this.state.outputOpacity = Math.min(1, Math.max(0, partial.opacity));\n }\n if ('layers' in partial) {\n layers = this.replaceAdjustmentNodes(layers, normalizeAdjustmentLayers(partial.layers));\n }\n this.state.layers = layers;\n this.notify();\n }\n\n /** True when the adjustments use an older rendering process than {@link LATEST_PROCESS_VERSION}. */\n public get isProcessOutdated(): boolean {\n this.syncViews();\n return normalizeProcessVersion(this.state.adjustments.process) < LATEST_PROCESS_VERSION;\n }\n\n /**\n * Update to Current Process: re-interprets the current adjustment values with the latest\n * rendering process (one undo step). Returns false when already current.\n */\n public upgradeProcess(): boolean {\n if (!this.isProcessOutdated) return false;\n this.setAdjustments({ process: LATEST_PROCESS_VERSION });\n return true;\n }\n\n /** Resets the base sliders (latest process). Adjustment layers are kept; see `clearAdjustmentLayers`. */\n public resetAdjustments(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) =>\n withBlur(withDevelop(l as PixelLayer, { ...DEFAULT_ADJUSTMENTS, process: LATEST_PROCESS_VERSION }), 0)\n );\n }\n delete this.state.legacyOutputBlur;\n this.state.outputOpacity = 1;\n this.notify();\n }\n\n // --- Adjustment layers ---\n\n /** The adjustment layers in stack order, bottom (first) to top (last). */\n public getAdjustmentLayers(): AdjustmentLayer[] {\n this.syncViews();\n return this.state.adjustments.layers ?? [];\n }\n\n public getAdjustmentLayer(id: string): AdjustmentLayer | undefined {\n return this.getAdjustmentLayers().find((l) => l.id === id);\n }\n\n private adjustmentNodes(layers: readonly ImageLayer[] = this.state.layers): AdjustmentLayerNode[] {\n return flattenLayers(layers).filter((l): l is AdjustmentLayerNode => l.type === 'adjustment');\n }\n\n /**\n * Where a new layer goes when no position is given: directly above the topmost layer that is\n * not an annotation, so it adjusts what adjustment layers always adjusted (annotations stay on top).\n */\n private defaultInsertIndex(layers: readonly ImageLayer[] = this.state.layers): number {\n let i = layers.length;\n while (i > 0 && layers[i - 1].type === 'annotation') i--;\n return i;\n }\n\n /** Inserts `node` by `aboveId` / `parentId` / adjustment index / default position. */\n private placeLayer(\n layers: ImageLayer[],\n node: ImageLayer,\n where: { aboveId?: string; parentId?: string; adjustmentIndex?: number }\n ): ImageLayer[] {\n if (where.aboveId) {\n const loc = findLayer(layers, where.aboveId);\n if (loc) return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n if (where.parentId) {\n const loc = findLayer(layers, where.parentId);\n if (loc && loc.layer.type === 'group') return insertLayer(layers, node, loc.layer.id);\n }\n if (where.adjustmentIndex !== undefined) {\n const nodes = this.adjustmentNodes(layers);\n const at = Math.max(0, Math.min(nodes.length, Math.round(where.adjustmentIndex)));\n if (at < nodes.length) {\n const loc = findLayer(layers, nodes[at].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index);\n }\n if (nodes.length) {\n const loc = findLayer(layers, nodes[nodes.length - 1].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n }\n return insertLayer(layers, node, null, this.defaultInsertIndex(layers));\n }\n\n /** `setAdjustments({ layers })`: updates nodes by id in place, removes missing ones, adds new ones. */\n private replaceAdjustmentNodes(layers: ImageLayer[], next: AdjustmentLayer[]): ImageLayer[] {\n const { width, height } = this.naturalSize;\n const byId = new Map(next.map((l) => [l.id, l]));\n let out = layers;\n for (const node of this.adjustmentNodes(layers)) {\n const update = byId.get(node.id);\n out = mapLayer(out, node.id, () => (update ? mergeAdjustmentNode(node, update, width, height) : null));\n }\n const existing = new Set(this.adjustmentNodes(out).map((n) => n.id));\n for (const l of next) {\n if (existing.has(l.id)) continue;\n out = this.placeLayer(out, adjustmentNodeFromLegacy(l, width, height), { adjustmentIndex: Infinity });\n }\n // Keep the given order among the adjustment layers.\n const order = next.map((l) => l.id);\n for (let to = 0; to < order.length; to++) out = this.moveAdjustmentNode(out, order[to], to);\n return out;\n }\n\n /**\n * Adds an adjustment layer and returns its id, as one undo step. With no position it goes\n * above the other adjustment layers (or, without any, above the topmost non-annotation layer,\n * so annotations stay on top); `index` counts adjustment layers (0 = below the others);\n * `aboveId` / `parentId` place it anywhere in the stack. `kind` alone gives neutral parameters.\n */\n public addAdjustmentLayer<K extends AdjustmentKind>(init: K | AdjustmentLayerInit<K>, index?: number): string {\n const spec: AdjustmentLayerInit<K> = typeof init === 'string' ? { kind: init } : init;\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer = createAdjustmentLayer(spec.kind, {\n id: spec.id && !ids.has(spec.id) ? spec.id : undefined,\n name: spec.name,\n params: spec.params,\n enabled: spec.enabled,\n opacity: spec.opacity,\n blend: spec.blend,\n mask: spec.mask ?? undefined,\n });\n while (ids.has(layer.id)) layer.id = newLayerId('adj');\n const { width, height } = this.naturalSize;\n this.recordHistory();\n const node = adjustmentNodeFromLegacy(layer, width, height);\n const positioned = Boolean(spec.aboveId || spec.parentId);\n this.state.layers = this.placeLayer(this.state.layers, node, {\n aboveId: spec.aboveId,\n parentId: spec.parentId,\n adjustmentIndex: index !== undefined ? index : positioned ? undefined : Infinity,\n });\n this.notify();\n return layer.id;\n }\n\n /**\n * Updates a layer (`params` merged into the current ones, then normalised). Pass\n * `commitToHistory = false` for the moves of a drag after recording its start once.\n * Returns false for an unknown id.\n */\n public updateAdjustmentLayer(id: string, update: AdjustmentLayerUpdate, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n const { width, height } = this.naturalSize;\n const node = loc.layer;\n const merged = normalizeAdjustmentLayer({\n id: node.id,\n kind: node.kind,\n enabled: update.enabled ?? node.visible,\n opacity: update.opacity ?? node.opacity,\n blend: update.blend ?? node.blend,\n name: update.name ?? node.name,\n params: { ...node.params, ...(update.params ?? {}) },\n });\n if (!merged) return false;\n if (commitToHistory) this.recordHistory();\n const next = {\n ...node,\n visible: merged.enabled,\n opacity: merged.opacity,\n blend: merged.blend,\n name: merged.name ?? '',\n params: merged.params,\n } as AdjustmentLayerNode;\n if (update.mask !== undefined) {\n const mask = update.mask ? rasterMaskFromLegacy(update.mask, width, height) : null;\n if (mask) next.mask = mask;\n else delete next.mask;\n }\n this.state.layers = mapLayer(this.state.layers, id, () => next);\n this.notify();\n return true;\n }\n\n /** Removes a layer as one undo step. Returns false for an unknown id. */\n public removeAdjustmentLayer(id: string): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, id, () => null);\n if (this.state.activeLayerId === id) this.state.activeLayerId = null;\n this.notify();\n return true;\n }\n\n private moveAdjustmentNode(layers: ImageLayer[], id: string, toIndex: number): ImageLayer[] {\n const nodes = this.adjustmentNodes(layers);\n const from = nodes.findIndex((n) => n.id === id);\n if (from === -1) return layers;\n const to = Math.max(0, Math.min(nodes.length - 1, Math.round(toIndex)));\n if (to === from) return layers;\n const node = nodes[from];\n const anchor = nodes[to];\n const without = mapLayer(layers, id, () => null);\n const loc = findLayer(without, anchor.id)!;\n return insertLayer(without, node, loc.parent?.id ?? null, to > from ? loc.index + 1 : loc.index);\n }\n\n /** Moves a layer to `toIndex` among the adjustment layers (0 = bottom) as one undo step. Returns false if nothing moved. */\n public moveAdjustmentLayer(id: string, toIndex: number): boolean {\n const next = this.moveAdjustmentNode(this.state.layers, id, toIndex);\n if (next === this.state.layers) return false;\n this.recordHistory();\n this.state.layers = next;\n this.notify();\n return true;\n }\n\n /** Shows or hides a layer (flips it without `enabled`) as one undo step. */\n public toggleAdjustmentLayer(id: string, enabled?: boolean): boolean {\n const layer = this.getAdjustmentLayer(id);\n if (!layer) return false;\n const next = enabled ?? !layer.enabled;\n if (next === layer.enabled) return false;\n return this.updateAdjustmentLayer(id, { enabled: next });\n }\n\n /** Removes every adjustment layer as one undo step. */\n public clearAdjustmentLayers(): void {\n const nodes = this.adjustmentNodes();\n if (!nodes.length) return;\n this.recordHistory();\n let layers = this.state.layers;\n for (const n of nodes) layers = mapLayer(layers, n.id, () => null);\n this.state.layers = layers;\n if (nodes.some((n) => n.id === this.state.activeLayerId)) this.state.activeLayerId = null;\n this.notify();\n }\n\n // --- Selection ---\n\n /** The active selection, or `null`. */\n public getSelection(): SelectionMask | null {\n return this.state.selection ?? null;\n }\n\n public get hasSelection(): boolean {\n return Boolean(this.state.selection?.bounds);\n }\n\n private get naturalSize(): { width: number; height: number } {\n return {\n width: Math.max(1, Math.round(this.state.imageDimensions.width)),\n height: Math.max(1, Math.round(this.state.imageDimensions.height)),\n };\n }\n\n private dropMismatchedSelection(): void {\n const sel = this.state.selection;\n if (!sel) return;\n const { width, height } = this.naturalSize;\n if (sel.width !== width || sel.height !== height) this.state.selection = null;\n }\n\n /**\n * Replaces the selection, or combines `mask` with it by `mode`, as one undo step. A mask of a\n * different size (e.g. an AI segmentation at another resolution) is resampled to the image.\n * Returns false when nothing changed.\n */\n public setSelection(mask: SelectionMask | null, mode: SelectionMode = 'replace'): boolean {\n const { width, height } = this.naturalSize;\n let next: SelectionMask | null = null;\n if (mask) {\n const sized = mask.width === width && mask.height === height ? mask : resizeSelection(mask, width, height);\n next = combineSelections(this.state.selection, sized, mode);\n } else if (mode !== 'replace') {\n return false;\n }\n const prev = normalizeSelection(this.state.selection ?? null);\n if (prev === next || (prev && next && sameSelection(prev, next))) return false;\n this.recordHistory();\n this.state.selection = next;\n this.notify();\n return true;\n }\n\n /** Select › All. */\n public selectAll(): boolean {\n const { width, height } = this.naturalSize;\n return this.setSelection(fullSelection(width, height));\n }\n\n /** Select › Deselect. */\n public deselect(): boolean {\n return this.setSelection(null);\n }\n\n /** Select › Inverse (with nothing selected, selects everything). */\n public invertSelection(): boolean {\n const { width, height } = this.naturalSize;\n const current = this.state.selection ?? createSelectionMask(width, height);\n return this.setSelection(invertSelectionMask(current));\n }\n\n private shapeCommand(mask: SelectionMask, options: SelectionCommandOptions): boolean {\n const shaped = options.feather && options.feather > 0 ? modifySelectionMask(mask, 'feather', options.feather) : mask;\n return this.setSelection(shaped, options.mode ?? 'replace');\n }\n\n /** Rectangular marquee in natural image pixels. */\n public selectRect(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(rectSelection(width, height, rect, options), options);\n }\n\n /** Elliptical marquee inscribed in `rect`. */\n public selectEllipse(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(ellipseSelection(width, height, rect, options), options);\n }\n\n /** Lasso / polygonal lasso. */\n public selectPolygon(points: ImagePoint[], options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(polygonSelection(width, height, points, options), options);\n }\n\n /**\n * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker\n * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)\n * these are also the Background layer's pixels.\n */\n public setSamplePixels(pixels: PixelBuffer | null): void {\n this.samplePixels = pixels;\n this.samplePixelsOverride = pixels !== null;\n }\n\n /**\n * The pixels the selection tools sample: the source image at natural size, or with\n * `sampleMerged` the composite of the layers (no crop, rotation or annotations). `null`\n * without pixel access (no DOM, tainted canvas).\n */\n public async getSamplePixels(options: SampleOptions = {}): Promise<PixelBuffer | null> {\n const { width, height } = this.naturalSize;\n let base = options.pixels ?? this.samplePixels;\n if (!base) {\n try {\n const canvas = await this.renderSource();\n const ctx = canvas.getContext('2d');\n const data = ctx?.getImageData?.(0, 0, width, height);\n if (data && data.data && data.data.length >= width * height * 4) {\n base = { width, height, data: data.data };\n this.samplePixels = base;\n this.samplePixelsOverride = false;\n }\n } catch {\n base = null;\n }\n }\n if (!base) return null;\n if (base.width !== width || base.height !== height) {\n if (!this.samplePixelsOverride && !options.pixels) this.samplePixels = null;\n return null;\n }\n const legacy = legacyView(this.stack(), width, height, null).exact;\n if (options.sample === 'active-layer') {\n const layer = this.getActiveLayer();\n if (!layer || layer.type !== 'pixel') {\n throw new LayerOperationError('Sampling the active layer needs an active pixel layer; select one, or sample the composite.');\n }\n const surface = layer.source === 'document' && options.pixels ? surfaceFromPixels(options.pixels, 4) : this.surfaceOf(layer);\n return { width, height, data: readRegion(surface, { x: -layer.offset.x, y: -layer.offset.y, width, height }) };\n }\n if (legacy && !options.sampleMerged) return base;\n this.syncViews();\n if (legacy && !options.pixels) {\n // The old shape: the source through the base adjustments and adjustment layers.\n const copy = { width: base.width, height: base.height, data: new Uint8ClampedArray(base.data as ArrayLike<number>) };\n applyAdjustments(copy as unknown as ImageData, legacyView(this.stack(), width, height, null).adjustments);\n return copy;\n }\n const pixels = options.pixels;\n const sources: CompositorSources = {\n width,\n height,\n surfaceOf: (l) => (l.type === 'pixel' && l.source === 'document' && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l)),\n version: pixels ? 'pixels' : this.sourceVersion(),\n };\n // Without `sampleMerged`, the pixels as painted: adjustment layers and filters left out.\n const stack = withoutAnnotations(this.state.layers);\n const layers = options.sampleMerged ? stack : pixelsOnly(stack);\n const data = new LayerCompositor().composite(layers, { x: 0, y: 0, width, height }, sources);\n return { width, height, data };\n }\n\n private async requirePixels(options: SampleOptions): Promise<PixelBuffer> {\n const pixels = await this.getSamplePixels(options);\n if (!pixels) throw new Error('The image pixels are not available (no canvas access); pass `pixels`.');\n return pixels;\n }\n\n /**\n * Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when\n * nothing is selected).\n */\n public async magicWand(\n point: ImagePoint,\n options: MagicWandOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(magicWand(pixels, point, options), options);\n return this.getSelection();\n }\n\n /** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */\n public async selectColorRange(\n samples: ColorRangeSample[],\n options: ColorRangeOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(colorRange(pixels, samples, options), options);\n return this.getSelection();\n }\n\n /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */\n public modifySelection(op: SelectionModifyOp, radius: number): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n return this.setSelection(modifySelectionMask(sel, op, radius));\n }\n\n /**\n * Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)\n * to the image's edges with a guided filter, then smooth / feather / contrast / shift edge.\n */\n public async refineSelection(params: RefineEdgeParams, options: SampleOptions = {}): Promise<boolean> {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;\n return this.setSelection(refineSelectionMask(pixels, sel, params));\n }\n\n /**\n * Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or\n * RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.\n */\n public async loadSelectionFromImage(\n source: string | PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number; mode?: SelectionMode } = {}\n ): Promise<SelectionMask | null> {\n let pixels: PixelBuffer;\n if (typeof source === 'string') {\n const img = await this.renderer.imageCache.get(source);\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const canvas = this.renderer.createCanvas(w, h);\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.drawImage(img, 0, 0, w, h);\n const data = ctx?.getImageData?.(0, 0, w, h);\n if (!data) throw new Error('The mask image pixels are not available (no canvas access); pass pixels.');\n pixels = { width: w, height: h, data: data.data };\n } else {\n pixels = source;\n }\n this.setSelection(selectionFromPixels(pixels, options), options.mode ?? 'replace');\n return this.getSelection();\n }\n\n /** Quick Mask view (tints the unselected area). View state: no undo step. */\n public setQuickMask(on: boolean): void {\n if (Boolean(this.state.quickMask) === on) return;\n this.state.quickMask = on;\n this.notify();\n }\n\n // --- Layer masks ---\n\n private maskFromSelection(sel: SelectionMask, props: LayerMaskProps = {}): RasterMask {\n const { width, height } = this.naturalSize;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const mask: RasterMask = { surface: surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0), defaultValue: 0 };\n return withMaskProps(mask, props);\n }\n\n /**\n * Adds the selection as the layer's mask (Reveal Selection), replacing any mask, as one undo\n * step. Without a selection, returns false.\n */\n public applySelectionAsLayerMask(layerId: string, props: LayerMaskProps = {}): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds || !this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, this.maskFromSelection(sel, props));\n }\n\n private setLayerMask(layerId: string, mask: RasterMask | null, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, layerId);\n if (!loc) return false;\n if (commitToHistory) this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n if (!mask && this.state.activeLayerId === layerId) this.state.activeTarget = 'pixels';\n this.notify();\n return true;\n }\n\n /** Sets (from a selection-shaped mask) or removes (`null`) a layer's mask as one undo step. */\n public setAdjustmentLayerMask(layerId: string, mask: SelectionMask | null, props: LayerMaskProps = {}): boolean {\n if (!this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, mask ? this.maskFromSelection(mask, props) : null);\n }\n\n /** Changes a layer mask's density (0..1), feather (px) or enabled flag; the mask pixels are kept. */\n public setAdjustmentLayerMaskProps(layerId: string, props: LayerMaskProps, commitToHistory = true): boolean {\n const layer = this.getLayer(layerId);\n if (!layer || layer.type !== 'adjustment' || !layer.mask) return false;\n return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);\n }\n\n /** Load Selection from a layer mask (its pixels; density and feather are not applied). */\n public selectionFromLayerMask(layerId: string, mode: SelectionMode = 'replace'): boolean {\n const layer = this.getLayer(layerId);\n if (!layer?.mask || layer.mask.source) return false;\n const { width, height } = this.naturalSize;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });\n const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));\n return this.setSelection(sel, mode);\n }\n\n // --- Histogram ---\n\n /**\n * Histogram of the last rendered preview (after adjustments and layers, before annotations),\n * or `null` before the first render or without canvas pixel access.\n */\n public getHistogram(): ImageHistogram | null {\n return this.renderer.previewHistogram;\n }\n\n /** Calls `listener` now and whenever a render produces a new preview histogram. */\n public subscribeHistogram(listener: (histogram: ImageHistogram | null) => void): () => void {\n this.histogramListeners.add(listener);\n listener(this.renderer.previewHistogram);\n return () => {\n this.histogramListeners.delete(listener);\n };\n }\n\n // --- Depth Masking (Gleamforge) ---\n\n /** Sets the depth mask (a parametric mask on the active pixel layer, or the Background) as one undo step. */\n public setDepthMask(partial: Partial<DepthMaskOptions>): void {\n this.recordHistory();\n this.syncViews();\n const target = this.developLayer();\n if (target) {\n const merged: DepthMaskOptions = { ...this.state.depthMask, ...partial };\n merged.depthRange = [...merged.depthRange] as [number, number];\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n if (isDefaultDepthMask(merged)) {\n if (next.mask?.source) delete next.mask;\n } else next.mask = depthRasterMask(merged);\n return next;\n });\n }\n this.notify();\n }\n\n public resetDepthMask(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target?.mask?.source) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n delete next.mask;\n return next;\n });\n }\n this.notify();\n }\n\n // --- Annotations & Layers ---\n\n private annotationLayer(id: string): AnnotationLayer | undefined {\n return flattenLayers(this.state.layers).find(\n (l): l is AnnotationLayer => l.type === 'annotation' && l.annotation.id === id\n );\n }\n\n /** Adds an annotation as its own layer (top of the stack unless `aboveId` is given). */\n public addAnnotation(annotation: CreateAnnotationPayload, placement: AnnotationPlacement = {}): string {\n this.recordHistory();\n const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n const fullAnnotation = { ...annotation, id } as Annotation;\n const layer = annotationLayerFor(fullAnnotation);\n const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;\n this.state.layers = loc\n ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1)\n : insertLayer(this.state.layers, layer, null);\n this.state.selectedAnnotationId = id;\n this.notify();\n return id;\n }\n\n public updateAnnotation(id: string, partial: Partial<Annotation>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const layer = this.annotationLayer(id);\n if (layer) {\n const annotation = { ...layer.annotation, ...partial, id } as Annotation;\n const name = layer.name === annotationLayerName(layer.annotation) ? annotationLayerName(annotation) : layer.name;\n this.state.layers = mapLayer(this.state.layers, layer.id, () => ({ ...layer, annotation, name }));\n }\n this.notify();\n }\n\n public removeAnnotation(id: string): void {\n this.recordHistory();\n const layer = this.annotationLayer(id);\n if (layer) this.state.layers = mapLayer(this.state.layers, layer.id, () => null);\n if (this.state.selectedAnnotationId === id) {\n this.state.selectedAnnotationId = null;\n }\n this.notify();\n }\n\n public clearAnnotations(): void {\n this.recordHistory();\n let layers = this.state.layers;\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.layers = layers;\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n public selectAnnotation(id: string | null): void {\n this.state.selectedAnnotationId = id;\n this.notify();\n }\n\n // --- Zoom & Pan ---\n\n public setZoom(zoom: number): void {\n this.state.zoom = Math.max(0.1, Math.min(10, zoom));\n this.notify();\n }\n\n public zoomIn(factor = 1.2): void {\n this.setZoom(this.state.zoom * factor);\n }\n\n public zoomOut(factor = 1.2): void {\n this.setZoom(this.state.zoom / factor);\n }\n\n public resetZoom(): void {\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.notify();\n }\n\n public setPan(pan: ImagePoint): void {\n this.state.pan = pan;\n this.notify();\n }\n\n public setComparing(comparing: boolean): void {\n if (this.state.isOriginalCompared === comparing) return;\n this.state.isOriginalCompared = comparing;\n this.notify();\n }\n\n public setFit(fit: ImageEditorFit): void {\n if (this.state.fit === fit) return;\n this.state.fit = fit;\n this.notify();\n }\n\n // --- Rendering & Export ---\n\n private blankSource: HTMLCanvasElement | null = null;\n\n /**\n * What the renderer draws from: the source image, or, for a document without one (an opened\n * PSD, a v3 document loaded on its own), a transparent canvas of the document size, since every\n * pixel then comes from the layers. `null` when there is nothing to draw.\n */\n private drawSource(): CanvasImageSource | string | null {\n const source = this.imageElement || this.state.sourceUrl;\n if (source) return source;\n if (!this.isSourceless()) return null;\n const { width, height } = this.naturalSize;\n if (!this.blankSource || this.blankSource.width !== width || this.blankSource.height !== height) {\n this.blankSource = this.renderer.createCanvas(width, height);\n }\n return this.blankSource;\n }\n\n public async render(targetCanvas: HTMLCanvasElement, options?: RenderOptions): Promise<void> {\n const source = this.drawSource();\n if (!source) return;\n this.syncViews();\n const dirty = this.pendingDirty;\n this.pendingDirty = [];\n try {\n await this.renderer.renderToCanvas(targetCanvas, source, this.state, {\n ...options,\n dirty: dirty === 'all' ? undefined : dirty,\n checkpointLayerId: this.state.activeLayerId,\n });\n } catch {\n // In non-DOM or test mock environments without image loading, safely ignore\n }\n }\n\n public async renderExport(options?: ExportOptions): Promise<HTMLCanvasElement> {\n const source = this.drawSource();\n if (!source) {\n return this.renderer.createCanvas(1, 1);\n }\n this.syncViews();\n if (options?.layerIds) {\n const keep = new Set(options.layerIds);\n const layers = flattenLayers(this.state.layers).filter((l) => keep.has(l.id));\n return this.renderer.renderExport(source, { ...this.state, layers }, options);\n }\n return this.renderer.renderExport(source, this.state, options);\n }\n\n /**\n * Renders the edited image (adjustments, layers and, by default, annotations) in the\n * source image's own orientation and frame, ignoring crop, rotation and flips, so it lines\n * up with natural-pixel coordinates (selections, `select_region`). `scale` ≤ 1 shrinks it.\n * Returns `null` when no image is loaded.\n */\n public async renderSourceView(options: { scale?: number; includeAnnotations?: boolean } = {}): Promise<HTMLCanvasElement | null> {\n const source = this.drawSource();\n if (!source) return null;\n this.syncViews();\n const { width, height } = this.state.imageDimensions;\n const k = Math.min(1, Math.max(0, options.scale ?? 1));\n return this.renderer.renderExport(\n source,\n { ...this.state, crop: null, transform: { rotate: 0, flipH: false, flipV: false } },\n {\n width: Math.max(1, Math.round(width * k)),\n height: Math.max(1, Math.round(height * k)),\n includeAnnotations: options.includeAnnotations,\n }\n );\n }\n\n public async toDataURL(options: ExportOptions = {}): Promise<string> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n return canvas.toDataURL(format, quality);\n }\n\n public async toBlob(options: ExportOptions = {}): Promise<Blob> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n\n return new Promise((resolve, reject) => {\n if (canvas.toBlob) {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob);\n else reject(new Error('Failed to create Blob from canvas'));\n }, format, quality);\n } else {\n // Fallback for environments lacking canvas.toBlob\n try {\n const dataUrl = canvas.toDataURL(format, quality);\n const parts = dataUrl.split(',');\n const byteStr = atob(parts[1]);\n const ab = new ArrayBuffer(byteStr.length);\n const ia = new Uint8Array(ab);\n for (let i = 0; i < byteStr.length; i++) {\n ia[i] = byteStr.charCodeAt(i);\n }\n resolve(new Blob([ab], { type: format }));\n } catch (err) {\n reject(err);\n }\n }\n });\n }\n\n // --- AI Masking & Transformation Helpers ---\n\n public async renderMask(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<HTMLCanvasElement> {\n this.syncViews();\n return this.renderer.renderMask(this.state, options);\n }\n\n public async toMaskDataURL(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<string> {\n const canvas = await this.renderMask(options);\n return canvas.toDataURL('image/png');\n }\n\n /**\n * Renders the current source image at natural size with no crop, rotation, flip,\n * adjustments or annotations. This is the image that `toMaskDataURL()` is aligned with\n * and the space that `applyInpaintedImage` / `applyBackgroundRemovedImage` expect back.\n */\n public async renderSource(): Promise<HTMLCanvasElement> {\n const { width, height } = this.state.imageDimensions;\n const canvas = this.renderer.createCanvas(width, height);\n const source = this.imageElement || this.state.sourceUrl;\n const ctx = canvas.getContext('2d');\n if (!ctx || !source) return canvas;\n const img = typeof source === 'string' ? await this.renderer.imageCache.get(source) : source;\n ctx.drawImage(img, 0, 0, width, height);\n return canvas;\n }\n\n /**\n * The image an AI hook should edit: the source (`toSourceDataURL`) when results replace the\n * Background, or the composite of the layers without annotations, in the source frame, when\n * they go to a new layer or mask (see `applyInpaintedImage`).\n */\n public async toAIInputDataURL(options: AIResultOptions = {}): Promise<string> {\n const target = options.target ?? this.defaultAITarget();\n if (target === 'replace') return this.toSourceDataURL();\n const canvas = await this.renderSourceView({ includeAnnotations: false });\n return canvas ? canvas.toDataURL('image/png') : this.toSourceDataURL();\n }\n\n public async toSourceDataURL(options: Pick<ExportOptions, 'format' | 'quality'> = {}): Promise<string> {\n const canvas = await this.renderSource();\n return canvas.toDataURL(options.format || 'image/png', options.quality ?? 0.92);\n }\n\n /**\n * Whether AI results go to layers: the document has more than one pixel layer or group (adjustment\n * and annotation layers are edits of the image, not images of their own).\n */\n /** `new-layer` in multi-layer documents and documents without a source image, else `replace`. */\n private defaultAITarget(): 'new-layer' | 'replace' {\n return this.isMultiLayer || this.isSourceless() ? 'new-layer' : 'replace';\n }\n\n /** No source image, and no layer reading one (an opened PSD): the layers are the whole document. */\n private isSourceless(): boolean {\n if (this.imageElement || this.state.sourceUrl || !this.state.layers.length) return false;\n return !flattenLayers(this.state.layers).some((l) => l.type === 'pixel' && l.source === 'document');\n }\n\n public get isMultiLayer(): boolean {\n return flattenLayers(this.state.layers).filter((l) => l.type === 'pixel' || l.type === 'group').length > 1;\n }\n\n /**\n * Applies an inpainted result (natural image space, see `toSourceDataURL`) as ONE undoable step.\n * In a document with several layers (see `isMultiLayer`), or with `target: 'new-layer'`, the\n * result goes to a new pixel layer above the active layer, limited to the selection. Otherwise\n * (`target: 'replace'`, the single-layer default) it replaces the Background's pixels and clears\n * the mask annotations, as before. Resolves to the new layer's id in new-layer mode.\n */\n public async applyInpaintedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, true);\n return;\n }\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const data = new Uint8ClampedArray(px.data as ArrayLike<number>);\n const sel = this.state.selection?.bounds ? this.state.selection : null;\n if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round((data[i] * sel.data[p]) / 255);\n return this.addPixelLayer({ name: 'Inpaint', pixels: { width, height, data } });\n }\n\n /**\n * Applies a background-removed result as ONE undoable step. In a document with several layers\n * (or with `target: 'new-layer'`) the result's alpha becomes the mask of the active pixel layer\n * (or the Background), replacing its mask. Otherwise (`target: 'replace'`, the single-layer\n * default) it replaces the Background's pixels; crop and annotations are kept (rescaled if the\n * new image has different dimensions). Resolves to the masked layer's id in mask mode.\n */\n public async applyBackgroundRemovedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, false);\n return;\n }\n const target = this.developLayer();\n if (!target) throw new LayerOperationError('There is no pixel layer to mask.');\n requireUnlocked(target, 'all', 'mask it');\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const alpha = new Uint8ClampedArray(width * height);\n for (let p = 0; p < alpha.length; p++) alpha[p] = (px.data as ArrayLike<number>)[p * 4 + 3];\n let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);\n if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);\n this.commitLayers(setLayerMaskOp(this.state.layers, target.id, { surface, defaultValue: 0 }));\n return target.id;\n }\n\n /**\n * Rasterize Layer: an annotation layer becomes a pixel layer with the same id, name and\n * properties (needs a canvas), and a pixel layer's filters are baked into its pixels. One undo\n * step. Returns the layer id.\n */\n public async rasterizeLayer(id: string): Promise<string> {\n const layer = requireLayer(this.state.layers, id).layer;\n requireUnlocked(layer, 'all', 'rasterize it');\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n let next: PixelLayer;\n if (layer.type === 'annotation') {\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n const surface = await this.compositeToSurface([content], this.paintBounds(layer));\n next = { type: 'pixel', id: layer.id, name: layer.name, visible: layer.visible, opacity: layer.opacity, blend, surface, offset: { x: 0, y: 0 } };\n if (layer.clipped) next.clipped = true;\n if (layer.mask) next.mask = layer.mask;\n if (layer.locks) next.locks = layer.locks;\n if (this.state.selectedAnnotationId === layer.annotation.id) this.state.selectedAnnotationId = null;\n } else if (layer.type === 'pixel') {\n if (!layer.filters?.length) throw new LayerOperationError(`Layer \"${layer.name || id}\" is already pixels with no filters to bake.`);\n requireUnlocked(layer, 'pixels', 'rasterize it');\n const spatial = layer.filters.some((f) => f.enabled && f.kind === 'blur');\n const content: PixelLayer = { ...layer, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n const rect = spatial ? canvas : this.getLayerBounds(id) ?? canvas;\n const surface = await this.compositeToSurface([content], rect);\n next = { ...layer, surface, offset: { x: 0, y: 0 } };\n delete next.filters;\n delete next.source;\n if (layer.mask && (layer.offset.x || layer.offset.y)) {\n // The mask lives in layer space: keep it where it was in the document.\n next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };\n }\n } else if (layer.type === 'smart') {\n return this.rasterizeSmartObject(id);\n } else {\n throw new LayerOperationError(`Only annotation layers, smart objects and pixel layers with filters rasterize; \"${layer.name || id}\" is a ${layer.type} layer. Merge it down or flatten instead.`);\n }\n this.commitLayers(mapLayer(this.state.layers, id, () => next));\n return id;\n }\n\n // --- PSD ---\n\n /** Pixels per inch of the last opened PSD (written back by `toPsd`). */\n private psdResolution: number | undefined;\n\n /**\n * Opens PSD / PSB bytes as the document (one undo step): the layers replace the stack and the\n * document has no source image afterwards (every pixel comes from the layers). What the model\n * cannot hold is approximated or left out; `warnings` says what, naming the layers.\n * Throws `PsdError` for files that cannot be read at all.\n */\n public loadPsd(bytes: Uint8Array): { layerIds: string[]; warnings: string[]; width: number; height: number; summary: PsdImportResult['summary'] } {\n const result = importPsd(bytes, { newId: () => newLayerId() });\n this.abandonContents();\n this.recordHistory();\n const s = this.state;\n this.imageElement = null;\n s.sourceUrl = null;\n s.imageDimensions = { width: result.width, height: result.height };\n if (!this.samplePixelsOverride) this.samplePixels = null;\n s.layers = result.layers;\n s.crop = null;\n s.transform = { ...DEFAULT_TRANSFORM_VALUES };\n s.outputOpacity = 1;\n delete s.legacyOutputBlur;\n s.selection = null;\n s.selectedAnnotationId = null;\n const pixels = flattenLayers(result.layers).filter((l) => l.type === 'pixel');\n s.activeLayerId = pixels.length ? pixels[pixels.length - 1].id : null;\n s.activeTarget = 'pixels';\n this.psdResolution = result.resolution;\n if (result.patterns.length) s.patterns = result.patterns;\n else delete s.patterns;\n if (result.globalLight) s.globalLight = result.globalLight;\n else delete s.globalLight;\n this.lastLoadProblems = result.warnings;\n this.notify();\n return {\n layerIds: flattenLayers(result.layers).map((l) => l.id),\n warnings: result.warnings,\n width: result.width,\n height: result.height,\n summary: result.summary,\n };\n }\n\n /**\n * The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked\n * into pixels and masks, annotation layers are rasterized (in a browser; elsewhere they are left\n * out), and the merged image is our composite. `warnings` lists every such change.\n */\n public async toPsd(): Promise<{ bytes: Uint8Array; warnings: string[]; psb: boolean; width: number; height: number }> {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n const warnings: string[] = [];\n const note = new Map<string, string[]>();\n const add = (msg: string, name: string) => note.set(msg, [...(note.get(msg) ?? []), name]);\n const canRasterize = this.renderer.canRasterize();\n await this.renderer.prepareLayerAssets(this.state.layers, width, height);\n const sources = this.renderer.compositorSources(width, height);\n const compositor = new LayerCompositor();\n\n const bakeMask = (layer: ImageLayer): RasterMask | undefined => {\n const mask = layer.mask;\n if (!mask?.source) return mask;\n add('Parametric (depth) masks were baked into pixel masks', layer.name);\n const plain = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } } as ImageLayer;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const coverage = compositor.maskCoverage(plain, canvas, sources);\n const bytes = new Uint8ClampedArray(width * height);\n if (coverage) for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(coverage[i] * 255);\n else bytes.fill(255);\n const baked: RasterMask = { surface: surfaceFromPixels({ width, height, data: bytes }, 1, -ox, -oy), defaultValue: 0 };\n if (mask.density !== undefined && mask.density < 1) baked.density = mask.density;\n if (mask.feather) baked.feather = mask.feather;\n if (mask.enabled === false) baked.enabled = false;\n return baked;\n };\n\n const prepare = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n const p = await prepareLayer(layer);\n // `exportPsd` writes it as Normal; the merged image keeps the paint mix.\n if (p?.blend === 'pigment') add('The Pigment blend mode has no PSD equivalent and was written as Normal', layer.name);\n return p;\n };\n\n const prepareLayer = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n if (layer.type === 'group') {\n const children: ImageLayer[] = [];\n for (const c of layer.children) {\n const p = await prepare(c);\n if (p) children.push(p);\n }\n return { ...layer, children, mask: bakeMask(layer) } as ImageLayer;\n }\n if (layer.type === 'adjustment') return { ...layer, mask: bakeMask(layer) } as ImageLayer;\n if (layer.type === 'annotation') {\n if (!canRasterize) {\n add('Annotation layers need a canvas (a browser) to rasterize and were left out', layer.name);\n return null;\n }\n add('Annotation layers were written as pixel layers', layer.name);\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n delete content.effects;\n const data = compositor.composite([content], canvas, sources);\n const out: PixelLayer = {\n type: 'pixel',\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend,\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n if (layer.clipped) out.clipped = true;\n if (layer.locks) out.locks = layer.locks;\n if (layer.effects) out.effects = layer.effects;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n return out;\n }\n if (layer.type === 'smart') {\n add('Smart objects were written as pixel layers (their rendered pixels)', layer.name);\n const px = rasterizeSmartObjectOp([layer], layer.id).layer;\n const mask = bakeMask(px);\n if (mask) px.mask = mask;\n return px;\n }\n let out: PixelLayer = { ...layer };\n if (layer.source === 'document') {\n out.surface = this.surfaceOf(layer);\n delete out.source;\n }\n const active = layer.filters?.filter(isActiveFilter) ?? [];\n if (active.length) {\n add('Layer filters (develop, blur) were baked into the pixels', layer.name);\n const content: PixelLayer = { ...out, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n delete content.effects;\n const data = compositor.composite([content], canvas, sources);\n out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };\n }\n if (layer.filters?.some((f) => !f.enabled)) add('Disabled layer filters were left out', layer.name);\n delete out.filters;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\n };\n\n const prepared: ImageLayer[] = [];\n for (const l of this.state.layers) {\n const p = await prepare(l);\n if (p) prepared.push(p);\n }\n const s = this.state;\n if (s.crop) warnings.push('The crop is not applied; the PSD is the uncropped document.');\n if (s.transform.rotate || s.transform.flipH || s.transform.flipV) warnings.push('Rotation and flips are not applied; the PSD is in the source orientation.');\n if (s.outputOpacity < 1) warnings.push('The document opacity is not written; layers keep their own opacity.');\n if (s.legacyOutputBlur) warnings.push('The document-level blur of an old document is not written.');\n for (const [msg, names] of note) warnings.push(`${msg} (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`);\n\n const merged = compositor.composite(prepared, canvas, { ...sources, version: `psd:${Date.now()}` });\n const result = exportPsd({ width, height, layers: prepared, merged, resolution: this.psdResolution, patterns: this.state.patterns, globalLight: this.state.globalLight });\n warnings.push(...result.warnings);\n return { bytes: result.bytes, warnings, psb: result.psb, width, height };\n }\n\n private async replaceSource(src: string, clearAnnotations: boolean): Promise<void> {\n const next = await this.resolveSource(src);\n this.recordHistory();\n\n const prev = this.state.imageDimensions;\n const sx = next.dimensions.width / (prev.width || next.dimensions.width);\n const sy = next.dimensions.height / (prev.height || next.dimensions.height);\n\n this.imageElement = next.element;\n this.state.sourceUrl = next.sourceUrl;\n this.state.imageDimensions = next.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (this.state.selection && (sx !== 1 || sy !== 1)) {\n this.state.selection = normalizeSelection(\n resizeSelection(this.state.selection, Math.round(next.dimensions.width), Math.round(next.dimensions.height))\n );\n }\n\n let layers = this.state.layers;\n if (sx !== 1 || sy !== 1) {\n if (this.state.crop) {\n this.state.crop = clampCropToImage(\n {\n x: Math.round(this.state.crop.x * sx),\n y: Math.round(this.state.crop.y * sy),\n width: Math.round(this.state.crop.width * sx),\n height: Math.round(this.state.crop.height * sy),\n },\n next.dimensions.width,\n next.dimensions.height\n );\n }\n layers = resizeLegacyMasks(layers, prev, next.dimensions);\n for (const l of flattenLayers(layers)) {\n if (l.type === 'annotation') {\n layers = mapLayer(layers, l.id, () => ({ ...l, annotation: scaleAnnotation(l.annotation, sx, sy) }));\n }\n }\n }\n // The Background now shows the new source.\n const bg = developTarget(layers, null);\n if (bg && bg.source !== 'document') layers = mapLayer(layers, bg.id, () => ({ ...bg, source: 'document' }));\n\n if (clearAnnotations) {\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.selectedAnnotationId = null;\n }\n this.state.layers = layers;\n this.notify();\n }\n\n // --- Serialization ---\n\n /**\n * The serializable state. A document whose layers still have the v1 / v2 shape (the\n * Background with its sliders and depth mask, adjustment layers, annotations) is written as\n * v1 / v2 exactly as before; anything else is written as v3 with the layer pixels as PNG\n * assets. `selection` is included (run-length encoded) only when something is selected.\n */\n public toJSON(): SerializedImageEditorState;\n /**\n * With `assets: 'external'`, layer pixels go to `store` (each PNG uploaded once per store) and\n * the v3 document holds the store's references. Old-shape documents have no assets either way.\n */\n public toJSON(options: { assets: 'external'; store: ImageEditorAssetStore }): Promise<SerializedImageEditorState>;\n public toJSON(options?: { assets: 'external'; store: ImageEditorAssetStore }): SerializedImageEditorState | Promise<SerializedImageEditorState> {\n // While contents are open, the document is the one set aside (without the open edits).\n const root = this.contentsStack[0]?.saved;\n const current = this.state;\n const element = this.imageElement;\n if (root) {\n this.state = root.state;\n this.imageElement = root.element;\n this.viewKey = [];\n }\n let out: SerializedImageEditorState;\n try {\n out = this.serializeState();\n if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);\n } finally {\n if (root) {\n this.state = current;\n this.imageElement = element;\n this.viewKey = [];\n }\n }\n if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;\n return serializeLayersExternal(this.rootLayers(), options.store).then(({ layers, assets, smartContents }) => (smartContents ? { ...out, layers, assets, smartContents } : { ...out, layers, assets }));\n }\n\n /**\n * Loads a stored state whose v3 assets may live in an asset store (references other than data\n * URLs are fetched with `store.get`), as one undo step. Assets that cannot be fetched are left\n * out and listed in `lastLoadProblems`.\n */\n public async loadJSONAsync(data: SerializedImageEditorState, options: { store?: ImageEditorAssetStore } = {}): Promise<void> {\n if (data.version !== LAYERED_SCHEMA_VERSION || !data.assets) {\n this.loadJSON(data);\n return;\n }\n const problems: string[] = [];\n const assets = await resolveExternalAssets(data.assets, options.store, problems);\n this.loadJSON({ ...data, assets });\n this.lastLoadProblems = [...problems, ...this.lastLoadProblems];\n }\n\n /** Loads a stored state of any version (one undo step). */\n public loadJSON(data: SerializedImageEditorState): void {\n this.abandonContents();\n this.recordHistory();\n this.applyStored(data);\n let selection: SelectionMask | null = null;\n if (data.selection) {\n try {\n selection = normalizeSelection(deserializeSelection(data.selection));\n } catch {\n selection = null;\n }\n }\n this.state.selection = selection;\n this.dropMismatchedSelection();\n this.notify();\n }\n\n /** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */\n public lastLoadProblems: string[] = [];\n\n private applyStored(raw: Partial<SerializedImageEditorState>): void {\n const s = this.state;\n const data = migrateSerializedImageEditorState(raw as SerializedImageEditorState);\n this.lastLoadProblems = [];\n if (isLayered(data)) {\n const problems: string[] = [];\n const canvas = typeof data.width === 'number' && data.width > 0 && typeof data.height === 'number' && data.height > 0 ? { width: data.width, height: data.height } : this.naturalSize;\n s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems, smartContents: Array.isArray(data.smartContents) ? data.smartContents : [], canvas });\n this.lastLoadProblems = problems;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.outputOpacity = data.opacity;\n if (typeof data.legacyOutputBlur === 'number' && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;\n const light = data.globalLight;\n if (light && Number.isFinite(light.angle) && Number.isFinite(light.altitude)) s.globalLight = { angle: light.angle, altitude: Math.min(90, Math.max(0, light.altitude)) };\n else delete s.globalLight;\n const patterns = deserializePatterns(data.patterns, problems);\n if (patterns.length) s.patterns = patterns;\n else delete s.patterns;\n if (!this.imageElement && typeof data.width === 'number' && typeof data.height === 'number' && data.width > 0 && data.height > 0) {\n s.imageDimensions = { width: data.width, height: data.height };\n }\n } else {\n const { width, height } = this.naturalSize;\n const stack = layersFromLegacy(data, width, height);\n s.layers = stack.layers;\n s.outputOpacity = stack.outputOpacity;\n if (stack.legacyOutputBlur !== undefined) s.legacyOutputBlur = stack.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.activeLayerId = null;\n delete s.globalLight;\n delete s.patterns;\n }\n s.activeTarget = 'pixels';\n this.viewKey = [];\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) s.selectedAnnotationId = null;\n }\n\n private serializeState(): SerializedImageEditorState {\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, null);\n // A global light is only stored in the layered schema.\n if (view.exact && !this.state.globalLight && !this.state.patterns?.length) {\n return {\n // States without adjustment layers keep schema 1, so they round-trip unchanged.\n version: view.adjustments.layers?.length ? 2 : 1,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n adjustments: serializeAdjustments(view.adjustments),\n depthMask: {\n ...view.depthMask,\n depthRange: [...view.depthMask.depthRange] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(view.annotations)),\n };\n }\n const { layers, assets, smartContents } = serializeLayers(this.state.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width,\n height,\n layers,\n assets,\n activeLayerId: this.state.activeLayerId,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n opacity: this.state.outputOpacity,\n };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n if (this.state.globalLight) out.globalLight = { ...this.state.globalLight };\n if (this.state.patterns?.length) out.patterns = serializePatterns(this.state.patterns);\n if (smartContents) out.smartContents = smartContents;\n return out;\n }\n}\n\n/**\n * Legacy-process adjustments are saved without `process` (and without an unused zero `tint`), so\n * they serialize exactly as they did before process versions existed.\n */\nfunction serializeAdjustments(adjustments: ImageAdjustments): ImageAdjustments {\n const out: ImageAdjustments = { ...adjustments };\n if (out.layers?.length) out.layers = JSON.parse(JSON.stringify(out.layers));\n else delete out.layers;\n if (normalizeProcessVersion(out.process) === LEGACY_PROCESS_VERSION) {\n delete out.process;\n if (!out.tint) delete out.tint;\n }\n return out;\n}\n\n/** A pixel layer with its develop filter updated from base-slider values (created if missing). */\nfunction withDevelop(layer: PixelLayer, partial: Partial<ImageAdjustments>): PixelLayer {\n const filters = [...(layer.filters ?? [])];\n let i = filters.findIndex((f) => f.kind === 'develop');\n if (i === -1) {\n filters.unshift({\n id: layer.source === 'document' ? DEVELOP_FILTER_ID : newLayerId('flt'),\n kind: 'develop',\n enabled: true,\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n process: LATEST_PROCESS_VERSION,\n });\n i = 0;\n }\n const next = { ...(filters[i] as DevelopFilter) };\n for (const k of DEVELOP_KEYS) {\n const v = partial[k];\n if (typeof v === 'number' && Number.isFinite(v)) next[k] = v;\n }\n if ('process' in partial) next.process = normalizeProcessVersion(partial.process);\n filters[i] = next;\n return { ...layer, filters };\n}\n\n/** A pixel layer with its blur filter set to `radius` (removed at 0). */\nfunction withBlur(layer: PixelLayer, radius: number): PixelLayer {\n const existing = layer.filters?.find((f) => f.kind === 'blur');\n const filters: LayerFilter[] = (layer.filters ?? []).filter((f) => f.kind !== 'blur');\n if (radius > 0) {\n filters.push({\n id: existing?.id ?? (layer.source === 'document' ? BLUR_FILTER_ID : newLayerId('flt')),\n kind: 'blur',\n enabled: true,\n radius,\n });\n }\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return next;\n}\n\n/** Updates a node from an old-shape layer (`setAdjustments({ layers })`), keeping a raster mask whose pixels did not change. */\nfunction mergeAdjustmentNode(node: AdjustmentLayerNode, update: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const next = {\n ...node,\n kind: update.kind,\n params: JSON.parse(JSON.stringify(update.params)),\n visible: update.enabled !== false,\n opacity: update.opacity,\n blend: update.blend,\n name: update.name ?? '',\n } as AdjustmentLayerNode;\n if (!update.mask) delete next.mask;\n else {\n const current = node.mask && !node.mask.source ? node.mask : null;\n const memo = current ? legacyMaskOf(current, width, height) : null;\n if (current && memo && memo.width === update.mask.width && memo.height === update.mask.height && memo.rle === update.mask.rle) {\n next.mask = withMaskProps(current, {\n density: update.mask.density ?? 1,\n feather: update.mask.feather ?? 0,\n enabled: update.mask.enabled !== false,\n });\n } else {\n const m = rasterMaskFromLegacy(update.mask, width, height);\n if (m) next.mask = m;\n else delete next.mask;\n }\n }\n return next;\n}\n\n/** Applies mask property changes; `undefined` fields keep their value, defaults are left out. */\nfunction withMaskProps(mask: RasterMask, props: LayerMaskProps): RasterMask {\n const next: RasterMask = { ...mask };\n if (props.density !== undefined) {\n const d = Math.min(1, Math.max(0, props.density));\n if (d !== 1) next.density = d;\n else delete next.density;\n }\n if (props.feather !== undefined) {\n const f = Math.min(1000, Math.max(0, props.feather));\n if (f) next.feather = f;\n else delete next.feather;\n }\n if (props.enabled !== undefined) {\n if (props.enabled) delete next.enabled;\n else next.enabled = false;\n }\n if (props.linked !== undefined) {\n if (props.linked) delete next.linked;\n else next.linked = false;\n }\n return next;\n}\n\n/** Rescales adjustment-layer masks (canvas-sized) when the canvas size changes. */\nfunction resizeLegacyMasks(layers: ImageLayer[], from: ImageDimensions, to: ImageDimensions): ImageLayer[] {\n const fw = Math.max(1, Math.round(from.width));\n const fh = Math.max(1, Math.round(from.height));\n const tw = Math.max(1, Math.round(to.width));\n const th = Math.max(1, Math.round(to.height));\n if (fw === tw && fh === th) return layers;\n let out = layers;\n for (const l of flattenLayers(layers)) {\n if (l.type !== 'adjustment' || !l.mask || l.mask.source) continue;\n const old = l.mask;\n const mask = rasterMaskFromLegacy(legacyMaskOf(old, fw, fh), tw, th);\n if (!mask) continue;\n out = mapLayer(out, l.id, (n) => ({ ...n, mask: { ...mask, density: old.density, feather: old.feather, enabled: old.enabled } }));\n }\n return out;\n}\n\n/** Whether every pixel of `surface` inside the canvas is opaque. */\nfunction isOpaqueInside(surface: PixelSurface, width: number, height: number): boolean {\n const row = new Uint8ClampedArray(width * 4);\n for (let y = 0; y < height; y++) {\n readRegion(surface, { x: 0, y, width, height: 1 }, row);\n for (let i = 3; i < row.length; i += 4) if (row[i] !== 255) return false;\n }\n return true;\n}\n\nfunction collectStateTiles(layers: readonly ImageLayer[], into: Set<Tile>): void {\n for (const l of flattenLayers(layers)) {\n if (l.type === 'pixel' && l.source !== 'document') collectTiles(l.surface, into);\n if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);\n if (l.type === 'smart') {\n collectTiles(l.surface, into);\n if (l.smartFilters?.mask) collectTiles(l.smartFilters.mask.surface, into);\n if (l.contents.raster) collectTiles(l.contents.raster, into);\n if (l.contents.document) collectStateTiles(l.contents.document.layers, into);\n }\n }\n}\n\n/** The stack without adjustment layers and layer filters (the pixels as painted). */\nfunction pixelsOnly(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'adjustment') continue;\n if (l.type === 'group') out.push({ ...l, children: pixelsOnly(l.children) });\n else if (l.type === 'pixel' && l.filters?.length) {\n const p = { ...l };\n delete p.filters;\n out.push(p);\n } else out.push(l);\n }\n return out;\n}\n\n/** Bilinear resample of RGBA pixels to `width × height`. */\nexport function resizePixels(px: PixelBuffer, width: number, height: number): PixelBuffer {\n const src = px.data as ArrayLike<number>;\n const out = new Uint8ClampedArray(width * height * 4);\n const kx = px.width / width;\n const ky = px.height / height;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(px.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(px.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(px.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(px.width - 1, x0 + 1);\n const tx = fx - x0;\n for (let c = 0; c < 4; c++) {\n const a = src[(y0 * px.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px.width + x1) * 4 + c] * tx;\n const b = src[(y1 * px.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px.width + x1) * 4 + c] * tx;\n out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;\n }\n }\n }\n return { width, height, data: out };\n}\n\nfunction withoutAnnotations(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: withoutAnnotations(l.children) } : l);\n }\n return out;\n}\n\n/** A colour as RGBA bytes: `#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` (0..255). */\nexport function parseColor(value: string | number[]): [number, number, number, number] {\n if (Array.isArray(value)) {\n if (value.length < 3 || value.length > 4 || !value.every((v) => typeof v === 'number' && v >= 0 && v <= 255)) {\n throw new LayerOperationError('A colour array is [r, g, b] or [r, g, b, a] with values 0..255.');\n }\n return [Math.round(value[0]), Math.round(value[1]), Math.round(value[2]), Math.round(value[3] ?? 255)];\n }\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(String(value).trim());\n if (!m) throw new LayerOperationError(`Invalid colour \"${value}\"; use #rrggbb or #rrggbbaa.`);\n let hex = m[1];\n if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');\n const n = (i: number) => parseInt(hex.slice(i, i + 2), 16);\n return [n(0), n(2), n(4), hex.length === 8 ? n(6) : 255];\n}\n\n/** `#rrggbb` of RGBA bytes (alpha dropped). */\nexport function toHex(c: readonly number[]): string {\n return '#' + [c[0], c[1], c[2]].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');\n}\n\n/** `null` for an empty selection, so \"nothing selected\" has one representation. */\nfunction normalizeSelection(mask: SelectionMask | null): SelectionMask | null {\n return mask?.bounds ? mask : null;\n}\n\nfunction sameSelection(a: SelectionMask, b: SelectionMask): boolean {\n if (a.width !== b.width || a.height !== b.height) return false;\n const x = a.data;\n const y = b.data;\n for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;\n return true;\n}\n\n/** Scales an annotation's geometry from one image size to another. */\nfunction scaleAnnotation(ann: Annotation, sx: number, sy: number): Annotation {\n const k = Math.sqrt(Math.abs(sx * sy));\n const base = { ...ann, x: ann.x * sx, y: ann.y * sy } as Annotation;\n switch (base.type) {\n case 'pen':\n return { ...base, points: base.points.map((p) => ({ x: p.x * sx, y: p.y * sy })), strokeWidth: base.strokeWidth * k };\n case 'rect':\n return { ...base, width: base.width * sx, height: base.height * sy, strokeWidth: base.strokeWidth * k };\n case 'image':\n return { ...base, width: base.width * sx, height: base.height * sy };\n case 'circle':\n return { ...base, radiusX: base.radiusX * sx, radiusY: base.radiusY * sy, strokeWidth: base.strokeWidth * k };\n case 'arrow':\n case 'line':\n return { ...base, endX: base.endX * sx, endY: base.endY * sy, strokeWidth: base.strokeWidth * k };\n case 'text':\n return { ...base, fontSize: base.fontSize * k };\n }\n}\n","import {\n loadDocument,\n type JsonObject,\n type LoadReport,\n type MigrationStep,\n} from '../persistence/migrate';\nimport { DEFAULT_DEPTH_MASK, DEFAULT_TRANSFORM, normalizeStoredAdjustments } from './controller';\nimport type {\n Annotation,\n CropBox,\n DepthMaskOptions,\n ImageAdjustments,\n ImageTransform,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { deserializeSelection } from './selection/mask';\nimport type { SerializedLayer, SerializedLayeredImageEditorState } from './layers/serialize';\n\n/**\n * The newest `version` that `ImageEditorController.toJSON()` writes (3: layers). Documents whose\n * layers still have the old shape are written as version 1 or 2.\n */\nexport const IMAGE_EDITOR_STATE_VERSION = 3;\n\n/**\n * Migration steps for the document-level schema (versions 1 and 2), keyed by the version they\n * start from. Empty: version 2 only adds `adjustments.layers`, which version 1 readers keep.\n */\nexport const imageEditorStateMigrations: Record<number, MigrationStep> = {};\n\nexport interface ImageEditorDocumentLoad {\n /** State to pass to `loadJSON` / `initialState`. */\n state: SerializedImageEditorState;\n report: LoadReport;\n /** Top-level keys this build does not know; spread them into the saved JSON to keep them. */\n unknown: JsonObject;\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value);\n\nfunction expectObject(value: unknown, what: string): Record<string, unknown> {\n if (!isObject(value)) throw new Error(`${what} must be an object`);\n return value;\n}\n\nfunction checkFields(\n value: Record<string, unknown>,\n types: Record<string, (v: unknown) => boolean>,\n what: string,\n) {\n for (const [field, ok] of Object.entries(types)) {\n if (field in value && value[field] !== undefined && !ok(value[field])) {\n throw new Error(`${what}.${field} has an invalid value`);\n }\n }\n}\n\nconst isBoolean = (v: unknown) => typeof v === 'boolean';\n\nfunction parseCrop(value: unknown): CropBox | null {\n if (value === null) return null;\n const crop = expectObject(value, 'crop');\n for (const field of ['x', 'y', 'width', 'height'] as const) {\n if (!isNumber(crop[field])) throw new Error(`crop.${field} must be a number`);\n }\n return { ...(crop as unknown as CropBox) };\n}\n\nfunction parseTransform(value: unknown): ImageTransform {\n const transform = expectObject(value, 'transform');\n checkFields(transform, { rotate: isNumber, flipH: isBoolean, flipV: isBoolean }, 'transform');\n return { ...DEFAULT_TRANSFORM, ...(transform as Partial<ImageTransform>) };\n}\n\nconst ADJUSTMENT_NUMBERS = [\n 'brightness',\n 'contrast',\n 'saturation',\n 'exposure',\n 'temperature',\n 'tint',\n 'blur',\n 'opacity',\n] as const;\n\nfunction parseAdjustments(value: unknown): ImageAdjustments {\n const adjustments = expectObject(value, 'adjustments');\n checkFields(\n adjustments,\n {\n ...Object.fromEntries(ADJUSTMENT_NUMBERS.map((k) => [k, isNumber])),\n process: (v) => v === 1 || v === 2,\n },\n 'adjustments',\n );\n // Kept as stored: `loadJSON` fills defaults and keeps a missing `process` legacy.\n return { ...(adjustments as unknown as ImageAdjustments) };\n}\n\nfunction parseDepthMask(value: unknown): DepthMaskOptions {\n const mask = expectObject(value, 'depthMask');\n checkFields(\n mask,\n {\n enabled: isBoolean,\n maskSource: (v) => v === null || typeof v === 'string',\n mode: (v) => v === 'threshold' || v === 'brightness',\n depthRange: (v) => Array.isArray(v) && v.length === 2 && v.every(isNumber),\n softness: isNumber,\n invert: isBoolean,\n },\n 'depthMask',\n );\n const merged = { ...DEFAULT_DEPTH_MASK, ...(mask as Partial<DepthMaskOptions>) };\n return { ...merged, depthRange: [...merged.depthRange] as [number, number] };\n}\n\nconst ANNOTATION_TYPES = new Set(['pen', 'rect', 'circle', 'arrow', 'line', 'text', 'image']);\n\nconst isAnnotation = (value: unknown): value is Annotation =>\n isObject(value) &&\n typeof value.id === 'string' &&\n typeof value.type === 'string' &&\n ANNOTATION_TYPES.has(value.type) &&\n isNumber(value.x) &&\n isNumber(value.y);\n\n/**\n * Loads stored image editor state (a JSON string or object) with a `LoadReport`:\n *\n * - Runs `imageEditorStateMigrations` up to `IMAGE_EDITOR_STATE_VERSION`; a missing\n * `version` is taken as current.\n * - A newer `version` is still loaded but reported `readOnly`: do not save over it.\n * - Each section (`crop`, `transform`, `adjustments`, `depthMask`, `annotations`, and the\n * optional run-length encoded `selection`)\n * falls back to its default on its own; invalid annotations are dropped one by one.\n * - Unknown top-level keys are returned in `unknown`.\n *\n * Valid state from `toJSON()` comes back unchanged, so `loadJSON(state)` behaves\n * exactly as with the raw object.\n */\nexport function loadImageEditorDocument(input: unknown): ImageEditorDocumentLoad {\n let parsed: unknown = input;\n if (typeof input === 'string') {\n try {\n parsed = JSON.parse(input);\n } catch {\n parsed = input;\n }\n }\n if (isObject(parsed) && parsed.version === 3) return loadLayeredDocument(parsed);\n // Version 2 only adds `adjustments.layers`, which the version 1 sections keep as stored.\n const isV2 = isObject(parsed) && parsed.version === 2;\n if (isV2) parsed = { ...(parsed as Record<string, unknown>), version: 1 };\n const newer = isObject(parsed) && typeof parsed.version === 'number' && parsed.version > IMAGE_EDITOR_STATE_VERSION;\n let dropped = 0;\n const result = loadDocument<Omit<LegacySerializedImageEditorState, 'version'>>(\n parsed,\n {\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n adjustments: {\n parse: parseAdjustments,\n // Same as what `loadJSON` derives from missing adjustments.\n defaults: () => normalizeStoredAdjustments(undefined),\n },\n depthMask: {\n parse: parseDepthMask,\n defaults: () => ({ ...DEFAULT_DEPTH_MASK, depthRange: [...DEFAULT_DEPTH_MASK.depthRange] as [number, number] }),\n },\n annotations: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('annotations must be an array');\n const kept = value.filter(isAnnotation);\n dropped = value.length - kept.length;\n return JSON.parse(JSON.stringify(kept)) as Annotation[];\n },\n defaults: () => [],\n },\n // Optional: only present when something was selected when saving.\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n },\n {\n // Versions 1 and 2 load as one schema; anything newer than 3 is reported read-only.\n currentVersion: newer ? IMAGE_EDITOR_STATE_VERSION : 1,\n steps: imageEditorStateMigrations,\n },\n );\n if (dropped > 0) {\n result.report.problems.push({\n section: 'annotations',\n message: `${dropped} invalid annotation${dropped === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 1, ...result.value } as LegacySerializedImageEditorState;\n if (isV2) {\n if (state.adjustments.layers?.length) state.version = 2;\n result.report.version = 2;\n }\n if (state.selection === undefined) delete state.selection;\n return { state, report: result.report, unknown: result.unknown };\n}\n\nconst LAYER_TYPES = new Set(['pixel', 'adjustment', 'group', 'annotation', 'smart']);\n\n/** Keeps layers that have an id-like shape and a known type (children checked too); counts the rest. */\nfunction cleanLayers(value: unknown[], dropped: { count: number }): SerializedLayer[] {\n const out: SerializedLayer[] = [];\n for (const raw of value) {\n if (!isObject(raw) || typeof raw.type !== 'string' || !LAYER_TYPES.has(raw.type)) {\n dropped.count++;\n continue;\n }\n if (raw.type === 'group') {\n const children = Array.isArray(raw.children) ? cleanLayers(raw.children, dropped) : [];\n out.push({ ...(raw as unknown as SerializedLayer), children } as SerializedLayer);\n } else out.push(JSON.parse(JSON.stringify(raw)) as SerializedLayer);\n }\n return out;\n}\n\n/** Version 3 (layers): each section falls back on its own; unreadable layers are dropped one by one. */\nfunction loadLayeredDocument(doc: Record<string, unknown>): ImageEditorDocumentLoad {\n const dropped = { count: 0 };\n const result = loadDocument<Omit<SerializedLayeredImageEditorState, 'version'>>(\n doc,\n {\n width: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('width must be a positive number'); })()), defaults: 0 },\n height: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('height must be a positive number'); })()), defaults: 0 },\n layers: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('layers must be an array');\n return cleanLayers(value, dropped);\n },\n defaults: () => [],\n },\n assets: {\n parse: (value) => {\n const assets = expectObject(value, 'assets');\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(assets)) if (typeof v === 'string') out[k] = v;\n return out;\n },\n defaults: () => ({}),\n },\n activeLayerId: { parse: (v) => (v === null || typeof v === 'string' ? v : null), defaults: null },\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n opacity: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('opacity must be a number');\n return Math.min(1, Math.max(0, v));\n },\n defaults: 1,\n },\n legacyOutputBlur: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('legacyOutputBlur must be a number');\n return Math.max(0, v);\n },\n defaults: undefined,\n },\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n smartContents: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('smartContents must be an array');\n // Entries are checked one by one when the layers load; keep the ones with an id.\n return value.filter((c) => isObject(c) && typeof c.id === 'string').map((c) => JSON.parse(JSON.stringify(c)));\n },\n defaults: undefined,\n },\n },\n { currentVersion: 3, steps: {} },\n );\n if (dropped.count > 0) {\n result.report.problems.push({\n section: 'layers',\n message: `${dropped.count} invalid layer${dropped.count === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 3, ...result.value } as SerializedLayeredImageEditorState;\n if (state.selection === undefined) delete state.selection;\n if (state.legacyOutputBlur === undefined) delete state.legacyOutputBlur;\n if (state.smartContents === undefined) delete state.smartContents;\n return { state, report: result.report, unknown: result.unknown };\n}\n","// Portions ported from lightcraft-main/crates/develop/src/controls.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentKind, ImageAdjustments } from './types';\n\n/**\n * Control specs: one table describes every numeric adjustment slider (id, label, section, range,\n * default, step, display precision and track style). The React and Svelte panels render from it,\n * the agent tools expose it as a schema, and values are addressed by id.\n */\n\nexport type ControlSection = 'light' | 'color' | 'effects' | 'layer';\n\n/** How a slider track is drawn. */\nexport type ControlTrack =\n /** Plain track, fill from the minimum. */\n | { kind: 'plain' }\n /** Zero in the middle. */\n | { kind: 'centered' }\n /** Blue → yellow (white-balance temperature). */\n | { kind: 'temp' }\n /** Green → magenta (tint). */\n | { kind: 'tint' }\n /** A two-colour gradient. */\n | { kind: 'gradient'; from: string; to: string }\n /** Full hue rainbow. */\n | { kind: 'rainbow' };\n\nexport interface ControlSpec {\n /** Dotted path of the value (`exposure`; inside a layer's params: `midtones.0`, `ranges.4.hue`). */\n id: string;\n label: string;\n section: ControlSection;\n min: number;\n max: number;\n step: number;\n default: number;\n /** Decimal places shown (after `displayScale`). */\n decimals: number;\n track: ControlTrack;\n /** Multiplier for display (opacity 0..1 shows as 0..100 %). */\n displayScale?: number;\n /** Unit suffix for display (`px`, `%`, `°`). */\n unit?: string;\n /** The rendering process the control needs (tint: process 2). */\n requiresProcess?: 1 | 2;\n}\n\nconst plain: ControlTrack = { kind: 'plain' };\nconst centered: ControlTrack = { kind: 'centered' };\n\n/** Every numeric base adjustment, in panel order. */\nexport const IMAGE_ADJUSTMENT_CONTROLS: readonly ControlSpec[] = [\n { id: 'brightness', label: 'Brightness', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'contrast', label: 'Contrast', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n {\n id: 'saturation', label: 'Saturation', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' },\n },\n { id: 'exposure', label: 'Exposure', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'temperature', label: 'Warmth / Temperature', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'temp' } },\n { id: 'tint', label: 'Tint', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'tint' }, requiresProcess: 2 },\n { id: 'blur', label: 'Blur', section: 'effects', min: 0, max: 30, step: 1, default: 0, decimals: 0, track: plain, unit: 'px' },\n { id: 'opacity', label: 'Opacity', section: 'effects', min: 0, max: 1, step: 0.05, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\nconst tone = (prefix: string, label: string): ControlSpec[] =>\n [\n ['0', 'Cyan – Red', '#00b7c7', '#e0343a'],\n ['1', 'Magenta – Green', '#d23ad2', '#2fb34a'],\n ['2', 'Yellow – Blue', '#e0c63a', '#3a5be0'],\n ].map(([i, l, from, to]) => ({\n id: `${prefix}.${i}`, label: `${label} ${l}`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from, to },\n }));\n\nconst levelsRecord = (ch: string, label: string): ControlSpec[] => [\n { id: `${ch}.inBlack`, label: `${label} input black`, section: 'layer', min: 0, max: 253, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.gamma`, label: `${label} gamma`, section: 'layer', min: 0.1, max: 9.99, step: 0.01, default: 1, decimals: 2, track: plain },\n { id: `${ch}.inWhite`, label: `${label} input white`, section: 'layer', min: 2, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n { id: `${ch}.outBlack`, label: `${label} output black`, section: 'layer', min: 0, max: 255, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.outWhite`, label: `${label} output white`, section: 'layer', min: 0, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n];\n\nconst RANGE_NAMES = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'];\n\n/**\n * Numeric parameters of each adjustment-layer kind, addressed inside `layer.params`. Curves points\n * and booleans / colours (colorize, preserve luminosity, tint, filter colour) are edited by the\n * dedicated controls, not sliders.\n */\nexport const ADJUSTMENT_LAYER_CONTROLS: Readonly<Record<AdjustmentKind, readonly ControlSpec[]>> = {\n curves: [],\n levels: [\n ...levelsRecord('master', 'RGB'),\n ...levelsRecord('red', 'Red'),\n ...levelsRecord('green', 'Green'),\n ...levelsRecord('blue', 'Blue'),\n ],\n hueSaturation: [\n { id: 'hue', label: 'Hue', section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n { id: 'lightness', label: 'Lightness', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } },\n ...RANGE_NAMES.flatMap((name, i): ControlSpec[] => [\n { id: `ranges.${i}.hue`, label: `${name} hue`, section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: `ranges.${i}.saturation`, label: `${name} saturation`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: `ranges.${i}.lightness`, label: `${name} lightness`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n ]),\n ],\n vibrance: [\n { id: 'vibrance', label: 'Vibrance', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#d8406a' } },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n ],\n colorBalance: [...tone('shadows', 'Shadows'), ...tone('midtones', 'Midtones'), ...tone('highlights', 'Highlights')],\n blackWhite: [\n ['reds', 'Reds', 40, '#e0343a'],\n ['yellows', 'Yellows', 60, '#e0c63a'],\n ['greens', 'Greens', 40, '#2fb34a'],\n ['cyans', 'Cyans', 60, '#00b7c7'],\n ['blues', 'Blues', 20, '#3a5be0'],\n ['magentas', 'Magentas', 80, '#d23ad2'],\n ].map(([id, label, def, to]) => ({\n id: id as string, label: label as string, section: 'layer' as const, min: -200, max: 300, step: 1, default: def as number, decimals: 0,\n track: { kind: 'gradient' as const, from: '#000000', to: to as string }, unit: '%',\n })),\n invert: [],\n posterize: [{ id: 'levels', label: 'Levels', section: 'layer', min: 2, max: 255, step: 1, default: 4, decimals: 0, track: plain }],\n threshold: [{ id: 'level', label: 'Threshold level', section: 'layer', min: 1, max: 255, step: 1, default: 128, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } }],\n photoFilter: [{ id: 'density', label: 'Density', section: 'layer', min: 0, max: 100, step: 1, default: 25, decimals: 0, track: plain, unit: '%' }],\n};\n\n/** Opacity of any adjustment layer (0..1, shown as %). */\nexport const ADJUSTMENT_LAYER_OPACITY_CONTROL: ControlSpec = {\n id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%',\n};\n\nconst BASE_BY_ID = new Map(IMAGE_ADJUSTMENT_CONTROLS.map((c) => [c.id, c]));\n\n/** The base-adjustment control with this id. */\nexport function findAdjustmentControl(id: string): ControlSpec | undefined {\n return BASE_BY_ID.get(id);\n}\n\n/** The control of a layer parameter (`opacity` included). */\nexport function findLayerControl(kind: AdjustmentKind, id: string): ControlSpec | undefined {\n if (id === 'opacity') return ADJUSTMENT_LAYER_OPACITY_CONTROL;\n return ADJUSTMENT_LAYER_CONTROLS[kind]?.find((c) => c.id === id);\n}\n\n/** `v` clamped to the control's range and snapped to its step; non-finite values give the default. */\nexport function clampControlValue(spec: ControlSpec, v: unknown): number {\n if (typeof v !== 'number' || !Number.isFinite(v)) return spec.default;\n const clamped = Math.min(spec.max, Math.max(spec.min, v));\n const steps = Math.round((clamped - spec.min) / spec.step);\n const snapped = spec.min + steps * spec.step;\n // Trim float noise from the step arithmetic.\n return Math.min(spec.max, Number(snapped.toFixed(6)));\n}\n\n/** A value as the slider shows it (`+12`, `35%`, `4px`). */\nexport function formatControlValue(spec: ControlSpec, v: number): string {\n const shown = (Number.isFinite(v) ? v : spec.default) * (spec.displayScale ?? 1);\n const s = shown.toFixed(spec.decimals);\n return `${s}${spec.unit ?? ''}`;\n}\n\n/** Where `v` sits on the track, 0 (left) to 1 (right). */\nexport function controlPosition(spec: ControlSpec, v: number): number {\n const p = (clampControlValue(spec, v) - spec.min) / (spec.max - spec.min || 1);\n return Math.min(1, Math.max(0, p));\n}\n\n/** CSS background for a control's track. */\nexport function controlTrackBackground(spec: ControlSpec): string | undefined {\n const t = spec.track;\n switch (t.kind) {\n case 'temp':\n return 'linear-gradient(to right, #3a7bd5, #d8d8d8, #f2c94c)';\n case 'tint':\n return 'linear-gradient(to right, #3fbf4f, #d8d8d8, #d23ad2)';\n case 'gradient':\n return `linear-gradient(to right, ${t.from}, ${t.to})`;\n case 'rainbow':\n return 'linear-gradient(to right, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff)';\n default:\n return undefined;\n }\n}\n\n/** Reads a dotted path (`ranges.2.hue`) from an object. */\nexport function getControlPath(obj: unknown, path: string): unknown {\n let cur: any = obj;\n for (const part of path.split('.')) {\n if (cur === null || cur === undefined) return undefined;\n cur = cur[part];\n }\n return cur;\n}\n\n/** A copy of `obj` with the dotted path set (arrays and objects along the path are cloned). */\nexport function setControlPath<T>(obj: T, path: string, value: unknown): T {\n const parts = path.split('.');\n const root: any = Array.isArray(obj) ? [...(obj as unknown[])] : { ...(obj as object) };\n let cur = root;\n for (let i = 0; i < parts.length - 1; i++) {\n const next = cur[parts[i]];\n cur[parts[i]] = Array.isArray(next) ? [...next] : next && typeof next === 'object' ? { ...next } : {};\n cur = cur[parts[i]];\n }\n cur[parts[parts.length - 1]] = value;\n return root as T;\n}\n\n/** The base adjustment values the control table describes. */\nexport function readAdjustmentControls(adjustments: ImageAdjustments): Record<string, number> {\n const out: Record<string, number> = {};\n for (const c of IMAGE_ADJUSTMENT_CONTROLS) {\n const v = (adjustments as unknown as Record<string, unknown>)[c.id];\n out[c.id] = typeof v === 'number' && Number.isFinite(v) ? v : c.default;\n }\n return out;\n}\n","import type { SelectionMask, SelectionMode } from './mask';\nimport type { ColorDistance } from './wand';\nimport type { RefineEdgeParams } from './refine';\n\n/** Selection tools of the image editor UI. */\nexport type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'lasso';\n\nexport const SELECTION_TOOLS: readonly { id: SelectionToolKind; label: string }[] = [\n { id: 'wand', label: 'Magic Wand' },\n { id: 'rect', label: 'Rectangle' },\n { id: 'ellipse', label: 'Ellipse' },\n { id: 'lasso', label: 'Lasso' },\n];\n\nexport const SELECTION_MODE_LABELS: Record<SelectionMode, string> = {\n replace: 'New',\n add: 'Add',\n subtract: 'Subtract',\n intersect: 'Intersect',\n};\n\n/** The options bar of the selection tools (shared by the React and Svelte UIs). */\nexport interface SelectionToolOptions {\n tool: SelectionToolKind;\n mode: SelectionMode;\n /** Magic wand tolerance, 0..255. */\n tolerance: number;\n contiguous: boolean;\n antiAlias: boolean;\n /** Sample the adjusted image instead of the source. */\n sampleMerged: boolean;\n /** Sample the whole image (`composite`, default) or only the active pixel layer. */\n sample?: 'composite' | 'active-layer';\n distance: ColorDistance;\n}\n\nexport const DEFAULT_SELECTION_TOOL_OPTIONS: SelectionToolOptions = {\n tool: 'wand',\n mode: 'replace',\n tolerance: 32,\n contiguous: true,\n antiAlias: true,\n sampleMerged: false,\n distance: 'rgb',\n};\n\n/**\n * Photoshop's modifier keys: Shift adds, Alt/Option subtracts, both intersect; otherwise the\n * options bar mode.\n */\nexport function selectionModeFromModifiers(\n modifiers: { shiftKey?: boolean; altKey?: boolean },\n fallback: SelectionMode\n): SelectionMode {\n if (modifiers.shiftKey && modifiers.altKey) return 'intersect';\n if (modifiers.shiftKey) return 'add';\n if (modifiers.altKey) return 'subtract';\n return fallback;\n}\n\nexport interface RefineEdgeControl {\n id: keyof Omit<RefineEdgeParams, 'smartRadius'>;\n label: string;\n min: number;\n max: number;\n step: number;\n unit: string;\n}\n\n/** Sliders of the Refine Edge panel. */\nexport const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[] = [\n { id: 'radius', label: 'Radius', min: 0, max: 64, step: 1, unit: 'px' },\n { id: 'smooth', label: 'Smooth', min: 0, max: 100, step: 1, unit: '' },\n { id: 'feather', label: 'Feather', min: 0, max: 50, step: 0.5, unit: 'px' },\n { id: 'contrast', label: 'Contrast', min: 0, max: 100, step: 1, unit: '%' },\n { id: 'shiftEdge', label: 'Shift Edge', min: -100, max: 100, step: 1, unit: '%' },\n];\n\n/** A one-line status: \"No selection\" or \"W × H px selected (n % of the image)\". */\nexport function describeSelection(mask: SelectionMask | null | undefined): string {\n if (!mask || !mask.bounds) return 'No selection';\n let sum = 0;\n for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];\n const pct = (sum / 255 / mask.data.length) * 100;\n const shown = pct >= 10 ? Math.round(pct) : Math.round(pct * 10) / 10;\n return `${mask.bounds.width} × ${mask.bounds.height} px selected (${shown}% of the image)`;\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { JsonSchema } from '../tools/schema.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind } from './types';\nimport { ADJUSTMENT_KINDS, isAdjustmentKind } from './adjustment-layers';\nimport { describeSelection } from './selection/tool-options';\nimport { SELECTION_MODES, parseSelectionMode, selectionArea } from './selection/mask';\nimport { SELECTION_MODIFY_OPS, type SelectionModifyOp } from './selection/modify';\nimport { layerMaskFromSelection } from './selection/layer-mask';\n\nconst modeSchema: JsonSchema = {\n type: 'string',\n enum: [...SELECTION_MODES],\n description: 'How the result combines with the current selection (default replace).',\n};\n\nconst num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\nfunction summary(controller: ImageEditorController) {\n const sel = controller.getSelection();\n const { width, height } = controller.getState().imageDimensions;\n const area = selectionArea(sel);\n return {\n empty: !sel,\n bounds: sel?.bounds ?? null,\n /** Σ coverage in pixels. */\n area: Math.round(area),\n percent: Math.round((area / Math.max(1, width * height)) * 10000) / 100,\n description: describeSelection(sel),\n };\n}\n\nconst writeAnnotations = (title: string, idempotent = false) => ({\n title,\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: idempotent,\n openWorldHint: false,\n});\n\n/**\n * Agent tools for selections and masks (part of `createImageEditorToolProvider`):\n * `get_selection`, `select_region`, `select_by_color`, `modify_selection`, `refine_selection`\n * and `apply_selection_as_mask`. Coordinates are natural pixels of the source image.\n */\nexport function createImageSelectionTools(controller: ImageEditorController): AgentTool[] {\n const getSelection: AgentTool = {\n name: 'get_selection',\n title: 'Read the selection',\n description:\n 'Returns the active selection: whether it is empty, its bounds (natural image pixels), its area in pixels and percent of the image.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read the selection', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n handler: () => summary(controller),\n };\n\n const selectRegion: AgentTool = {\n name: 'select_region',\n title: 'Select a region',\n description:\n 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), everything (\"all\"), nothing (\"none\") or the inverse of the current selection (\"invert\"). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n shape: { type: 'string', enum: ['rect', 'ellipse', 'all', 'none', 'invert'], description: 'What to select.' },\n x: { type: 'number', description: 'rect / ellipse: left edge in natural image pixels.' },\n y: { type: 'number', description: 'rect / ellipse: top edge in natural image pixels.' },\n width: { type: 'number', description: 'rect / ellipse: width in natural image pixels.' },\n height: { type: 'number', description: 'rect / ellipse: height in natural image pixels.' },\n mode: modeSchema,\n feather: { type: 'number', minimum: 0, description: 'Feather radius in pixels (default 0).' },\n },\n required: ['shape'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select a region', true),\n handler: ({ shape, x, y, width, height, mode, feather }) => {\n const opts = { mode: parseSelectionMode(mode), feather: num(feather, 0) };\n switch (shape) {\n case 'all':\n controller.selectAll();\n break;\n case 'none':\n controller.deselect();\n break;\n case 'invert':\n controller.invertSelection();\n break;\n case 'rect':\n case 'ellipse': {\n const rect = { x: num(x, NaN), y: num(y, NaN), width: num(width, NaN), height: num(height, NaN) };\n if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) {\n throw new Error('rect and ellipse need numeric x, y, width and height.');\n }\n if (shape === 'rect') controller.selectRect(rect, opts);\n else controller.selectEllipse(rect, opts);\n break;\n }\n default:\n throw new Error(`Unknown shape \"${String(shape)}\". Use rect, ellipse, all, none or invert.`);\n }\n return summary(controller);\n },\n };\n\n const selectByColor: AgentTool = {\n name: 'select_by_color',\n title: 'Select by colour',\n description:\n 'Magic wand at (x, y) with tolerance (0-255) and contiguous, or with \"color\" (\"#rrggbb\") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Seed point x in natural image pixels.' },\n y: { type: 'number', description: 'Seed point y in natural image pixels.' },\n color: { type: 'string', description: '#rrggbb: select this colour everywhere instead of a seed point.' },\n tolerance: { type: 'number', minimum: 0, maximum: 255, description: 'Magic wand tolerance (default 32).' },\n contiguous: { type: 'boolean', description: 'Only the connected region around (x, y) (default true).' },\n fuzziness: { type: 'number', minimum: 0, maximum: 200, description: 'Color Range fuzziness (default 40).' },\n sampleMerged: { type: 'boolean', description: 'Sample the adjusted image rather than the source.' },\n sample: {\n type: 'string',\n enum: ['composite', 'active-layer'],\n description: 'composite (default): the whole image; active-layer: only the active pixel layer (see set_active_layer).',\n },\n mode: modeSchema,\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select by colour', true),\n handler: async ({ x, y, color, tolerance, contiguous, fuzziness, sampleMerged, sample, mode }) => {\n const common = {\n mode: parseSelectionMode(mode),\n sampleMerged: sampleMerged === true,\n sample: (sample as 'composite' | 'active-layer' | undefined) ?? 'composite',\n };\n if (typeof color === 'string') {\n await controller.selectColorRange([{ color }], { ...common, fuzziness: num(fuzziness, 40) });\n } else if (typeof x === 'number' && typeof y === 'number') {\n await controller.magicWand({ x, y }, { ...common, tolerance: num(tolerance, 32), contiguous: contiguous !== false });\n } else {\n throw new Error('Pass x and y (magic wand) or color (#rrggbb).');\n }\n return summary(controller);\n },\n };\n\n const modify: AgentTool = {\n name: 'modify_selection',\n title: 'Modify the selection',\n description: 'Select › Modify: grow, shrink, feather, border or smooth the selection by radius pixels. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n op: { type: 'string', enum: [...SELECTION_MODIFY_OPS], description: 'How to modify the selection.' },\n radius: { type: 'number', minimum: 0, maximum: 500, description: 'Radius in pixels.' },\n },\n required: ['op', 'radius'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Modify the selection'),\n handler: ({ op, radius }) => {\n if (!(SELECTION_MODIFY_OPS as readonly string[]).includes(op as string)) throw new Error(`Unknown op \"${String(op)}\".`);\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n controller.modifySelection(op as SelectionModifyOp, Math.min(500, Math.max(0, num(radius, 0))));\n return summary(controller);\n },\n };\n\n const refine: AgentTool = {\n name: 'refine_selection',\n title: 'Refine the selection edge',\n description:\n 'Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %). Use it after loading a coarse AI mask. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n radius: { type: 'number', minimum: 0, maximum: 64, description: 'Edge detection radius in pixels.' },\n smartRadius: { type: 'boolean', description: 'Adapt the radius to hard and soft edges.' },\n smooth: { type: 'number', minimum: 0, maximum: 100, description: 'Smooth the edge outline (0-100).' },\n feather: { type: 'number', minimum: 0, maximum: 250, description: 'Feather the edge (pixels).' },\n contrast: { type: 'number', minimum: 0, maximum: 100, description: 'Sharpen the edge transition (0-100).' },\n shiftEdge: { type: 'number', minimum: -100, maximum: 100, description: 'Move the edge out (+) or in (-), in percent.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Refine the selection edge'),\n handler: async (params) => {\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n await controller.refineSelection({\n radius: num(params.radius, 8),\n smartRadius: params.smartRadius === true,\n smooth: num(params.smooth, 0),\n feather: num(params.feather, 0),\n contrast: num(params.contrast, 0),\n shiftEdge: num(params.shiftEdge, 0),\n });\n return summary(controller);\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_selection_as_mask',\n title: 'Mask a layer with the selection',\n description:\n 'Uses the selection as an adjustment layer mask: on layerId, or on a new layer of kind (with optional params), so the adjustment only affects the selected area. density (0-1) and feather (px) soften the mask non-destructively. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Existing adjustment layer to mask.' },\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Kind of a new layer to add and mask (instead of layerId).' },\n params: { type: 'object', description: 'Parameters for the new layer (see list_controls).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0-1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Mask feather in pixels.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Mask a layer with the selection'),\n handler: ({ layerId, kind, params, density, feather }) => {\n const sel = controller.getSelection();\n if (!sel) throw new Error('Nothing is selected.');\n const props = {\n ...(typeof density === 'number' ? { density } : {}),\n ...(typeof feather === 'number' ? { feather } : {}),\n };\n let id: string;\n if (typeof layerId === 'string') {\n if (!controller.getAdjustmentLayer(layerId)) throw new Error(`No adjustment layer \"${layerId}\".`);\n controller.applySelectionAsLayerMask(layerId, props);\n id = layerId;\n } else if (isAdjustmentKind(kind)) {\n id = controller.addAdjustmentLayer({\n kind: kind as AdjustmentKind,\n params: params as never,\n mask: layerMaskFromSelection(sel, props),\n });\n } else {\n throw new Error('Pass layerId (an existing layer) or kind (a new adjustment layer).');\n }\n const layer = controller.getAdjustmentLayer(id)!;\n const { rle: _rle, ...mask } = layer.mask!;\n return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };\n },\n };\n\n return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];\n}\n","import type { CaptureRenderRequest, CaptureSurface } from '../tools/capture.js';\nimport type { ImageEditorController } from './controller';\nimport type { SelectionMask } from './selection/mask';\n\nconst NO_CANVAS = 'Rendering is not available in this environment.';\n\n/** Darkens what is outside the selection (natural pixels), so its coverage reads at a glance. */\nfunction selectionShade(selection: SelectionMask, doc: Document): HTMLCanvasElement | null {\n const canvas = doc.createElement('canvas');\n canvas.width = selection.width;\n canvas.height = selection.height;\n const context = canvas.getContext('2d');\n if (!context) return null;\n const pixels = context.createImageData(selection.width, selection.height);\n for (let i = 0; i < selection.data.length; i++) {\n pixels.data[i * 4 + 3] = Math.round(150 * (1 - selection.data[i] / 255));\n }\n context.putImageData(pixels, 0, 0);\n return canvas;\n}\n\n/** Draws a region of the image editor's source view for `capture_view`. */\nasync function renderImageRegion(controller: ImageEditorController, request: CaptureRenderRequest): Promise<string> {\n const state = controller.getState();\n // Never render above natural size; small regions are enlarged when drawn.\n const k = Math.min(1, request.scale);\n const view = await controller.renderSourceView({ scale: k });\n if (!view) throw new Error('No image is loaded, so there is nothing to capture.');\n if (typeof document === 'undefined') throw new Error(NO_CANVAS);\n const out = document.createElement('canvas');\n out.width = request.width;\n out.height = request.height;\n const context = out.getContext('2d');\n if (!context) throw new Error(NO_CANVAS);\n const { rect } = request;\n // Upscaled regions keep hard pixel edges, so the agent sees real pixels.\n context.imageSmoothingEnabled = request.scale < 1;\n if (request.format === 'jpeg') {\n context.fillStyle = '#ffffff';\n context.fillRect(0, 0, out.width, out.height);\n }\n const kx = view.width / state.imageDimensions.width;\n const ky = view.height / state.imageDimensions.height;\n context.drawImage(view, rect.x * kx, rect.y * ky, rect.width * kx, rect.height * ky, 0, 0, out.width, out.height);\n if (request.includeOverlays) {\n const s = request.scale;\n const selection = state.selection;\n if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {\n const shade = selectionShade(selection, document);\n if (shade) context.drawImage(shade, rect.x, rect.y, rect.width, rect.height, 0, 0, out.width, out.height);\n }\n if (state.crop) {\n context.save();\n context.strokeStyle = '#ffd400';\n context.lineWidth = 2;\n context.setLineDash([6, 4]);\n context.strokeRect((state.crop.x - rect.x) * s, (state.crop.y - rect.y) * s, state.crop.width * s, state.crop.height * s);\n context.restore();\n }\n }\n const url = out.toDataURL(request.format === 'jpeg' ? 'image/jpeg' : 'image/png', request.quality);\n if (!url.startsWith('data:image/') || url.indexOf(',') === url.length - 1) throw new Error(NO_CANVAS);\n return url;\n}\n\n/**\n * The image editor as a capture surface. Units are natural pixels of the source image,\n * the same space as `get_selection` and `select_region`: crop, rotation and flips are not\n * applied (use `render_preview` for the final framing), adjustments, layers and annotations\n * are. Regions: the whole image, the selection's bounds, or a rect. Small regions are\n * enlarged up to 8× with hard pixel edges for close inspection.\n */\nexport function createImageCaptureSurface(controller: ImageEditorController): CaptureSurface {\n const whole = () => {\n const { width, height } = controller.getState().imageDimensions;\n return width > 0 && height > 0 ? { x: 0, y: 0, width, height } : null;\n };\n return {\n units: 'image pixels',\n content: whole,\n selection: () => ({ rect: controller.getState().selection?.bounds ?? null }),\n overlays: 'a shade over unselected pixels and the crop box outline',\n maxScale: 8,\n render: (request) => renderImageRegion(controller, request),\n };\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { BlendMode } from './blend';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport { getRotatedSize, normalizeAngle } from './geometry';\nimport type { ImageHistogram } from './histogram';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Adjustment layer id from get_adjustments (e.g. \"adj_1\").' } as const;\n\nconst round = (n: number, digits = 2) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** Mean and the 1st / 99th percentiles of a 256-bin histogram. */\nfunction binStats(bins: ArrayLike<number>, samples: number) {\n if (!samples) return { mean: null, p1: null, p99: null };\n let sum = 0;\n let seen = 0;\n let p1: number | null = null;\n let p99: number | null = null;\n for (let i = 0; i < bins.length; i++) {\n sum += i * bins[i];\n seen += bins[i];\n if (p1 === null && seen >= samples * 0.01) p1 = i;\n if (p99 === null && seen >= samples * 0.99) p99 = i;\n }\n return { mean: round(sum / samples, 1), p1, p99 };\n}\n\n/** Sums 256 bins into `count` equal buckets. */\nfunction rebin(bins: ArrayLike<number>, count: number): number[] {\n const out = new Array<number>(count).fill(0);\n const width = bins.length / count;\n for (let i = 0; i < bins.length; i++) out[Math.floor(i / width)] += bins[i];\n return out;\n}\n\nfunction histogramSummary(h: ImageHistogram, bins: number | undefined) {\n const pct = (n: number) => (h.samples ? round((n / h.samples) * 100) : 0);\n const channels = { red: h.red, green: h.green, blue: h.blue, luminance: h.luminance };\n return {\n samples: h.samples,\n transparent: h.transparent,\n clipping: {\n shadowsPercent: { red: pct(h.shadows[0]), green: pct(h.shadows[1]), blue: pct(h.shadows[2]) },\n highlightsPercent: { red: pct(h.highlights[0]), green: pct(h.highlights[1]), blue: pct(h.highlights[2]) }\n },\n stats: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, binStats(v, h.samples)])),\n ...(bins ? { bins: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, rebin(v, bins)])) } : {})\n };\n}\n\n/**\n * Agent tools for the image editor's layers, geometry and history: `get_image_info`,\n * `get_histogram`, `update_adjustment_layer`, `remove_adjustment_layer`,\n * `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`, `flip_image`,\n * `undo_image` and `redo_image`. Each write is one undo step and returns what changed.\n */\nexport function createImageEditTools(controller: ImageEditorController): AgentTool[] {\n const layerOrThrow = (id: string) => {\n const layers = controller.getAdjustmentLayers();\n const index = layers.findIndex((l) => l.id === id);\n if (index === -1) {\n const ids = layers.map((l) => l.id);\n throw new Error(`Unknown adjustment layer \"${id}\". ${ids.length ? `Layers: ${ids.join(', ')}.` : 'There are no adjustment layers.'}`);\n }\n return { layer: layers[index], index };\n };\n const layerJson = (id: string) => JSON.parse(JSON.stringify(controller.getAdjustmentLayer(id)));\n const geometry = () => {\n const s = controller.getState();\n const region = s.crop ?? { x: 0, y: 0, ...s.imageDimensions };\n const out = getRotatedSize(region.width, region.height, s.transform);\n return {\n image: { ...s.imageDimensions },\n crop: s.crop ? { ...s.crop } : null,\n rotation: normalizeAngle(s.transform.rotate),\n flipH: s.transform.flipH,\n flipV: s.transform.flipV,\n output: { width: Math.round(out.width), height: Math.round(out.height) }\n };\n };\n const history = () => ({ canUndo: controller.canUndo, canRedo: controller.canRedo });\n const requireImage = () => {\n const { width, height } = controller.getState().imageDimensions;\n if (!width || !height) throw new Error('No image is loaded. Ask the user to open one first.');\n return { width, height };\n };\n\n const getImageInfo: AgentTool = {\n name: 'get_image_info',\n title: 'Read image geometry',\n description:\n 'Returns the source image size in pixels, the crop box (natural image pixels, or null for the whole image), rotation in degrees, flips, the output size after crop and rotation, the number of adjustment layers and whether undo / redo are available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read image geometry', ...READ },\n handler: () => ({ ...geometry(), layers: controller.getAdjustmentLayers().length, ...history() })\n };\n\n const getHistogram: AgentTool = {\n name: 'get_histogram',\n title: 'Read the histogram',\n description:\n 'Returns the histogram of the last rendered preview (after adjustments and layers): per channel and luminance mean and 1st / 99th percentiles (0-255), and the percentage of pixels clipped to pure black or white per channel. ' +\n 'Use it to judge exposure and clipping numerically. Pass bins for the counts themselves. Fails before the first render: call render_preview first.',\n inputSchema: {\n type: 'object',\n properties: {\n bins: { type: 'integer', enum: [16, 32, 64, 256], description: 'Also return the counts in this many buckets per channel (default: no counts).' }\n },\n additionalProperties: false\n },\n annotations: { title: 'Read the histogram', ...READ },\n handler: ({ bins }) => {\n const h = controller.getHistogram();\n if (!h) throw new Error('No histogram yet: it comes from the rendered preview. Call render_preview, then try again.');\n return histogramSummary(h, bins);\n }\n };\n\n const updateLayer: AgentTool = {\n name: 'update_adjustment_layer',\n title: 'Change an adjustment layer',\n description:\n 'Shows or hides a layer, renames it, or changes its blend mode or opacity, as one undo step. Change its parameters with set_adjustments (\"layers.<id>.<control>\"). Returns the updated layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n enabled: { type: 'boolean', description: 'false hides the layer without deleting it.' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (see list_controls).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1.' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' }\n },\n required: ['layer'],\n additionalProperties: false\n },\n annotations: { title: 'Change an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, enabled, blend, opacity, name }) => {\n layerOrThrow(layer);\n if (enabled === undefined && blend === undefined && opacity === undefined && name === undefined) {\n throw new Error('Nothing to change. Pass enabled, blend, opacity or name.');\n }\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n controller.updateAdjustmentLayer(layer, { enabled, blend: blend as BlendMode | undefined, opacity, name });\n return { layer: layerJson(layer) };\n }\n };\n\n const removeLayer: AgentTool = {\n name: 'remove_adjustment_layer',\n title: 'Delete an adjustment layer',\n description: 'Deletes an adjustment layer and its mask as one undo step. To hide it instead, use update_adjustment_layer with enabled false. Returns the remaining layer ids, bottom to top.',\n inputSchema: { type: 'object', properties: { layer: LAYER_ID }, required: ['layer'], additionalProperties: false },\n annotations: { title: 'Delete an adjustment layer', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n handler: ({ layer }) => {\n layerOrThrow(layer);\n controller.removeAdjustmentLayer(layer);\n return { removed: layer, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const moveLayer: AgentTool = {\n name: 'move_adjustment_layer',\n title: 'Reorder an adjustment layer',\n description:\n 'Moves an adjustment layer to a position in the stack (0 = bottom, applied first) as one undo step. Layers apply bottom to top, so order changes the result. Returns the layer ids, bottom to top.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n to: { type: 'integer', minimum: 0, description: 'New index, 0 = bottom; past the top means the top.' }\n },\n required: ['layer', 'to'],\n additionalProperties: false\n },\n annotations: { title: 'Reorder an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, to }) => {\n const { index } = layerOrThrow(layer);\n const top = controller.getAdjustmentLayers().length - 1;\n const target = Math.min(to, top);\n if (target === index) throw new Error(`Layer \"${layer}\" is already at index ${index} (0 = bottom, ${top} = top).`);\n controller.moveAdjustmentLayer(layer, target);\n return { layer, index: target, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const setCrop: AgentTool = {\n name: 'set_crop',\n title: 'Crop the image',\n description:\n 'Sets the crop box in natural image pixels of the un-rotated source (origin top-left; see get_image_info for the size) as one undo step. Non-destructive: reset_crop restores the whole image. Returns the new geometry and output size.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', minimum: 0, description: 'Left edge in source pixels.' },\n y: { type: 'number', minimum: 0, description: 'Top edge in source pixels.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width in source pixels.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height in source pixels.' }\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false\n },\n annotations: { title: 'Crop the image', ...WRITE_IDEMPOTENT },\n handler: ({ x, y, width, height }) => {\n const image = requireImage();\n if (x + width > image.width + 0.5 || y + height > image.height + 0.5) {\n throw new Error(`The crop box (${x}, ${y}, ${width} × ${height}) goes outside the ${image.width} × ${image.height} image.`);\n }\n controller.setCrop({ x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) });\n return geometry();\n }\n };\n\n const resetCrop: AgentTool = {\n name: 'reset_crop',\n title: 'Remove the crop',\n description: 'Removes the crop so the whole image shows again, as one undo step. Returns the new geometry.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Remove the crop', ...WRITE_IDEMPOTENT },\n handler: () => {\n if (!controller.getState().crop) throw new Error('The image is not cropped.');\n controller.resetCrop();\n return geometry();\n }\n };\n\n const setRotation: AgentTool = {\n name: 'set_rotation',\n title: 'Rotate the image',\n description:\n 'Sets the image rotation in degrees clockwise, as one undo step. Quarter turns (90, 180, 270) keep every pixel; other angles straighten the image inside a larger frame with transparent corners, so crop afterwards. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { degrees: { type: 'number', minimum: -360, maximum: 360, description: 'Absolute rotation in degrees clockwise (0 = upright).' } },\n required: ['degrees'],\n additionalProperties: false\n },\n annotations: { title: 'Rotate the image', ...WRITE_IDEMPOTENT },\n handler: ({ degrees }) => {\n requireImage();\n controller.setRotation(degrees);\n return geometry();\n }\n };\n\n const flipImage: AgentTool = {\n name: 'flip_image',\n title: 'Flip the image',\n description: 'Mirrors the image horizontally (left-right) or vertically (top-bottom), as one undo step. Flipping twice restores it. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left-right; vertical mirrors top-bottom.' } },\n required: ['axis'],\n additionalProperties: false\n },\n annotations: { title: 'Flip the image', ...WRITE },\n handler: ({ axis }) => {\n requireImage();\n if (axis === 'horizontal') controller.flipHorizontal();\n else controller.flipVertical();\n return geometry();\n }\n };\n\n const undo: AgentTool = {\n name: 'undo_image',\n title: 'Undo an image edit',\n description: 'Undoes the last image edit (one tool call is one step). Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Undo an image edit', ...WRITE },\n handler: () => {\n if (!controller.undo()) throw new Error('Nothing to undo.');\n return history();\n }\n };\n\n const redo: AgentTool = {\n name: 'redo_image',\n title: 'Redo an image edit',\n description: 'Redoes the last undone image edit. Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Redo an image edit', ...WRITE },\n handler: () => {\n if (!controller.redo()) throw new Error('Nothing to redo.');\n return history();\n }\n };\n\n return [getImageInfo, getHistogram, updateLayer, removeLayer, moveLayer, setCrop, resetCrop, setRotation, flipImage, undo, redo];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { contentBounds } from '../raster/surface';\nimport { findLayer, type ImageLayer, type LayerFilter, type RasterMask } from './layers/model';\nimport { LayerOperationError, requireLayer, type LayerPlacement } from './layers/ops';\nimport { adjustmentKindLabel } from './adjustment-layers';\nimport { BLEND_MODES } from './blend';\nimport type { BlendMode } from './blend';\nimport type { CropBox } from './types';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** A short description of a mask for agents (no pixels). */\nexport function maskSummary(mask: RasterMask | undefined): Record<string, unknown> | null {\n if (!mask) return null;\n const out: Record<string, unknown> = {\n kind: mask.source ? 'depth' : 'raster',\n enabled: mask.enabled !== false,\n linked: mask.linked !== false,\n density: mask.density ?? 1,\n feather: mask.feather ?? 0,\n };\n if (!mask.source) {\n out.defaultValue = mask.defaultValue;\n out.paintedBounds = contentBounds(mask.surface);\n }\n return out;\n}\n\n/**\n * A plain description of a layer (and its children) for agents: ids, types, names, document\n * bounds, visibility, opacity, fill, blend, clipping, locks, mask and effects summaries and\n * whether it is active. `boundsOf` gives a pixel layer's document bounds.\n */\nexport function describeLayer(\n layer: ImageLayer,\n activeId: string | null,\n boundsOf: (layer: ImageLayer) => CropBox | null\n): Record<string, unknown> {\n const out: Record<string, unknown> = {\n id: layer.id,\n type: layer.type,\n name: layer.name || (layer.type === 'adjustment' ? adjustmentKindLabel(layer.kind) : ''),\n visible: layer.visible,\n opacity: round(layer.opacity),\n blend: layer.blend,\n clipped: Boolean(layer.clipped),\n locks: layer.locks ? { ...layer.locks } : {},\n mask: maskSummary(layer.mask),\n effects: layer.effects\n ? { enabled: layer.effects.enabled, scale: layer.effects.scale ?? 1, items: layer.effects.items.map((e) => ({ id: e.id, kind: e.kind, enabled: e.enabled })) }\n : null,\n active: layer.id === activeId,\n };\n switch (layer.type) {\n case 'pixel':\n out.bounds = boundsOf(layer);\n out.offset = { ...layer.offset };\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.filters = (layer.filters ?? []).map((f) => ({ ...f }));\n if (layer.source === 'document') out.source = 'document';\n break;\n case 'adjustment':\n out.kind = layer.kind;\n out.params = JSON.parse(JSON.stringify(layer.params));\n break;\n case 'annotation':\n out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\n break;\n case 'group':\n out.collapsed = Boolean(layer.collapsed);\n out.children = layer.children.map((c) => describeLayer(c, activeId, boundsOf));\n break;\n case 'smart':\n out.bounds = boundsOf(layer);\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.contents = { id: layer.contents.id, name: layer.contents.name, kind: layer.contents.kind, width: layer.contents.width, height: layer.contents.height };\n out.smartFilters = (layer.smartFilters?.filters ?? []).map((f) => ({ id: f.id, kind: f.kind, enabled: f.enabled }));\n out.filterMask = maskSummary(layer.smartFilters?.mask);\n break;\n }\n return out;\n}\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers.' } as const;\nconst PLACEMENT = {\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (same group).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer (same group).' },\n parentId: { type: 'string', description: 'Put the layer inside this group (at index, default its top).' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom (with parentId, or at the top level).' },\n} as const;\nconst BLEND = { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' } as const;\n\nfunction placementFrom(args: Record<string, unknown>): LayerPlacement {\n const p: LayerPlacement = {};\n if (typeof args.aboveId === 'string') p.aboveId = args.aboveId;\n if (typeof args.belowId === 'string') p.belowId = args.belowId;\n if (typeof args.parentId === 'string') p.parentId = args.parentId;\n if (typeof args.index === 'number') p.index = args.index;\n return p;\n}\n\n/**\n * Agent tools for the image editor's layer stack: `list_layers`, `set_active_layer`,\n * `add_pixel_layer`, `duplicate_layer`, `remove_layer`, `move_layer`, `set_layer_properties`,\n * `group_layers`, `ungroup_layer`, `merge_down`, `merge_visible`, `flatten_image`, `offset_layer`,\n * `add_layer_mask`, `apply_layer_mask`, `remove_layer_mask`, `set_layer_mask_properties`,\n * `layer_via_copy`, `add_layer_filter`, `update_layer_filter` and `remove_layer_filter`. Each write\n * is one undo step, validates before changing anything and returns what it changed.\n */\nexport function createImageLayerTools(controller: ImageEditorController): AgentTool[] {\n const describe = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n if (!loc) return null;\n return {\n ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)),\n parentId: loc.parent?.id ?? null,\n index: loc.index,\n };\n };\n const exists = (id: unknown, what = 'layerId') => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError(`${what} is required (a layer id from list_layers).`);\n requireLayer(controller.getLayers(), id);\n return id;\n };\n\n const listLayers: AgentTool = {\n name: 'list_layers',\n title: 'List image layers',\n description:\n '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.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'List image layers', ...READ },\n handler: () => {\n const s = controller.getState();\n return {\n canvas: { ...s.imageDimensions },\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n };\n },\n };\n\n const setActive: AgentTool = {\n name: 'set_active_layer',\n title: 'Set the active layer',\n description:\n 'Makes a layer active (the base sliders of set_adjustments then edit its develop filter when it is a pixel layer) and chooses whether painting targets its pixels or its mask. View state, not an undo step. Returns the active layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Layer to activate (from list_layers).' },\n target: { type: 'string', enum: ['pixels', 'mask'], description: 'Paint target (default pixels; mask needs a mask).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the active layer', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, target }) => {\n const id = exists(layerId);\n const layer = controller.getLayer(id)!;\n if (target === 'mask' && !layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n controller.setActiveLayer(id, (target as 'pixels' | 'mask' | undefined) ?? 'pixels');\n return { layer: describe(id), activeTarget: controller.getState().activeTarget };\n },\n };\n\n const addPixelLayer: AgentTool = {\n name: 'add_pixel_layer',\n title: 'Add a pixel layer',\n description:\n 'Adds a pixel layer and makes it active: empty (default), filled with color over the canvas, or holding an image (PNG data URL or base64 PNG; other formats and URLs only in a browser) placed at x, y. Goes above the active layer unless aboveId / belowId / parentId / index is given. One undo step. Returns the new layer with its id, bounds and position.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Layer name (default \"Layer N\").' },\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa; omit for an empty layer.' },\n image: { type: 'string', description: 'Image to place: PNG data URL or base64 PNG (or any URL in a browser).' },\n x: { type: 'integer', description: 'Left of the image in document pixels (default 0).' },\n y: { type: 'integer', description: 'Top of the image in document pixels (default 0).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1 (default 1).' },\n blend: BLEND,\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a pixel layer', ...WRITE },\n handler: async (args) => {\n if (args.color !== undefined && args.image !== undefined) throw new LayerOperationError('Give color or image, not both.');\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n const init = {\n name: args.name as string | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n ...placementFrom(args),\n };\n const id =\n typeof args.image === 'string'\n ? await controller.addPixelLayerFromImage(args.image, init)\n : controller.addPixelLayer({ ...init, color: args.color as string | undefined });\n return { layer: describe(id) };\n },\n };\n\n const duplicate: AgentTool = {\n name: 'duplicate_layer',\n title: 'Duplicate a layer',\n description: 'Duplicates a layer (groups with their children) directly above it and makes the copy active. One undo step. Returns the copy with its new id.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' } },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Duplicate a layer', ...WRITE },\n handler: ({ layerId, name }) => ({ layer: describe(controller.duplicateLayer(exists(layerId), name as string | undefined)) }),\n };\n\n const remove: AgentTool = {\n name: 'remove_layer',\n title: 'Delete a layer',\n description:\n 'Deletes a layer (a group with its children). Refused for a layer locked with all. Prefer hiding it (set_layer_properties visible false) when it may be wanted back. One undo step. Returns the removed id and the remaining top-level ids.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayer(id);\n return { removed: id, layers: controller.getLayers().map((l) => l.id) };\n },\n };\n\n const move: AgentTool = {\n name: 'move_layer',\n title: 'Move a layer in the stack',\n description:\n 'Moves a layer above or below another layer (aboveId / belowId, in that layer\\'s group), into a group (parentId, optional index), or to an index at the top level. Give exactly one of aboveId, belowId, parentId, or index alone. One undo step. Returns the new parentId and index.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, ...PLACEMENT },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer in the stack', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n return { layerId: id, ...controller.moveLayer(id, placementFrom(args)) };\n },\n };\n\n const setProps: AgentTool = {\n name: 'set_layer_properties',\n title: 'Set layer properties',\n description:\n 'Changes a layer\\'s name, visible, opacity (0..1), fillOpacity (0..1, pixel layers: fades the pixels but not masks), blend (pass-through is for groups), clipped (clip to the layer below), locks ({pixels, transparency, position, all}) or collapsed (groups). Only the given fields change. Refused (except visible, locks and collapsed) on a layer locked with all. One undo step. Returns the layer after the change.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n name: { type: 'string', description: 'New name.' },\n visible: { type: 'boolean', description: 'Show (true) or hide (false).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1.' },\n fillOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill 0..1 (pixel layers).' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode; pass-through for groups only.' },\n clipped: { type: 'boolean', description: 'Clip to the nearest unclipped layer below.' },\n locks: {\n type: 'object',\n description: 'Locks to set (missing keys are unlocked).',\n properties: {\n pixels: { type: 'boolean', description: 'Refuse painting and filling.' },\n transparency: { type: 'boolean', description: 'Painting keeps alpha.' },\n position: { type: 'boolean', description: 'Refuse moving.' },\n all: { type: 'boolean', description: 'Everything, including properties and deletion.' },\n },\n additionalProperties: false,\n },\n collapsed: { type: 'boolean', description: 'Collapse a group in the layers panel.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer properties', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['name', 'visible', 'opacity', 'fillOpacity', 'blend', 'clipped', 'locks', 'collapsed']) if (args[k] !== undefined) props[k] = args[k];\n if (!Object.keys(props).length) throw new LayerOperationError('Give at least one property to change.');\n controller.setLayerProperties(id, props);\n return { layer: describe(id) };\n },\n };\n\n const group: AgentTool = {\n name: 'group_layers',\n title: 'Group layers',\n description:\n 'Puts sibling layers (same group or top level) into a new pass-through group at the position of the topmost one, keeping their order. One undo step. Returns the group with its id and children.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Ids of the layers to group (from list_layers).' },\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n },\n required: ['layerIds'],\n additionalProperties: false,\n },\n annotations: { title: 'Group layers', ...WRITE },\n handler: ({ layerIds, name }) => {\n const ids = (layerIds as string[]).map((id) => exists(id, 'layerIds[]'));\n return { group: describe(controller.groupLayers(ids, name as string | undefined)) };\n },\n };\n\n const addGroup: AgentTool = {\n name: 'add_group',\n title: 'Add an empty group',\n description:\n 'Adds an empty group (pass-through unless blend is given) above the active layer, or at aboveId / belowId / parentId / index, and makes it active. Move layers into it with move_layer parentId, or use group_layers to group existing layers. One undo step. Returns the group.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode (default pass-through).' },\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add an empty group', ...WRITE },\n handler: (args) => {\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n return { group: describe(controller.addGroup({ name: args.name as string | undefined, blend: args.blend as never, ...placementFrom(args) })) };\n },\n };\n\n const ungroup: AgentTool = {\n name: 'ungroup_layer',\n title: 'Ungroup a group',\n description: 'Replaces a group by its children at its position (the group\\'s own opacity, blend and mask are dropped). One undo step. Returns the child ids bottom to top.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'Group id.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Ungroup a group', ...WRITE },\n handler: ({ layerId }) => ({ childIds: controller.ungroupLayer(exists(layerId)) }),\n };\n\n const mergeDown: AgentTool = {\n name: 'merge_down',\n title: 'Merge a layer down',\n description:\n 'Composites a layer (pixel, adjustment, group or annotation) onto the pixel layer directly below it. The lower layer keeps its name, opacity and blend; masks, fill and filters are baked into its pixels. Refused when the layer below is not a pixel layer or is locked, or when the layer is hidden. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'The upper layer to merge down.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge a layer down', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.mergeDown(exists(layerId))) }),\n };\n\n const mergeVisible: AgentTool = {\n name: 'merge_visible',\n title: 'Merge visible layers',\n description: 'Composites every visible top-level layer into one normal pixel layer at the position of the lowest of them; hidden layers stay. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Merge visible layers', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.mergeVisible()) }),\n };\n\n const flatten: AgentTool = {\n name: 'flatten_image',\n title: 'Flatten the image',\n description: 'Composites every visible layer into one Background pixel layer and discards hidden layers. Crop, rotation and output opacity stay. One undo step. Returns the Background layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Flatten the image', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.flattenImage()) }),\n };\n\n const offset: AgentTool = {\n name: 'offset_layer',\n title: 'Move a layer\\'s pixels',\n description:\n 'Moves a pixel layer (and its mask) to an absolute offset x, y or by dx, dy, in document pixels (integers). Refused when its position is locked. One undo step. Returns the new offset and document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n x: { type: 'integer', description: 'New horizontal offset in document pixels.' },\n y: { type: 'integer', description: 'New vertical offset in document pixels.' },\n dx: { type: 'integer', description: 'Move right by this many pixels (negative: left).' },\n dy: { type: 'integer', description: 'Move down by this many pixels (negative: up).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer\\'s pixels', ...WRITE },\n handler: ({ layerId, x, y, dx, dy }) => {\n const id = exists(layerId);\n if ([x, y, dx, dy].every((v) => v === undefined)) throw new LayerOperationError('Give x and y, or dx and dy.');\n if ((x !== undefined || y !== undefined) && (dx !== undefined || dy !== undefined)) throw new LayerOperationError('Give x / y or dx / dy, not both.');\n return { layerId: id, ...controller.offsetLayer(id, { x: x as number | undefined, y: y as number | undefined, dx: dx as number | undefined, dy: dy as number | undefined }) };\n },\n };\n\n const addMask: AgentTool = {\n name: 'add_layer_mask',\n title: 'Add a layer mask',\n description:\n 'Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal) or selection (the selection\\'s coverage). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection'], description: 'What the mask starts as (default reveal-all).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px (default 0).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer mask', ...WRITE },\n handler: ({ layerId, from, density, feather }) => {\n const id = exists(layerId);\n const mask = controller.addLayerMask(id, (from as 'reveal-all' | 'hide-all' | 'selection' | undefined) ?? 'reveal-all', {\n density: density as number | undefined,\n feather: feather as number | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_layer_mask',\n title: 'Apply a layer mask',\n description: \"Bakes a pixel layer's mask into its pixels (alpha × coverage, including density and feather) and removes the mask. Pixel layers only; refused when its pixels are locked. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Apply a layer mask', ...DESTRUCTIVE },\n handler: async ({ layerId }) => {\n const id = exists(layerId);\n await controller.applyLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const removeMask: AgentTool = {\n name: 'remove_layer_mask',\n title: 'Delete a layer mask',\n description: \"Deletes a layer's mask without applying it (the layer shows everywhere again). Disable it instead with set_layer_mask_properties enabled false to keep it. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer mask', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const maskProps: AgentTool = {\n name: 'set_layer_mask_properties',\n title: 'Set layer mask properties',\n description:\n \"Changes a layer mask's density (0..1), feather (px), enabled flag or linked flag; its pixels are kept. An unlinked mask (linked false) stays in place when the layer is moved, transformed or warped; transform it alone with transform_layer target mask. One undo step. Returns the mask summary.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Density 0..1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px.' },\n enabled: { type: 'boolean', description: 'false ignores the mask but keeps it.' },\n linked: { type: 'boolean', description: 'false unlinks the mask from the layer pixels (it stays put when they move or transform); true links it again.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer mask properties', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, density, feather, enabled, linked }) => {\n const id = exists(layerId);\n if ([density, feather, enabled, linked].every((v) => v === undefined)) throw new LayerOperationError('Give density, feather, enabled or linked.');\n const mask = controller.setLayerMaskProps(id, {\n density: density as number | undefined,\n feather: feather as number | undefined,\n enabled: enabled as boolean | undefined,\n linked: linked as boolean | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const viaCopy: AgentTool = {\n name: 'layer_via_copy',\n title: 'New layer from the selection',\n description:\n 'Layer via Copy: copies the selected pixels of a pixel layer (layerId, default the active layer) to a new layer directly above it and makes it active. cut: true also removes them from the source (Layer via Cut). Needs a selection. One undo step. Returns the new layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Pixel layer to copy from (default the active layer).' },\n cut: { type: 'boolean', description: 'Remove the pixels from the source too (default false).' },\n name: { type: 'string', description: 'Name of the new layer.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'New layer from the selection', ...WRITE },\n handler: ({ layerId, cut, name }) => {\n if (layerId !== undefined) exists(layerId);\n return { layer: describe(controller.layerViaCopy({ layerId: layerId as string | undefined, cut: cut === true, name: name as string | undefined })) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_layer',\n title: 'Rasterize a layer',\n description:\n 'Rasterize Layer: turns an annotation layer into a pixel layer (same id, name, opacity, blend and mask; needs a browser) or bakes a pixel layer\\'s filters (develop, blur) into its pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a layer', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) }),\n };\n\n const filterArgs = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it.' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n } as const;\n const filterValues = (args: Record<string, unknown>) => {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(filterArgs)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n };\n\n const addFilter: AgentTool = {\n name: 'add_layer_filter',\n title: 'Add a layer filter',\n description:\n 'Adds a live filter to a pixel layer: develop (the base sliders: brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px). One of each kind per layer, applied bottom to top before the mask. One undo step. Returns the layer\\'s filters with their ids.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' }, ...filterArgs },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer filter', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n const filters = controller.addLayerFilter(id, { kind: args.kind as LayerFilter['kind'], ...filterValues(args) } as never);\n return { layerId: id, filters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_layer_filter',\n title: 'Change a layer filter',\n description: \"Changes one filter of a pixel layer (only the given values; the kind cannot change). One undo step. Returns the layer's filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers or add_layer_filter.' }, ...filterArgs },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const changes = filterValues(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n return { layerId: id, filters: controller.updateLayerFilter(id, args.filterId as string, changes) };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_layer_filter',\n title: 'Remove a layer filter',\n description: \"Removes one filter from a pixel layer. One undo step. Returns the layer's remaining filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers.' } },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Remove a layer filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = exists(layerId);\n return { layerId: id, filters: controller.removeLayerFilter(id, filterId as string) };\n },\n };\n\n return [\n listLayers,\n setActive,\n addPixelLayer,\n duplicate,\n remove,\n move,\n setProps,\n group,\n addGroup,\n ungroup,\n mergeDown,\n mergeVisible,\n flatten,\n offset,\n addMask,\n applyMask,\n removeMask,\n maskProps,\n viaCopy,\n rasterize,\n addFilter,\n updateFilter,\n removeFilter,\n ];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES } from './blend';\nimport { LayerOperationError, requireLayer } from './layers/ops';\nimport {\n effectsMargin,\n isLayerEffectKind,\n layerEffectLabel,\n LAYER_EFFECT_KINDS,\n type LayerEffectKind,\n type LayerEffects,\n} from '../raster/effects/model';\nimport { builtinPattern, builtinPatternIds } from '../raster/effects/patterns';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers (a pixel, annotation or group layer).' } as const;\nconst EFFECT_ID = { type: 'string', description: 'Effect id from list_layer_effects or add_layer_effect.' } as const;\nconst BLEND_IDS = BLEND_MODES.map((m) => m.id).filter((id) => id !== 'pigment');\n\n/** Shared shapes, referenced as `#/$defs/...`. */\nconst DEFS = {\n color: { type: 'string', pattern: '^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$', description: 'Colour as #rrggbb.' },\n contour: {\n type: 'object',\n 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}].',\n properties: {\n name: { type: 'string', description: 'Display name (e.g. \"Cone\", \"Gaussian\").' },\n points: {\n type: 'array',\n minItems: 2,\n description: 'Curve points.',\n items: {\n type: 'object',\n properties: {\n input: { type: 'number', minimum: 0, maximum: 1, description: 'Coverage in (0..1).' },\n output: { type: 'number', minimum: 0, maximum: 1, description: 'Coverage out (0..1).' },\n corner: { type: 'boolean', description: 'Sharp corner instead of a smooth point (default false).' },\n },\n required: ['input', 'output'],\n additionalProperties: false,\n },\n },\n },\n required: ['name', 'points'],\n additionalProperties: false,\n },\n gradient: {\n type: 'object',\n description: 'A gradient: colour stops, optional opacity stops and its layout.',\n properties: {\n stops: {\n type: 'array',\n minItems: 1,\n description: 'Colour stops { position 0..1, color #rrggbb }.',\n items: {\n type: 'object',\n properties: { position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' }, color: { $ref: '#/$defs/color' } },\n required: ['position', 'color'],\n additionalProperties: false,\n },\n },\n opacityStops: {\n type: 'array',\n description: 'Opacity stops { position 0..1, opacity 0..1 } (default opaque).',\n items: {\n type: 'object',\n properties: {\n position: { type: 'number', minimum: 0, maximum: 1, description: '0..1 along the gradient.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: '0..1.' },\n },\n required: ['position', 'opacity'],\n additionalProperties: false,\n },\n },\n style: { type: 'string', enum: ['linear', 'radial', 'angle', 'reflected', 'diamond'], description: 'Geometry.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Degrees, 90 = bottom to top.' },\n scale: { type: 'number', minimum: 0.1, maximum: 1.5, description: '1 = 100 %.' },\n reverse: { type: 'boolean', description: 'Swap the ends.' },\n align: { type: 'boolean', description: 'Lay out over the layer bounds (true) or the canvas.' },\n offset: {\n type: 'object',\n description: 'Centre offset as fractions of the bounds.',\n 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.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n required: ['stops', 'style', 'angle', 'scale', 'reverse', 'align', 'offset'],\n additionalProperties: false,\n },\n phase: {\n type: 'object',\n description: 'Pattern origin offset in px.',\n properties: { x: { type: 'number', description: 'px.' }, y: { type: 'number', description: 'px.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n fill: {\n description: 'What a stroke or glow paints with. Glows take color or gradient only.',\n anyOf: [\n {\n type: 'object',\n properties: { type: { const: 'color', description: 'Solid colour.' }, color: { $ref: '#/$defs/color' } },\n required: ['type', 'color'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: { type: { const: 'gradient', description: 'Gradient.' }, gradient: { $ref: '#/$defs/gradient' } },\n required: ['type', 'gradient'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: {\n type: { const: 'pattern', description: 'Pattern (strokes only).' },\n patternId: { type: 'string', description: 'Pattern id from list_layer_effects.patterns.' },\n name: { type: 'string', description: 'Display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 %.' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Rotation in degrees (default 0).' },\n link: { type: 'boolean', description: 'Tile from the layer (default true) or the canvas origin.' },\n phase: { $ref: '#/$defs/phase' },\n },\n required: ['type', 'patternId', 'scale'],\n additionalProperties: false,\n },\n ],\n },\n} as const;\n\nconst nullable = (schema: Record<string, unknown>, description: string) => ({ anyOf: [schema, { type: 'null' }], description });\n\n/** Every effect setting (each kind takes a subset; the description says which). */\nfunction settingsSchema(forUpdate: boolean): Record<string, unknown> {\n const opt = (schema: Record<string, unknown>, description: string) => (forUpdate ? nullable(schema, `${description} null removes it.`) : { ...schema, description });\n return {\n type: 'object',\n description: forUpdate\n ? 'Settings to change (only these; nested objects are replaced whole). Which settings a kind takes: see add_layer_effect.'\n : \"Settings to change from Photoshop's defaults (all optional). Unknown settings for the kind are refused with the list of its settings.\",\n properties: {\n enabled: { type: 'boolean', description: 'All kinds: false keeps the effect but hides it (default true).' },\n blend: { type: 'string', enum: BLEND_IDS, description: 'All but bevel-emboss: blend mode (shadows multiply, glows screen, others normal).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'All but bevel-emboss: 0..1 (shadows / glows 0.75, satin 0.5, others 1).' },\n color: { $ref: '#/$defs/color', description: 'drop-shadow, inner-shadow, satin, color-overlay: #rrggbb.' },\n 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).' },\n useGlobalLight: { type: 'boolean', description: 'Shadows, bevel-emboss: follow the document global light instead of angle (default true).' },\n distance: { type: 'number', minimum: 0, maximum: 30000, description: 'Shadows, satin: offset in px (default 5; satin 11).' },\n spread: { type: 'number', minimum: 0, maximum: 1, description: 'drop-shadow, outer-glow: 0..1 of the size made solid (default 0).' },\n choke: { type: 'number', minimum: 0, maximum: 1, description: 'inner-shadow, inner-glow: 0..1 of the size made solid (default 0).' },\n 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).' },\n contour: opt({ $ref: '#/$defs/contour' }, 'Shadows, glows, satin: coverage contour (default linear).'),\n antiAlias: { type: 'boolean', description: 'Shadows, glows, satin: smooth the contour (default false; satin true).' },\n noise: { type: 'number', minimum: 0, maximum: 1, description: 'Shadows, glows: speckle 0..1 (default 0).' },\n knocksOut: { type: 'boolean', description: 'drop-shadow: the layer hides the shadow beneath its see-through fill (default true).' },\n fill: { $ref: '#/$defs/fill', description: 'outer-glow, inner-glow (color or gradient), stroke (color, gradient or pattern).' },\n 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).' },\n range: { type: 'number', minimum: 0.01, maximum: 1, description: 'Glows: the part of the glow the contour spans (default 0.5).' },\n jitter: { type: 'number', minimum: 0, maximum: 1, description: 'Glows: gradient jitter 0..1 (stored, not rendered; default 0).' },\n source: { type: 'string', enum: ['edge', 'center'], description: 'inner-glow: glow from the edge (default) or the centre.' },\n style: { type: 'string', enum: ['inner-bevel', 'outer-bevel', 'emboss', 'pillow-emboss', 'stroke-emboss'], description: 'bevel-emboss: style (default inner-bevel).' },\n depth: { type: 'number', minimum: 0, maximum: 10, description: 'bevel-emboss: depth, 1 = 100 % (default 1).' },\n direction: { type: 'string', enum: ['up', 'down'], description: 'bevel-emboss: raised (up, default) or sunk (down).' },\n soften: { type: 'number', minimum: 0, maximum: 16, description: 'bevel-emboss: blur of the height in px (default 0).' },\n altitude: { type: 'number', minimum: 0, maximum: 90, description: 'bevel-emboss: light altitude in degrees (default 30).' },\n glossContour: opt({ $ref: '#/$defs/contour' }, 'bevel-emboss: gloss contour over the shading (default linear).'),\n highlightColor: { $ref: '#/$defs/color', description: 'bevel-emboss: highlight colour (default #ffffff).' },\n highlightBlend: { type: 'string', enum: BLEND_IDS, description: 'bevel-emboss: highlight mode (default screen).' },\n highlightOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'bevel-emboss: highlight opacity (default 0.75).' },\n shadowColor: { $ref: '#/$defs/color', description: 'bevel-emboss: shadow colour (default #000000).' },\n shadowBlend: { type: 'string', enum: BLEND_IDS, description: 'bevel-emboss: shadow mode (default multiply).' },\n shadowOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'bevel-emboss: shadow opacity (default 0.75).' },\n contourElement: opt(\n {\n type: 'object',\n properties: {\n contour: { $ref: '#/$defs/contour' },\n range: { type: 'number', minimum: 0, maximum: 1, description: 'Stored range (default 0.5).' },\n antiAlias: { type: 'boolean', description: 'Smooth the contour.' },\n },\n required: ['contour', 'range', 'antiAlias'],\n additionalProperties: false,\n },\n \"bevel-emboss: the Contour element, shaping the bevel's height profile (default off).\"\n ),\n texture: opt(\n {\n type: 'object',\n properties: {\n patternId: { type: 'string', description: 'Pattern id from list_layer_effects.patterns.' },\n name: { type: 'string', description: 'Display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: '1 = 100 %.' },\n depth: { type: 'number', minimum: -10, maximum: 10, description: '1 = +100 %; negative carves the pattern in.' },\n invert: { type: 'boolean', description: 'Invert the pattern.' },\n link: { type: 'boolean', description: 'Tile from the layer (default true).' },\n phase: { $ref: '#/$defs/phase' },\n },\n required: ['patternId', 'scale', 'depth', 'invert'],\n additionalProperties: false,\n },\n \"bevel-emboss: the Texture element, a pattern's luminance as extra height (default off).\"\n ),\n invert: { type: 'boolean', description: 'satin: invert (default true).' },\n gradient: { $ref: '#/$defs/gradient', description: 'gradient-overlay: the gradient (default black to white, 90°).' },\n dither: { type: 'boolean', description: 'gradient-overlay: dither (stored; default false).' },\n patternId: { type: 'string', description: 'pattern-overlay: pattern id from list_layer_effects.patterns (default checker).' },\n name: { type: 'string', description: 'pattern-overlay: pattern display name.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: 'pattern-overlay: 1 = 100 % (default 1).' },\n link: { type: 'boolean', description: 'pattern-overlay: tile from the layer (default true) or the canvas origin.' },\n phase: { $ref: '#/$defs/phase', description: 'pattern-overlay: origin offset in px (default 0, 0).' },\n position: { type: 'string', enum: ['outside', 'inside', 'center'], description: 'stroke: where the stroke sits on the edge (default outside).' },\n },\n additionalProperties: false,\n };\n}\n\n/** The patterns pattern effects can use: the document's (from a PSD), then the built-in ones. */\nfunction patternList(controller: ImageEditorController): Array<{ id: string; name: string; width: number; height: number; builtin: boolean }> {\n const doc = controller.getDocumentPatterns().map((p) => ({ id: p.id, name: p.name, width: p.width, height: p.height, builtin: false }));\n const builtins = builtinPatternIds().map((id) => {\n const p = builtinPattern(id)!;\n return { id, name: p.name, width: p.width, height: p.height, builtin: true };\n });\n return [...doc, ...builtins];\n}\n\n/** A layer's effects for agents: the master switch, scale, reach and every effect with its settings. */\nexport function describeLayerEffects(effects: LayerEffects): Record<string, unknown> {\n return {\n enabled: effects.enabled,\n scale: effects.scale ?? 1,\n margin: effectsMargin(effects),\n fromPsd: Boolean(effects.psd),\n effects: effects.items.map((e) => ({ ...JSON.parse(JSON.stringify(e)), label: layerEffectLabel(e.kind) })),\n };\n}\n\n/**\n * Agent tools for layer effects: `list_layer_effects`, `add_layer_effect`, `update_layer_effect`,\n * `remove_layer_effect`, `set_layer_effects_enabled` and `set_global_light`. Each write validates\n * every setting first, is one undo step and returns the layer's effects after the change.\n */\nexport function createImageEffectTools(controller: ImageEditorController): AgentTool[] {\n const layerArg = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a layer id from list_layers).');\n requireLayer(controller.getLayers(), id);\n return id;\n };\n const result = (layerId: string, extra: Record<string, unknown> = {}) => ({ layerId, ...extra, ...describeLayerEffects(controller.getLayerEffects(layerId)) });\n\n const list: AgentTool = {\n name: 'list_layer_effects',\n title: 'List layer effects',\n description:\n \"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.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'List layer effects', ...READ },\n handler: ({ layerId }) => result(layerArg(layerId), { globalLight: controller.getGlobalLight(), patterns: patternList(controller), kinds: [...LAYER_EFFECT_KINDS] }),\n };\n\n const add: AgentTool = {\n name: 'add_layer_effect',\n title: 'Add a layer effect',\n description:\n \"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.\",\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: [...LAYER_EFFECT_KINDS], description: 'Effect kind.' },\n settings: settingsSchema(false),\n index: { type: 'integer', minimum: 0, description: \"Position in the layer's effect list (default the end).\" },\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer effect', ...WRITE },\n handler: ({ layerId, kind, settings, index }) => {\n const id = layerArg(layerId);\n if (!isLayerEffectKind(kind)) throw new LayerOperationError(`kind must be one of ${LAYER_EFFECT_KINDS.join(', ')}.`);\n const { effect } = controller.addLayerEffect(id, kind as LayerEffectKind, (settings as Record<string, unknown>) ?? {}, index as number | undefined);\n return result(id, { effectId: effect.id });\n },\n };\n\n const update: AgentTool = {\n name: 'update_layer_effect',\n title: 'Change a layer effect',\n description:\n \"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.\",\n inputSchema: {\n type: 'object',\n $defs: DEFS,\n properties: { layerId: LAYER_ID, effectId: EFFECT_ID, settings: settingsSchema(true) },\n required: ['layerId', 'effectId', 'settings'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer effect', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, effectId, settings }) => {\n const id = layerArg(layerId);\n const changes = (settings as Record<string, unknown>) ?? {};\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one setting to change.');\n controller.updateLayerEffect(id, effectId as string, changes);\n return result(id);\n },\n };\n\n const remove: AgentTool = {\n name: 'remove_layer_effect',\n title: 'Remove a layer effect',\n 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.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, effectId: EFFECT_ID }, required: ['layerId', 'effectId'], additionalProperties: false },\n annotations: { title: 'Remove a layer effect', ...DESTRUCTIVE },\n handler: ({ layerId, effectId }) => {\n const id = layerArg(layerId);\n controller.removeLayerEffect(id, effectId as string);\n return result(id);\n },\n };\n\n const setEnabled: AgentTool = {\n name: 'set_layer_effects_enabled',\n title: 'Switch layer effects on or off',\n description:\n \"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.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n enabled: { type: 'boolean', description: 'true renders the enabled effects; false hides them all.' },\n scale: { type: 'number', minimum: 0.01, maximum: 10, description: 'Scale Effects, 1 = 100 % (unchanged when left out).' },\n },\n required: ['layerId', 'enabled'],\n additionalProperties: false,\n },\n annotations: { title: 'Switch layer effects on or off', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, enabled, scale }) => {\n const id = layerArg(layerId);\n controller.setLayerEffectsEnabled(id, enabled as boolean, scale as number | undefined);\n return result(id);\n },\n };\n\n const light: AgentTool = {\n name: 'set_global_light',\n title: 'Set the global light',\n description:\n 'Sets the document global light that shadows and bevels with useGlobalLight follow (Photoshop default: angle 120°, altitude 30°). One undo step. Returns the light.',\n inputSchema: {\n type: 'object',\n properties: {\n angle: { type: 'number', minimum: -360, maximum: 360, description: \"Degrees, counter-clockwise from 3 o'clock: where the light comes from (unchanged when left out).\" },\n altitude: { type: 'number', minimum: 0, maximum: 90, description: 'Degrees above the canvas, for bevels (unchanged when left out).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Set the global light', ...WRITE_IDEMPOTENT },\n handler: ({ angle, altitude }) => {\n if (angle === undefined && altitude === undefined) throw new LayerOperationError('Give angle, altitude or both.');\n const next: { angle?: number; altitude?: number } = {};\n if (angle !== undefined) next.angle = angle as number;\n if (altitude !== undefined) next.altitude = altitude as number;\n return { globalLight: controller.setGlobalLight(next) };\n },\n };\n\n return [list, add, update, remove, setEnabled, light];\n}\n","/**\n * The paint tools' view model, shared by the React and Svelte editors: the brush options bar's\n * controls, Photoshop's shortcuts (B, Shift+B, E, V, [ ], Shift+[ ], number keys, D, X) and a\n * pointer gesture that turns pointer events into strokes (Brush, Mixer Brush, Eraser) or layer\n * moves (Move).\n */\n\nimport type { ControlSpec } from '../controls';\nimport type { ImageEditorController, PaintStrokeSession } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { MAX_BRUSH_SIZE, type BrushSettings, type MixerSettings } from '../../paint';\n\nconst plain = { kind: 'plain' } as const;\n\n/** Controls of the brush options bar (`hardness`, `opacity`, `flow`, `smoothing` shown in %). */\nexport const BRUSH_OPTION_CONTROLS: readonly (ControlSpec & { id: 'size' | 'hardness' | 'opacity' | 'flow' | 'smoothing' })[] = [\n { id: 'size', label: 'Size', section: 'layer', min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain, unit: 'px' },\n { id: 'hardness', label: 'Hardness', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'flow', label: 'Flow', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'smoothing', label: 'Smoothing', section: 'layer', min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** Controls of the Mixer Brush (shown in %, with the brush controls, while the mixer is active). */\nexport const MIXER_OPTION_CONTROLS: readonly (ControlSpec & { id: 'wet' | 'load' | 'mix' })[] = [\n { id: 'wet', label: 'Wet', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'load', label: 'Load', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'mix', label: 'Mix', section: 'layer', min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** The Mixer Brush's mixing models, for its RGB / Pigment toggle. */\nexport const MIXER_MIXING_OPTIONS: readonly { id: MixerSettings['mixing']; label: string; title: string }[] = [\n { id: 'rgb', label: 'RGB', title: 'Mix colours as RGB averages' },\n { id: 'pigment', label: 'Pigment', title: 'Mix colours like paint: blue and yellow make green' },\n];\n\n/** The paint tools in the toolbar. */\nexport const PAINT_TOOLS = [\n { id: 'move', label: 'Move', shortcut: 'V' },\n { id: 'brush', label: 'Brush', shortcut: 'B' },\n { id: 'mixer', label: 'Mixer', shortcut: 'Shift+B' },\n { id: 'eraser', label: 'Eraser', shortcut: 'E' },\n] as const;\n\nexport type PaintTool = (typeof PAINT_TOOLS)[number]['id'];\n\nexport const isPaintTool = (tool: string): tool is PaintTool => tool === 'move' || tool === 'brush' || tool === 'mixer' || tool === 'eraser';\n\n/** `#rrggbb` of an sRGB triple (0..1), e.g. the Mixer Brush's reservoir. */\nexport function rgbUnitToHex(c: readonly [number, number, number]): string {\n return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, '0')).join('')}`;\n}\n\n/** Photoshop's bracket step for a brush size. */\nfunction sizeStep(size: number): number {\n return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;\n}\n\nexport interface ShortcutEvent {\n key: string;\n shiftKey?: boolean;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n /** The event target: shortcuts are ignored while typing in inputs. */\n target?: unknown;\n}\n\nconst isTyping = (t: unknown) => {\n const el = t as { tagName?: string; isContentEditable?: boolean } | null;\n return Boolean(el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'));\n};\n\n/**\n * Applies a paint shortcut. Returns true when it was one (the caller then prevents the default).\n * B / Shift+B (Mixer Brush) / E / V choose the tool; [ and ] change the size, with Shift the hardness by 25 %; 1..9 set the\n * opacity to 10..90 % and 0 to 100 %; D resets the colours to black and white; X swaps them.\n */\nexport function handlePaintShortcut(controller: ImageEditorController, e: ShortcutEvent): boolean {\n if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;\n const brush = controller.getState().paint.brush;\n const set = (b: Partial<BrushSettings>) => controller.setPaintOptions({ brush: b });\n const key = e.key;\n switch (key.toLowerCase()) {\n case 'b':\n controller.setTool(e.shiftKey || key === 'B' ? 'mixer' : 'brush');\n return true;\n case 'e':\n controller.setTool('eraser');\n return true;\n case 'v':\n controller.setTool('move');\n return true;\n case 'd':\n controller.resetPaintColors();\n return true;\n case 'x':\n controller.swapPaintColors();\n return true;\n }\n if (key === '[' || key === '{') {\n if (e.shiftKey || key === '{') set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });\n else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });\n return true;\n }\n if (key === ']' || key === '}') {\n if (e.shiftKey || key === '}') set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });\n else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });\n return true;\n }\n if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {\n set({ opacity: key === '0' ? 1 : Number(key) / 10 });\n return true;\n }\n return false;\n}\n\n/** One pointer sample in document pixels. */\nexport interface PaintPointerSample extends ImagePoint {\n /** 0..1; a mouse is 1. */\n pressure: number;\n}\n\n/** Pressure of a pointer event: pens report it; mice report 0.5 while pressed, which means \"full\". */\nexport function pointerPressure(e: { pointerType?: string; pressure?: number }): number {\n if (e.pointerType === 'pen' && typeof e.pressure === 'number' && e.pressure > 0) return Math.min(1, e.pressure);\n return 1;\n}\n\n/**\n * Turns pointer events into strokes (Brush, Mixer Brush, Eraser on the active layer's target) or layer moves\n * (Move on the active pixel layer). `down` returns an error message when the tool cannot act.\n */\nexport class PaintGesture {\n private stroke: PaintStrokeSession | null = null;\n private move: { id: string; start: ImagePoint; origin: { x: number; y: number }; moved: boolean } | null = null;\n /** The last refusal (e.g. \"layer is locked\"), for the editor to show. */\n error: string | null = null;\n\n constructor(private readonly controller: ImageEditorController) {}\n\n get active(): boolean {\n return Boolean(this.stroke || this.move);\n }\n\n down(sample: PaintPointerSample): string | null {\n this.error = null;\n const tool = this.controller.getState().activeTool;\n try {\n if (tool === 'move') {\n const layer = this.controller.getActiveLayer();\n if (!layer) throw new Error('Select a layer to move in the Layers panel.');\n if (layer.type !== 'pixel') throw new Error(`Only pixel layers move; \"${layer.name}\" is a ${layer.type} layer.`);\n if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer \"${layer.name}\" has its position locked.`);\n this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };\n return null;\n }\n if (tool === 'brush' || tool === 'eraser' || tool === 'mixer') {\n this.stroke = this.controller.beginStroke({ tool });\n this.stroke.push([sample]);\n return null;\n }\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.stroke = null;\n this.move = null;\n }\n return this.error;\n }\n\n /** Feeds pointer samples (all coalesced events of one move, in order). */\n moveTo(samples: PaintPointerSample[]): void {\n if (!samples.length) return;\n if (this.stroke) {\n this.stroke.push(samples);\n return;\n }\n const m = this.move;\n if (m) {\n const last = samples[samples.length - 1];\n const x = m.origin.x + Math.round(last.x - m.start.x);\n const y = m.origin.y + Math.round(last.y - m.start.y);\n try {\n // The first move of a drag records the undo step; the rest replace it.\n this.controller.offsetLayer(m.id, { x, y }, !m.moved);\n m.moved = true;\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.move = null;\n }\n }\n }\n\n up(): void {\n if (this.stroke) {\n try {\n this.stroke.end();\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n }\n this.stroke = null;\n this.move = null;\n }\n\n cancel(): void {\n this.stroke?.cancel();\n this.stroke = null;\n this.move = null;\n }\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { MAX_BRUSH_SIZE, MIXING_MODELS, type BrushSettings, type MixerLoad, type MixerSettings, type StrokePoint } from '../paint';\nimport type { LayerTarget } from './layers/model';\nimport { rgbUnitToHex } from './layers/paint-ui';\nimport { LayerOperationError } from './layers/ops';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to paint (default the active layer; see list_layers).' },\n channel: {\n type: 'string',\n enum: ['pixels', 'mask', 'filter-mask'],\n description: \"pixels (default for a pixel layer), the layer's mask, or a smart object's smart filter mask (filter-mask): black hides, white reveals.\",\n },\n} as const;\n\nconst BRUSH = {\n type: 'object',\n description: 'Brush settings; missing fields use the current paint options (default round, hard, 20 px).',\n properties: {\n size: { type: 'number', minimum: 1, maximum: MAX_BRUSH_SIZE, description: 'Diameter in px (default 20).' },\n hardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness 0 (soft) .. 1 (hard, default).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees (default 0).' },\n roundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness (default 1).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity: overlapping dabs never exceed it (default 1).' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab towards the opacity (default 1).' },\n smoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Point smoothing 0..0.95 (default 0).' },\n pressureSize: { type: 'boolean', description: 'Point pressure scales the size (default true).' },\n pressureOpacity: { type: 'boolean', description: 'Point pressure scales the opacity (default false).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'How paint combines with the layer (default normal). pigment mixes it in like real paint (blue into yellow gives green) instead of laying it over.' },\n },\n additionalProperties: false,\n} as const;\n\nconst MIXER = {\n type: 'object',\n description:\n 'Mixer Brush settings (tool mixer only); missing fields use the current mixer options (wet 0.5, load 0.5, mix 0.5, loadEachStroke true, cleanEachStroke true, mixing rgb).',\n properties: {\n wet: { type: 'number', minimum: 0, maximum: 1, description: 'How much canvas colour each dab picks up, 0 (dry: paints only the reservoir) .. 1 (default 0.5).' },\n load: { type: 'number', minimum: 0, maximum: 1, description: 'How much paint the reservoir holds, 0 .. 1 (default 0.5); below 1 it drains per dab, so long strokes run dry.' },\n mix: { type: 'number', minimum: 0, maximum: 1, description: \"How far each dab's paint moves from the reservoir towards the picked-up colour, 0 .. 1 (default 0.5).\" },\n loadEachStroke: { type: 'boolean', description: 'Refill the reservoir with `color` (or the foreground) before the stroke (default true). Off, the brush keeps what is left of its last load.' },\n cleanEachStroke: { type: 'boolean', description: 'Empty the pickup well after the stroke (default true). Off, the next stroke starts with the colour this one picked up.' },\n mixing: { type: 'string', enum: [...MIXING_MODELS], description: 'rgb (default): RGB averages. pigment: mixes like paint (Kubelka-Munk), so blue dragged through yellow makes green.' },\n },\n additionalProperties: false,\n} as const;\n\n/** A Mixer Brush load as tool output: hex colours and the amount left. */\nexport function mixerLoadResult(load: MixerLoad): { reservoir: string | null; amount: number; pickup: string | null } {\n return {\n reservoir: load.reservoir ? rgbUnitToHex(load.reservoir) : null,\n amount: Math.round(load.amount * 1000) / 1000,\n pickup: load.pickup ? rgbUnitToHex(load.pickup) : null,\n };\n}\n\nfunction readTarget(args: Record<string, unknown>): { layerId?: string; channel?: LayerTarget } {\n return { layerId: args.layerId as string | undefined, channel: args.channel as LayerTarget | undefined };\n}\n\n/**\n * Agent tools for painting: `paint_stroke` (brush, Mixer Brush or eraser along points, on pixels\n * or a mask), `fill_pixels` and `clear_pixels` (Edit › Fill / Clear inside the selection), each\n * one undo step returning the layer, channel and changed bounds; and the Mixer Brush's load:\n * `get_mixer_brush`, `load_mixer_brush` and `clean_mixer_brush` (view state, no undo).\n */\nexport function createImagePaintTools(controller: ImageEditorController): AgentTool[] {\n const paintStroke: AgentTool = {\n name: 'paint_stroke',\n title: 'Paint a brush stroke',\n description:\n 'Paints one stroke with the brush, smears and blends paint with the Mixer Brush (tool mixer: its reservoir mixes with colour picked up from the layer under each dab, in RGB or pigment mixing), or removes pixels with the eraser, through points in document pixels (optional pressure 0..1 each) on a pixel layer or a layer mask (the mixer paints pixels only). The selection limits the paint; a transparency-locked layer keeps its alpha (the eraser then paints the background colour). One undo step. Returns the layerId, channel, changed bounds and dab count, and for the mixer its mixerLoad after the stroke ({reservoir, amount, pickup}).',\n inputSchema: {\n type: 'object',\n properties: {\n tool: { type: 'string', enum: ['brush', 'eraser', 'mixer'], description: 'brush (default), eraser, or mixer (the Mixer Brush: picks up and blends the layer colour under each dab).' },\n points: {\n type: 'array',\n minItems: 1,\n maxItems: 10000,\n description: 'The path in document pixels; one point paints one dab.',\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in px.' },\n y: { type: 'number', description: 'Document y in px.' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pen pressure (default 1).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n color: {\n type: 'string',\n description:\n 'Paint colour #rrggbb (default the foreground). On a mask its grey value: #000000 hides, #ffffff reveals. For the mixer, the colour its reservoir is loaded with before the stroke (given, it loads even with loadEachStroke false).',\n },\n brush: BRUSH,\n mixer: MIXER,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: (args) => {\n const tool = args.tool as 'brush' | 'eraser' | 'mixer' | undefined;\n if (args.mixer !== undefined && tool !== 'mixer') throw new LayerOperationError('mixer settings apply to the Mixer Brush only; pass tool \"mixer\", or drop mixer.');\n const result = controller.paintStroke({\n tool,\n points: args.points as StrokePoint[],\n color: args.color as string | undefined,\n brush: args.brush as Partial<BrushSettings> | undefined,\n mixer: args.mixer as Partial<MixerSettings> | undefined,\n ...readTarget(args),\n });\n return result.mixerLoad ? { ...result, mixerLoad: mixerLoadResult(result.mixerLoad) } : result;\n },\n };\n\n const getMixer: AgentTool = {\n name: 'get_mixer_brush',\n title: 'Get the Mixer Brush',\n description:\n \"Reads the Mixer Brush: its settings (wet, load, mix, loadEachStroke, cleanEachStroke, mixing) and what it carries now (reservoir #rrggbb or null, amount 0..1 left, pickup #rrggbb or null). Call it before a paint_stroke with tool mixer and loadEachStroke false, to know the brush's starting paint.\",\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Get the Mixer Brush', ...READ },\n handler: () => {\n const { mixer, mixerLoad } = controller.getState().paint;\n return { settings: { ...mixer }, load: mixerLoadResult(mixerLoad) };\n },\n };\n\n const loadMixerTool: AgentTool = {\n name: 'load_mixer_brush',\n title: 'Load the Mixer Brush',\n description:\n 'Fills the Mixer Brush reservoir with a colour (amount 1); the picked-up colour is kept. View state, not an undo step. Use it with loadEachStroke false to carry one load across several paint_stroke calls. Returns the load {reservoir, amount, pickup}.',\n inputSchema: {\n type: 'object',\n properties: { color: { type: 'string', description: 'Paint colour #rrggbb (default the foreground).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Load the Mixer Brush', ...WRITE, idempotentHint: true },\n handler: (args) => mixerLoadResult(controller.loadMixerBrush(args.color as string | undefined)),\n };\n\n const cleanMixerTool: AgentTool = {\n name: 'clean_mixer_brush',\n title: 'Clean the Mixer Brush',\n description:\n 'Cleans the Mixer Brush: empties the pickup well so the next stroke starts without canvas colour; with emptyReservoir also drops the loaded paint (the brush then only smears). View state, not an undo step. Returns the load {reservoir, amount, pickup}.',\n inputSchema: {\n type: 'object',\n properties: { emptyReservoir: { type: 'boolean', description: 'Also drop the reservoir (default false).' } },\n additionalProperties: false,\n },\n annotations: { title: 'Clean the Mixer Brush', ...WRITE, idempotentHint: true },\n handler: (args) => mixerLoadResult(args.emptyReservoir === true ? controller.emptyMixerBrush() : controller.cleanMixerBrush()),\n };\n\n const fill: AgentTool = {\n name: 'fill_pixels',\n title: 'Fill pixels',\n description:\n 'Edit › Fill: fills the selection (or the whole canvas without one, or with useSelection false) on a pixel layer or a mask with a colour (default the foreground) at opacity, with a blend mode. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa (default the foreground).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill opacity 0..1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal). pigment mixes the colour into the pixels like paint, opacity being the share of new paint.' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill pixels', ...WRITE },\n handler: (args) =>\n controller.fillPixels({\n color: args.color as string | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n useSelection: args.useSelection as boolean | undefined,\n ...readTarget(args),\n }),\n };\n\n const clear: AgentTool = {\n name: 'clear_pixels',\n title: 'Clear pixels',\n description:\n 'Edit › Clear: removes the selected pixels of a pixel layer (or, without a selection or with useSelection false, all of them); on a transparency-locked layer or a mask it fills with the background colour instead. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'How much to clear 0..1 (default 1).' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Clear pixels', ...DESTRUCTIVE },\n handler: (args) =>\n controller.clearPixels({ opacity: args.opacity as number | undefined, useSelection: args.useSelection as boolean | undefined, ...readTarget(args) }),\n };\n\n return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { describeLayer } from './layer-tools';\nimport { base64ToBytes, bytesToBase64 } from '../raster/png';\nimport { PsdError } from './psd/binary';\nimport { importPsd } from './psd/import';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst DATA = {\n type: 'string',\n minLength: 1,\n description: 'The PSD or PSB file as base64 (a `data:` URL prefix is accepted).',\n} as const;\n\n/** Base64 (or a data URL) → bytes, refusing anything that is not base64 text. */\nexport function psdBytesFromBase64(data: unknown): Uint8Array {\n if (typeof data !== 'string' || !data.trim()) throw new PsdError('data is required: the PSD file as base64.');\n const body = data.trim().replace(/^data:[^,]*;base64,/i, '');\n if (!/^[A-Za-z0-9+/=_\\s-]+$/.test(body)) throw new PsdError('data is not base64; send the PSD file bytes base64-encoded.');\n const bytes = base64ToBytes(body);\n if (bytes.length < 26) throw new PsdError(`data decodes to ${bytes.length} bytes, too short for a PSD (at least 26).`);\n return bytes;\n}\n\n/**\n * Agent tools for Photoshop files: `inspect_psd` (what a file holds and how it would map),\n * `import_psd` (open it as the document) and `export_psd` (the document as a PSD).\n */\nexport function createImagePsdTools(controller: ImageEditorController): AgentTool[] {\n const inspect: AgentTool = {\n name: 'inspect_psd',\n title: 'Inspect a PSD',\n description:\n '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.',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Inspect a PSD', ...READ },\n handler: ({ data }) => {\n const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });\n return {\n width: res.width,\n height: res.height,\n ...res.info,\n resolution: res.resolution ?? null,\n layers: res.summary.map(({ id: _id, ...s }) => s),\n warnings: res.warnings,\n };\n },\n };\n\n const open: AgentTool = {\n name: 'import_psd',\n title: 'Open a PSD as the document',\n description:\n 'Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility, locks, layer effects (with the patterns and global light of the file) map directly; 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).',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Open a PSD as the document', ...DESTRUCTIVE },\n handler: ({ data }) => {\n const res = controller.loadPsd(psdBytesFromBase64(data));\n const s = controller.getState();\n return {\n width: res.width,\n height: res.height,\n layerIds: res.layerIds,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n activeLayerId: s.activeLayerId,\n warnings: res.warnings,\n };\n },\n };\n\n const save: AgentTool = {\n name: 'export_psd',\n title: 'Export the document as a PSD',\n description:\n 'Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers, masks and layer effects (an effects block opened from a PSD is written back unchanged while its effects are; the patterns effects use are embedded), in the source frame (crop and rotation are not applied). 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.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Export the document as a PSD', ...READ },\n handler: async () => {\n const res = await controller.toPsd();\n return {\n data: bytesToBase64(res.bytes),\n mimeType: 'image/vnd.adobe.photoshop',\n size: res.bytes.length,\n psb: res.psb,\n width: res.width,\n height: res.height,\n warnings: res.warnings,\n };\n },\n };\n\n return [inspect, open, save];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { LayerOperationError } from './layers/ops';\nimport { FREE_TRANSFORM_CONTENTS, type FreeTransformContent, type FreeTransformInit, type FreeTransformResult } from './free-transform';\nimport {\n TRANSFORM_ANCHORS,\n WARP_STYLES,\n anchorPoint,\n isTransformAnchor,\n type AffineMatrix,\n type Quad,\n type TransformParams,\n type WarpMesh,\n type WarpStyle,\n} from './transform';\nimport { INTERPOLATIONS, type Interpolation } from '../raster/transform';\nimport { identityWarpMesh, warpMeshColumns, warpMeshRows } from '../geom/warp';\nimport type { LayerTarget } from './layers/model';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\nconst roundPoint = (p: { x: number; y: number }) => ({ x: round(p.x), y: round(p.y) });\n\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'Document pixels from the left.' }, y: { type: 'number', description: 'Document pixels from the top.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to transform (from list_layers; default the active layer).' },\n content: {\n type: 'string',\n enum: FREE_TRANSFORM_CONTENTS.filter((c) => c !== 'selection-outline'),\n description:\n 'What moves: layer (all its pixels with a linked mask), selection (only the selected pixels, committed back into the layer; the outline moves too), selection-copy (Layer via Copy: the selected pixels on a new layer above, then transformed; the original stays) or auto (default: selection when something is selected, else layer).',\n },\n channel: {\n type: 'string',\n enum: ['pixels', 'mask'],\n description: 'pixels (default) moves the pixels and a linked mask; mask moves an unlinked mask alone (a linked mask always moves with its pixels).',\n },\n interpolation: { type: 'string', enum: INTERPOLATIONS, description: 'Resampling: nearest (hard pixels), bilinear or bicubic (default, Photoshop’s Bicubic).' },\n} as const;\n\nconst GEOMETRY = {\n scaleX: { type: 'number', description: 'Width in percent of the frame (default 100; negative flips horizontally).' },\n scaleY: { type: 'number', description: 'Height in percent of the frame (default 100; negative flips vertically).' },\n rotate: { type: 'number', description: 'Rotation in degrees, clockwise (default 0).' },\n skewX: { type: 'number', minimum: -89, maximum: 89, description: 'Horizontal skew in degrees: vertical edges lean right for positive angles (default 0).' },\n skewY: { type: 'number', minimum: -89, maximum: 89, description: 'Vertical skew in degrees: horizontal edges lean down for positive angles (default 0).' },\n dx: { type: 'number', description: 'Move right after scaling and rotating, in document pixels (default 0).' },\n dy: { type: 'number', description: 'Move down after scaling and rotating, in document pixels (default 0).' },\n reference: {\n description: 'Fixed point of scale, skew and rotate: an anchor of the frame (top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right; default center) or a document point {x, y}.',\n anyOf: [{ type: 'string', enum: TRANSFORM_ANCHORS }, POINT],\n },\n matrix: {\n type: 'array',\n items: { type: 'number' },\n minItems: 6,\n maxItems: 6,\n description: \"Instead of the numeric fields: an affine matrix [a, b, c, d, e, f] in document pixels (x' = a·x + c·y + e, y' = b·x + d·y + f).\",\n },\n corners: {\n type: 'array',\n items: POINT,\n minItems: 4,\n maxItems: 4,\n description:\n 'Instead of the numeric fields: where the frame’s corners go, [top-left, top-right, bottom-right, bottom-left] in document pixels (Distort; for Perspective move a pair of corners symmetrically). The quad must stay convex. Get the frame and its corners from get_transform_frame.',\n },\n} as const;\n\nconst NUMERIC = ['scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'dx', 'dy', 'reference'] as const;\n\nfunction geometryFrom(args: Record<string, unknown>): { matrix?: AffineMatrix; corners?: Quad; params?: TransformParams } {\n const numeric = NUMERIC.filter((k) => args[k] !== undefined);\n const ways = [numeric.length > 0, args.matrix !== undefined, args.corners !== undefined].filter(Boolean).length;\n if (ways === 0) throw new LayerOperationError('Give the transform one way: numeric fields (scaleX, scaleY, rotate, skewX, skewY, dx, dy, reference), matrix, or corners.');\n if (ways > 1) throw new LayerOperationError('Give the transform only one way: numeric fields, matrix or corners, not a mix.');\n if (args.matrix !== undefined) return { matrix: args.matrix as unknown as AffineMatrix };\n if (args.corners !== undefined) return { corners: args.corners as unknown as Quad };\n const params: TransformParams = {};\n for (const k of NUMERIC) if (args[k] !== undefined) (params as Record<string, unknown>)[k] = args[k];\n if (params.scaleX === 0 || params.scaleY === 0) throw new LayerOperationError('scaleX and scaleY must not be 0 (that collapses the layer to a line).');\n if (typeof params.reference === 'string' && !isTransformAnchor(params.reference)) throw new LayerOperationError(`Unknown reference \"${params.reference}\". Use ${TRANSFORM_ANCHORS.join(', ')} or {x, y}.`);\n return { params };\n}\n\nfunction targetFrom(args: Record<string, unknown>): FreeTransformInit {\n const init: FreeTransformInit = {};\n if (typeof args.layerId === 'string') init.layerId = args.layerId;\n if (args.content !== undefined) init.content = args.content as FreeTransformContent;\n init.channel = (args.channel as LayerTarget | undefined) ?? 'pixels';\n if (args.interpolation !== undefined) init.interpolation = args.interpolation as Interpolation;\n return init;\n}\n\nfunction summary(res: FreeTransformResult) {\n return {\n layerId: res.layerId,\n content: res.content,\n moved: res.moved,\n changed: res.changed,\n frame: { ...res.box },\n corners: res.corners.map(roundPoint),\n bounds: res.bounds,\n };\n}\n\n/**\n * Agent tools for Free Transform in the image editor: `get_transform_frame`, `transform_layer`\n * (scale, rotate, skew and move as numbers, a matrix, or distort / perspective corners),\n * `warp_layer` (a preset or a Bézier mesh), `transform_selection` (the outline only),\n * `flip_layer` and `rotate_layer_90`. Every write is one undo step, resamples tile by tile and\n * returns the new bounds.\n */\nexport function createImageTransformTools(controller: ImageEditorController): AgentTool[] {\n const getFrame: AgentTool = {\n name: 'get_transform_frame',\n title: 'Get the Free Transform frame',\n description:\n 'Returns what a transform would move and its frame: content (layer, selection, selection-copy or selection-outline), moved (pixels, mask, selection), the frame (document bounds of the content), its corners [top-left, top-right, bottom-right, bottom-left] and centre. For a smart object (smartObject true) the frame is the contents’ rectangle in contents pixels and the corners are where they land now. Pass meshColumns / meshRows to also get the identity warp mesh over the frame to edit for warp_layer. Changes nothing.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: TARGET.layerId,\n content: { type: 'string', enum: FREE_TRANSFORM_CONTENTS, description: `${TARGET.content.description} selection-outline frames the selection outline alone.` },\n channel: TARGET.channel,\n meshColumns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns of an identity warp mesh to return (omit for none).' },\n meshRows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows of an identity warp mesh to return (default meshColumns).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Get the Free Transform frame', ...READ },\n handler: (args) => {\n const init = targetFrom(args);\n // A copy's frame is the selection's; framing it must not add the layer.\n if (init.content === 'selection-copy') init.content = 'selection';\n const s = controller.beginFreeTransform(init);\n const out: Record<string, unknown> = {\n layerId: s.layerId,\n content: args.content === 'selection-copy' ? 'selection-copy' : s.content,\n moved: [...s.moves],\n frame: { ...s.box },\n corners: s.startGeometry.corners.map(roundPoint),\n center: roundPoint(anchorPoint(s.startGeometry, 'center')),\n };\n if (s.smartObject) out.smartObject = true;\n s.cancel();\n if (args.meshColumns !== undefined) {\n const cols = args.meshColumns as number;\n const rows = (args.meshRows as number | undefined) ?? cols;\n const mesh = identityWarpMesh(s.box, cols, rows);\n out.mesh = { columns: cols, rows, pointsPerRow: warpMeshColumns(mesh), pointRows: warpMeshRows(mesh), points: mesh.points.map(roundPoint) };\n }\n return out;\n },\n };\n\n const transformLayer: AgentTool = {\n name: 'transform_layer',\n title: 'Free Transform a layer',\n description:\n 'Edit › Free Transform: scales, rotates, skews, moves, distorts or puts in perspective a pixel layer (with its linked mask), the selected pixels, a copy of them on a new layer, or an unlinked mask. Give the transform one way: the numeric fields (as Photoshop’s options bar, about a reference point), a matrix, or the frame’s new corners (distort / perspective; read the frame with get_transform_frame). Resamples once, tile by tile; one undo step. Returns layerId (the new layer for selection-copy), what moved, the frame, the corners and the new document bounds.',\n inputSchema: {\n type: 'object',\n properties: { ...TARGET, ...GEOMETRY },\n additionalProperties: false,\n },\n annotations: { title: 'Free Transform a layer', ...WRITE },\n handler: (args) => summary(controller.transformLayer({ ...targetFrom(args), ...geometryFrom(args) })),\n };\n\n const warpLayer: AgentTool = {\n name: 'warp_layer',\n title: 'Warp a layer',\n description:\n 'Edit › Transform › Warp: bends a pixel layer (with its linked mask), the selected pixels, a copy of them, or an unlinked mask through a preset (style with bend and distortions in percent) or a Bézier mesh over the frame (get the frame and an identity mesh to edit from get_transform_frame with meshColumns). Give style or mesh, not both. One undo step. Returns what moved and the new document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n ...TARGET,\n style: { type: 'string', enum: WARP_STYLES, description: 'Preset warp (Photoshop’s menu): arc, arcLower, arcUpper, arch, bulge, shellLower, shellUpper, flag, wave, fish, rise, fisheye, inflate, squeeze, twist.' },\n bend: { type: 'number', minimum: -100, maximum: 100, description: 'Preset bend in percent, −100..100 (default 50).' },\n horizontal: { type: 'number', minimum: -100, maximum: 100, description: 'Preset horizontal distortion in percent, −100..100 (default 0).' },\n vertical: { type: 'number', minimum: -100, maximum: 100, description: 'Preset vertical distortion in percent, −100..100 (default 0).' },\n orientation: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Bend along the horizontal (default) or vertical axis.' },\n mesh: {\n type: 'object',\n description: 'A custom warp: patch columns and rows, and the (3·columns + 1) × (3·rows + 1) control points row by row in document pixels (anchors where both indices are multiples of 3, Bézier handles between). Start from get_transform_frame’s identity mesh.',\n properties: {\n columns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns.' },\n rows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows.' },\n points: { type: 'array', items: POINT, minItems: 16, description: 'Control points, row by row, document pixels.' },\n },\n required: ['columns', 'rows', 'points'],\n additionalProperties: false,\n },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Warp a layer', ...WRITE },\n handler: (args) => {\n const hasPreset = ['style', 'bend', 'horizontal', 'vertical', 'orientation'].some((k) => args[k] !== undefined);\n if (hasPreset && args.mesh !== undefined) throw new LayerOperationError('Give a preset (style, bend, horizontal, vertical, orientation) or a mesh, not both.');\n if (!hasPreset && args.mesh === undefined) throw new LayerOperationError('Give a preset style or a mesh.');\n let warp: WarpMesh | { style: WarpStyle; bend: number; horizontal?: number; vertical?: number; orientation?: 'horizontal' | 'vertical' };\n if (hasPreset) {\n if (args.style === undefined) throw new LayerOperationError(`style is required with a preset. Styles: ${WARP_STYLES.join(', ')}.`);\n warp = {\n style: args.style as WarpStyle,\n bend: (args.bend as number | undefined) ?? 50,\n horizontal: args.horizontal as number | undefined,\n vertical: args.vertical as number | undefined,\n orientation: args.orientation as 'horizontal' | 'vertical' | undefined,\n };\n } else {\n const m = args.mesh as { columns: number; rows: number; points: { x: number; y: number }[] };\n const want = (3 * m.columns + 1) * (3 * m.rows + 1);\n if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} × ${m.rows} mesh needs (3·${m.columns} + 1) × (3·${m.rows} + 1) = ${want} points, got ${m.points.length}.`);\n const knots = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n return summary(controller.transformLayer({ ...targetFrom(args), warp }));\n },\n };\n\n const transformSelection: AgentTool = {\n name: 'transform_selection',\n title: 'Transform the selection outline',\n description:\n 'Select › Transform Selection: scales, rotates, skews, moves or distorts only the selection outline; no pixels change. Give the transform one way: the numeric fields (about a reference point of the selection’s bounds), a matrix, or the new corners of its bounds. Needs a selection. One undo step. Returns the new selection bounds.',\n inputSchema: {\n type: 'object',\n properties: { interpolation: { ...TARGET.interpolation, description: 'Resampling of soft edges: nearest, bilinear or bicubic (default).' }, ...GEOMETRY },\n additionalProperties: false,\n },\n annotations: { title: 'Transform the selection outline', ...WRITE },\n handler: (args) => {\n const res = controller.transformLayer({ content: 'selection-outline', interpolation: args.interpolation as Interpolation | undefined, ...geometryFrom(args) });\n return { changed: res.changed, frame: { ...res.box }, corners: res.corners.map(roundPoint), bounds: res.bounds };\n },\n };\n\n const flipLayer: AgentTool = {\n name: 'flip_layer',\n title: 'Flip a layer',\n description:\n 'Edit › Transform › Flip Horizontal or Vertical: mirrors a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (no resampling). To flip the whole image use flip_image. One undo step. Returns the new bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left ↔ right, vertical top ↔ bottom.' },\n layerId: TARGET.layerId,\n content: TARGET.content,\n channel: TARGET.channel,\n },\n required: ['axis'],\n additionalProperties: false,\n },\n annotations: { title: 'Flip a layer', ...WRITE },\n handler: (args) => summary(controller.flipLayer(args.axis as 'horizontal' | 'vertical', targetFrom(args))),\n };\n\n const rotate90: AgentTool = {\n name: 'rotate_layer_90',\n title: 'Rotate a layer by quarter turns',\n description:\n 'Edit › Transform › Rotate 90° / 180°: turns a pixel layer (with its linked mask), the selected pixels or a copy of them about the centre of their frame, exactly (pixels land on whole pixels; no resampling). To rotate the whole image use set_rotation. One undo step. Returns the new bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n turn: { type: 'integer', enum: [90, -90, 180], description: 'Degrees: 90 clockwise, -90 counter-clockwise, or 180.' },\n layerId: TARGET.layerId,\n content: TARGET.content,\n channel: TARGET.channel,\n },\n required: ['turn'],\n additionalProperties: false,\n },\n annotations: { title: 'Rotate a layer by quarter turns', ...WRITE },\n handler: (args) => summary(controller.rotateLayer90(args.turn as 90 | -90 | 180, targetFrom(args))),\n };\n\n return [getFrame, transformLayer, warpLayer, transformSelection, flipLayer, rotate90];\n}\n\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { bytesToBase64 } from '../raster/png';\nimport { INTERPOLATIONS, type Interpolation } from '../raster/transform';\nimport { WARP_STYLES, type AffineMatrix, type Quad, type WarpMesh, type WarpStyle } from './transform';\nimport { BLEND_MODES } from './blend';\nimport { findLayer } from './layers/model';\nimport { LayerOperationError, requireLayer } from './layers/ops';\nimport { requireSmartObject } from './layers/smart';\nimport { describeLayer, maskSummary } from './layer-tools';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\nconst roundPoint = (p: { x: number; y: number }) => ({ x: round(p.x), y: round(p.y) });\n\nconst LAYER_ID = { type: 'string', description: 'Smart object layer id from list_layers (type smart).' } as const;\nconst FILTER_ID = { type: 'string', description: 'Smart filter id from get_smart_object or add_smart_filter.' } as const;\nconst POINT = {\n type: 'object',\n properties: { x: { type: 'number', description: 'Document pixels from the left.' }, y: { type: 'number', description: 'Document pixels from the top.' } },\n required: ['x', 'y'],\n additionalProperties: false,\n} as const;\nconst FILTER_BLENDS = BLEND_MODES.map((m) => m.id).filter((m) => m !== 'pigment');\n\nconst FILTER_SETTINGS = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it (the eye in the Layers panel; default true).' },\n blend: { type: 'string', enum: FILTER_BLENDS, description: 'How the filter’s result blends with the result below it (Blending Options; default normal).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Strength of the filter’s result over the result below it, 0..1 (default 1).' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 blue to yellow (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n} as const;\n\nfunction settingsFrom(args: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(FILTER_SETTINGS)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n}\n\n/** A PNG (data URL or base64) as a data URL. */\nfunction imageArg(image: unknown): string {\n if (typeof image !== 'string' || !image.trim()) throw new LayerOperationError('image is required: a PNG as a data URL or base64 (other formats need a browser).');\n return image.trim();\n}\n\n/**\n * Agent tools for smart objects in the image editor: `get_smart_object`, `convert_to_smart_object`,\n * `rasterize_smart_object`, `set_smart_object_transform`, `replace_smart_object_contents`,\n * `export_smart_object_contents`, `copy_smart_object`, `edit_smart_object_contents`,\n * `close_smart_object_contents`, and the smart filter stack: `add_smart_filter`,\n * `update_smart_filter`, `remove_smart_filter`, `move_smart_filter`, `set_smart_filters_enabled`,\n * `set_smart_filter_mask`. Every write is one undo step, validates first and returns what changed.\n */\nexport function createImageSmartObjectTools(controller: ImageEditorController): AgentTool[] {\n const smartId = (id: unknown) => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError('layerId is required (a smart object id from list_layers).');\n requireSmartObject(controller.getLayers(), id);\n return id;\n };\n const info = (id: string) => {\n const i = controller.getSmartObject(id);\n return {\n ...i,\n placement: { ...i.placement, corners: i.placement.corners.map(roundPoint), center: roundPoint(i.placement.center), scaleX: round(i.placement.scaleX), scaleY: round(i.placement.scaleY), rotate: round(i.placement.rotate), skewX: round(i.placement.skewX) },\n };\n };\n const layerSummary = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n return loc ? { ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)), parentId: loc.parent?.id ?? null, index: loc.index } : null;\n };\n\n const getSmart: AgentTool = {\n name: 'get_smart_object',\n title: 'Read a smart object',\n description:\n 'Returns a smart object’s contents (id shared by linked instances, name, kind raster or document, pixel size, the file type it came from, the instances placed from it), its placement (corners in document pixels, warp mesh in contents pixels, interpolation, and scale / rotate / skew in percent and degrees of the contents’ own size when affine), its smart filters bottom to top (ids, kind, settings, enabled, blend, opacity; kind psd marks a Photoshop filter kept but not drawn) with the filter mask, and the rendered bounds. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Read a smart object', ...READ },\n handler: ({ layerId }) => info(smartId(layerId)),\n };\n\n const convert: AgentTool = {\n name: 'convert_to_smart_object',\n title: 'Convert to Smart Object',\n description:\n 'Layer › Smart Objects › Convert to Smart Object: sibling layers become one smart object at the topmost one’s position, so later transforms and filters are non-destructive. One pixel layer keeps its id, name, opacity, blend, fill, mask and effects, and its filters become smart filters (it looks exactly the same); several layers, a group or an adjustment become a nested layer document (edit it with edit_smart_object_contents). One undo step. Returns the new layer and its contents.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Sibling layer ids from list_layers (default the active layer).' },\n name: { type: 'string', description: 'Name of the smart object (default the top layer’s name).' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'Convert to Smart Object', ...WRITE },\n handler: ({ layerIds, name }) => {\n for (const id of (layerIds as string[] | undefined) ?? []) requireLayer(controller.getLayers(), id);\n const id = controller.convertToSmartObject(layerIds as string[] | undefined, { name: name as string | undefined });\n return { layer: layerSummary(id), smartObject: info(id) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_smart_object',\n title: 'Rasterize a smart object',\n description:\n 'Layer › Smart Objects › Rasterize: the smart object becomes a pixel layer holding what it shows now (smart filters baked; same id, mask, effects, opacity, blend and fill). The contents, placement and filters are gone, so later transforms resample the pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a smart object', ...DESTRUCTIVE },\n handler: ({ layerId }) => ({ layer: layerSummary(controller.rasterizeSmartObject(smartId(layerId))) }),\n };\n\n const setTransform: AgentTool = {\n name: 'set_smart_object_transform',\n title: 'Set a smart object’s transform',\n description:\n 'Sets where a smart object’s contents land, absolutely (not relative to the current transform), and renders it again from the contents, so nothing degrades however often it is transformed. Give at most one of: corners (Distort / Perspective), matrix (contents pixels → document), or the numbers scaleX / scaleY (percent of the contents’ own size), rotate, skewX, skewY and x, y (where the contents’ centre goes; unset numbers keep their current values). warp sets a preset or a mesh in contents pixels (null removes it); with nothing else given the corners stay. A linked mask follows a warp-free change. For a relative nudge use transform_layer. One undo step. Returns the placement and bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n corners: { type: 'array', items: POINT, minItems: 4, maxItems: 4, description: 'Where the contents’ corners land: [top-left, top-right, bottom-right, bottom-left] in document pixels. The quad must stay convex.' },\n matrix: { type: 'array', items: { type: 'number' }, minItems: 6, maxItems: 6, description: \"Affine map from contents pixels to document pixels [a, b, c, d, e, f] (x' = a·x + c·y + e, y' = b·x + d·y + f).\" },\n scaleX: { type: 'number', description: 'Width in percent of the contents’ own width (negative flips).' },\n scaleY: { type: 'number', description: 'Height in percent of the contents’ own height (negative flips).' },\n rotate: { type: 'number', description: 'Rotation in degrees, clockwise.' },\n skewX: { type: 'number', minimum: -89, maximum: 89, description: 'Horizontal skew in degrees.' },\n skewY: { type: 'number', minimum: -89, maximum: 89, description: 'Vertical skew in degrees.' },\n x: { type: 'number', description: 'Document x of the contents’ centre (default where it is now).' },\n y: { type: 'number', description: 'Document y of the contents’ centre (default where it is now).' },\n warp: {\n description: 'A warp over the contents: a preset {style, bend, horizontal, vertical, orientation}, a mesh {columns, rows, points} in contents pixels, or null to remove the warp. Omit to keep the current warp.',\n anyOf: [\n { type: 'null' },\n {\n type: 'object',\n properties: {\n style: { type: 'string', enum: WARP_STYLES, description: 'Preset (Photoshop’s menu).' },\n bend: { type: 'number', minimum: -100, maximum: 100, description: 'Bend in percent (default 50).' },\n horizontal: { type: 'number', minimum: -100, maximum: 100, description: 'Horizontal distortion in percent (default 0).' },\n vertical: { type: 'number', minimum: -100, maximum: 100, description: 'Vertical distortion in percent (default 0).' },\n orientation: { type: 'string', enum: ['horizontal', 'vertical'], description: 'Bend axis (default horizontal).' },\n },\n required: ['style'],\n additionalProperties: false,\n },\n {\n type: 'object',\n properties: {\n columns: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch columns.' },\n rows: { type: 'integer', minimum: 1, maximum: 16, description: 'Patch rows.' },\n points: { type: 'array', items: POINT, minItems: 16, description: 'The (3·columns + 1) × (3·rows + 1) control points row by row, in contents pixels (0..width × 0..height is no warp).' },\n },\n required: ['columns', 'rows', 'points'],\n additionalProperties: false,\n },\n ],\n },\n interpolation: { type: 'string', enum: INTERPOLATIONS, description: 'Resampling: nearest, bilinear or bicubic (default: as before, bicubic for new smart objects).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set a smart object’s transform', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = smartId(args.layerId);\n let warp: WarpMesh | { style: WarpStyle; bend: number; horizontal?: number; vertical?: number; orientation?: 'horizontal' | 'vertical' } | null | undefined;\n const w = args.warp as Record<string, unknown> | null | undefined;\n if (w === null) warp = null;\n else if (w && 'style' in w) warp = { style: w.style as WarpStyle, bend: (w.bend as number | undefined) ?? 50, horizontal: w.horizontal as number | undefined, vertical: w.vertical as number | undefined, orientation: w.orientation as 'horizontal' | 'vertical' | undefined };\n else if (w) {\n const m = w as { columns: number; rows: number; points: { x: number; y: number }[] };\n const want = (3 * m.columns + 1) * (3 * m.rows + 1);\n if (m.points.length !== want) throw new LayerOperationError(`A ${m.columns} × ${m.rows} mesh needs (3·${m.columns} + 1) × (3·${m.rows} + 1) = ${want} points, got ${m.points.length}.`);\n const knots = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n warp = { us: knots(m.columns), vs: knots(m.rows), points: m.points.map((p) => ({ x: p.x, y: p.y })) };\n }\n const keys = ['corners', 'matrix', 'scaleX', 'scaleY', 'rotate', 'skewX', 'skewY', 'x', 'y', 'warp', 'interpolation'];\n if (!keys.some((k) => args[k] !== undefined)) throw new LayerOperationError(`Give something to set: ${keys.join(', ')}.`);\n const res = controller.setSmartObjectTransform(id, {\n corners: args.corners as Quad | undefined,\n matrix: args.matrix as AffineMatrix | undefined,\n scaleX: args.scaleX as number | undefined,\n scaleY: args.scaleY as number | undefined,\n rotate: args.rotate as number | undefined,\n skewX: args.skewX as number | undefined,\n skewY: args.skewY as number | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n warp,\n interpolation: args.interpolation as Interpolation | undefined,\n });\n return { layerId: id, placement: info(id).placement, masksMoved: res.masksMoved, bounds: controller.getLayerBounds(id) };\n },\n };\n\n const replace: AgentTool = {\n name: 'replace_smart_object_contents',\n title: 'Replace a smart object’s contents',\n description:\n 'Layer › Smart Objects › Replace Contents: new raster contents from an image for the smart object and every linked instance (same contents id). Each instance keeps its scale and rotation about the contents’ centre and its smart filters, and renders again. One undo step. Returns the contents id, the new size and the layers that changed.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n image: { type: 'string', description: 'The new contents: a PNG as a data URL or base64 (other formats and URLs need a browser).' },\n name: { type: 'string', description: 'File name of the contents (default the old name with .png).' },\n },\n required: ['layerId', 'image'],\n additionalProperties: false,\n },\n annotations: { title: 'Replace a smart object’s contents', ...DESTRUCTIVE },\n handler: async ({ layerId, image, name }) => controller.replaceSmartObjectContentsFromImage(smartId(layerId), imageArg(image), name as string | undefined),\n };\n\n const exportContents: AgentTool = {\n name: 'export_smart_object_contents',\n title: 'Export a smart object’s contents',\n description:\n 'Layer › Smart Objects › Export Contents: the contents as a file, base64: the file they came from while unedited (a PSB, PNG, JPEG…), else a PNG for raster contents or a PSD / PSB of a nested document. Returns name, mimeType, byteLength and base64. Changes nothing.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Export a smart object’s contents', ...READ },\n handler: ({ layerId }) => {\n const f = controller.exportSmartObjectContents(smartId(layerId));\n return { name: f.name, mimeType: f.mimeType, byteLength: f.bytes.length, base64: bytesToBase64(f.bytes) };\n },\n };\n\n const copy: AgentTool = {\n name: 'copy_smart_object',\n title: 'Copy a smart object',\n description:\n 'Duplicates a smart object directly above it. linked true (Duplicate Layer) shares the contents, so editing or replacing them changes both; linked false (Layer › Smart Objects › New Smart Object via Copy) gives the copy its own contents. Placement, filters, mask and effects are copied either way. One undo step. Returns the copy.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n linked: { type: 'boolean', description: 'true shares the contents with the original; false (default) makes independent contents.' },\n name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Copy a smart object', ...WRITE },\n handler: ({ layerId, linked, name }) => {\n const id = controller.copySmartObject(smartId(layerId), linked === true, name as string | undefined);\n return { layer: layerSummary(id), contentsId: controller.getSmartObject(id).contents.id };\n },\n };\n\n const editContents: AgentTool = {\n name: 'edit_smart_object_contents',\n title: 'Edit a smart object’s contents',\n description:\n 'Layer › Smart Objects › Edit Contents: opens the contents as a document of their own (a layer document, or raster contents as one pixel layer \"Layer 0\") with their own undo history. Every other tool (list_layers, paint, adjustments…) then works inside the contents until close_smart_object_contents saves them back. Contents can nest. Returns the contents’ size and layers.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Edit a smart object’s contents', ...WRITE },\n handler: ({ layerId }) => {\n const res = controller.editSmartObjectContents(smartId(layerId));\n const s = controller.getState();\n return { ...res, editing: controller.getEditingContents(), layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))) };\n },\n };\n\n const closeContents: AgentTool = {\n name: 'close_smart_object_contents',\n title: 'Close a smart object’s contents',\n description:\n 'Closes the contents opened with edit_smart_object_contents. save true (default) writes the edits back: the smart object and every linked instance render again, as one undo step in the document; save false drops them. Returns the smart object, whether it changed, the layers that changed and what is still open.',\n inputSchema: {\n type: 'object',\n properties: { save: { type: 'boolean', description: 'true (default) saves the edits into the smart object; false discards them.' } },\n additionalProperties: false,\n },\n annotations: { title: 'Close a smart object’s contents', ...WRITE },\n handler: ({ save }) => ({ ...controller.closeSmartObjectContents(save !== false), editing: controller.getEditingContents() }),\n };\n\n const addFilter: AgentTool = {\n name: 'add_smart_filter',\n title: 'Add a smart filter',\n description:\n 'Adds a smart filter to a smart object: develop (brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px), with its own blend mode and opacity over the result below it. Filters run bottom to top on the placed contents, then the filter mask mixes in the unfiltered contents. Unlike layer filters, a kind can repeat. One undo step. Returns the filter (with its id) and the stack.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' },\n index: { type: 'integer', minimum: 0, description: 'Position in the stack, 0 = bottom, applied first (default the top).' },\n ...FILTER_SETTINGS,\n },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a smart filter', ...WRITE },\n handler: (args) => {\n const id = smartId(args.layerId);\n const filter = controller.addSmartFilter(id, { kind: args.kind as string, ...settingsFrom(args) }, args.index as number | undefined);\n return { layerId: id, filter, smartFilters: info(id).smartFilters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_smart_filter',\n title: 'Change a smart filter',\n description:\n 'Changes one smart filter: its settings, enabled, blend or opacity (only the given values; the kind cannot change). A Photoshop filter kept from a PSD (kind psd) only takes enabled, blend and opacity. One undo step. Returns the filter and the stack.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: FILTER_ID, ...FILTER_SETTINGS },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a smart filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = smartId(args.layerId);\n const changes = settingsFrom(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n const filter = controller.updateSmartFilter(id, args.filterId as string, changes);\n return { layerId: id, filter, smartFilters: info(id).smartFilters };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_smart_filter',\n title: 'Remove a smart filter',\n description: 'Removes one smart filter (the filter mask goes with the last one). One undo step. Returns the remaining stack.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID, filterId: FILTER_ID }, required: ['layerId', 'filterId'], additionalProperties: false },\n annotations: { title: 'Remove a smart filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = smartId(layerId);\n controller.removeSmartFilter(id, filterId as string);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const moveFilter: AgentTool = {\n name: 'move_smart_filter',\n title: 'Reorder a smart filter',\n description: 'Moves one smart filter to a position in the stack (0 = bottom, applied first), as dragging it in the Layers panel. One undo step. Returns the stack in its new order.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: FILTER_ID, index: { type: 'integer', minimum: 0, description: 'New position, 0 = bottom (applied first).' } },\n required: ['layerId', 'filterId', 'index'],\n additionalProperties: false,\n },\n annotations: { title: 'Reorder a smart filter', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, filterId, index }) => {\n const id = smartId(layerId);\n controller.moveSmartFilter(id, filterId as string, index as number);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const setEnabled: AgentTool = {\n name: 'set_smart_filters_enabled',\n title: 'Turn smart filters on or off',\n description: 'Layer › Smart Filter › Disable / Enable Smart Filters: switches the whole stack; each filter keeps its own enabled flag. One undo step. Returns the stack.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, enabled: { type: 'boolean', description: 'false hides every smart filter; true shows them again.' } },\n required: ['layerId', 'enabled'],\n additionalProperties: false,\n },\n annotations: { title: 'Turn smart filters on or off', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, enabled }) => {\n const id = smartId(layerId);\n controller.setSmartFiltersEnabled(id, enabled === true);\n return { layerId: id, smartFilters: info(id).smartFilters };\n },\n };\n\n const setMask: AgentTool = {\n name: 'set_smart_filter_mask',\n title: 'Set the smart filter mask',\n description:\n 'Sets the one filter mask of a smart object’s smart filters, which says where the filtered result shows over the unfiltered contents: from reveal-all (white), hide-all (black), selection (the selection’s coverage) or none (remove it); keep changes only its properties. Paint it with paint_stroke channel filter-mask. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection', 'none', 'keep'], description: 'What the mask starts from; none removes it; keep only changes the properties below.' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'How much hidden areas hide, 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Gaussian softening in px (default 0).' },\n enabled: { type: 'boolean', description: 'false keeps the mask but ignores it.' },\n linked: { type: 'boolean', description: 'false keeps the mask in place when the smart object is transformed or moved (default linked).' },\n },\n required: ['layerId', 'from'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the smart filter mask', ...WRITE },\n handler: (args) => {\n const id = smartId(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['density', 'feather', 'enabled', 'linked']) if (args[k] !== undefined) props[k] = args[k];\n const mask = controller.setSmartFilterMask(id, args.from as 'reveal-all' | 'hide-all' | 'selection' | 'none' | 'keep', props);\n return { layerId: id, filterMask: maskSummary(mask ?? undefined) };\n },\n };\n\n return [getSmart, convert, rasterize, setTransform, replace, exportContents, copy, editContents, closeContents, addFilter, updateFilter, removeFilter, moveFilter, setEnabled, setMask];\n}\n","import type { AgentTool, AgentToolProvider } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind, ImageAdjustments } from './types';\nimport { normalizeProcessVersion } from './color';\nimport {\n ADJUSTMENT_KINDS,\n isAdjustmentKind,\n} from './adjustment-layers';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport {\n ADJUSTMENT_LAYER_CONTROLS,\n ADJUSTMENT_LAYER_OPACITY_CONTROL,\n IMAGE_ADJUSTMENT_CONTROLS,\n clampControlValue,\n findAdjustmentControl,\n findLayerControl,\n readAdjustmentControls,\n setControlPath,\n} from './controls';\nimport { createImageSelectionTools } from './selection-tools';\nimport { createImageCaptureSurface } from './capture';\nimport { createCaptureTool } from '../tools/capture.js';\nimport { createImageEditTools } from './edit-tools';\nimport { createImageLayerTools } from './layer-tools';\nimport { createImageEffectTools } from './effect-tools';\nimport { createImagePaintTools } from './paint-tools';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { createImagePsdTools } from './psd-tools';\nimport { createImageTransformTools } from './transform-tools';\nimport { createImageSmartObjectTools } from './smart-tools';\n\nexport interface ImageEditorToolProviderOptions {\n /** Provider id (unique per registry). Default `image-editor`. */\n id?: string;\n /** Always prefix tool names with `<namespace>__`. */\n namespace?: string;\n}\n\nconst LAYER_PATH = /^layers\\.([^.]+)\\.(.+)$/;\n\nfunction adjustmentsSummary(adjustments: ImageAdjustments) {\n return {\n process: normalizeProcessVersion(adjustments.process),\n values: readAdjustmentControls(adjustments),\n layers: (adjustments.layers ?? []).map((l) => JSON.parse(JSON.stringify(l))),\n };\n}\n\n/**\n * Agent tools for an image editor, for a `ToolRegistry`:\n *\n * - `get_adjustments`: base slider values (by control id), process version and adjustment layers.\n * - `list_controls`: the control table (ids, ranges, defaults, steps), layer kinds and blend modes.\n * - `set_adjustments {values, label}`: sets base controls (`exposure`) and layer parameters\n * (`layers.<id>.vibrance`, `layers.<id>.opacity`) in one undo step; values are clamped to the\n * control ranges and the applied values are echoed.\n * - `add_adjustment_layer {kind, params?, opacity?, blend?, name?}`.\n * - `render_preview {maxSide}`: the rendered image as a PNG, longest side at most `maxSide`.\n * - `capture_view {region, rect?, ...}`: a screenshot of the whole image, the selection or a rect in\n * natural pixels (up to 8× for close inspection), optionally with the selection shaded.\n * - Selections and masks: `get_selection`, `select_region`, `select_by_color`, `modify_selection`,\n * `refine_selection`, `apply_selection_as_mask` (see `createImageSelectionTools`).\n * - Layers, geometry and history: `get_image_info`, `get_histogram`, `update_adjustment_layer`,\n * `remove_adjustment_layer`, `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`,\n * `flip_image`, `undo_image`, `redo_image` (see `createImageEditTools`).\n * - Free Transform: `get_transform_frame`, `transform_layer`, `warp_layer`, `transform_selection`,\n * `flip_layer`, `rotate_layer_90` (see `createImageTransformTools`).\n * - Smart objects and smart filters: `get_smart_object`, `convert_to_smart_object`,\n * `rasterize_smart_object`, `set_smart_object_transform`, `replace_smart_object_contents`,\n * `export_smart_object_contents`, `copy_smart_object`, `edit_smart_object_contents`,\n * `close_smart_object_contents`, `add_smart_filter`, `update_smart_filter`, `remove_smart_filter`,\n * `move_smart_filter`, `set_smart_filters_enabled`, `set_smart_filter_mask` (see\n * `createImageSmartObjectTools`).\n */\nexport function createImageEditorToolProvider(\n controller: ImageEditorController,\n options: ImageEditorToolProviderOptions = {}\n): AgentToolProvider {\n const getAdjustments: AgentTool = {\n name: 'get_adjustments',\n title: 'Read image adjustments',\n description:\n 'Returns the image editor adjustment values by control id, the rendering process version and the adjustment layers (bottom to top).',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'Read image adjustments',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => adjustmentsSummary(controller.getState().adjustments),\n };\n\n const listControls: AgentTool = {\n name: 'list_controls',\n title: 'List adjustment controls',\n description:\n 'Lists every adjustment control (id, label, section, min, max, step, default), the parameters of each adjustment-layer kind, and the blend modes.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'List adjustment controls',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => ({\n controls: IMAGE_ADJUSTMENT_CONTROLS.map(({ id, label, section, min, max, step, default: def, requiresProcess }) => ({\n id, label, section, min, max, step, default: def, ...(requiresProcess ? { requiresProcess } : {}),\n })),\n layerKinds: ADJUSTMENT_KINDS.map(({ kind, label }) => ({\n kind,\n label,\n controls: [...ADJUSTMENT_LAYER_CONTROLS[kind], ADJUSTMENT_LAYER_OPACITY_CONTROL].map(({ id, label: l, min, max, step, default: def }) => ({\n id, label: l, min, max, step, default: def,\n })),\n })),\n blendModes: BLEND_MODES.map((m) => m.id),\n layerPathFormat: 'layers.<layerId>.<controlId>',\n }),\n };\n\n const setAdjustments: AgentTool = {\n name: 'set_adjustments',\n title: 'Set image adjustments',\n description:\n 'Sets adjustment values by control id in one undoable step. Base controls use their id (e.g. \"exposure\"); layer parameters use \"layers.<layerId>.<controlId>\" (e.g. \"layers.adj_1.vibrance\"). Values are clamped to each control range; the applied values are returned.',\n inputSchema: {\n type: 'object',\n properties: {\n values: {\n type: 'object',\n description: 'Map of control id to number. See list_controls for ids and ranges.',\n additionalProperties: { type: 'number' },\n },\n label: { type: 'string', description: 'Short description of the change (echoed back).' },\n },\n required: ['values'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Set image adjustments',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: ({ values, label }) => {\n const entries = Object.entries((values ?? {}) as Record<string, unknown>);\n if (!entries.length) throw new Error('No values given. Pass {\"values\": {\"<control id>\": number}}.');\n const state = controller.getState();\n const base: Partial<ImageAdjustments> = {};\n const layers = (state.adjustments.layers ?? []).map((l) => ({ ...l }));\n const applied: Record<string, number> = {};\n const clamped: string[] = [];\n const unknown: string[] = [];\n for (const [id, raw] of entries) {\n const m = LAYER_PATH.exec(id);\n if (m) {\n const layer = layers.find((l) => l.id === m[1]);\n const spec = layer ? findLayerControl(layer.kind, m[2]) : undefined;\n if (!layer || !spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n if (spec.id === 'opacity') layer.opacity = v;\n else layer.params = setControlPath(layer.params, spec.id, v) as typeof layer.params;\n applied[id] = v;\n continue;\n }\n const spec = findAdjustmentControl(id);\n if (!spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n (base as Record<string, number>)[id] = v;\n applied[id] = v;\n }\n if (unknown.length) {\n const accepted = IMAGE_ADJUSTMENT_CONTROLS.map((c) => c.id).join(', ');\n throw new Error(\n `Unknown control id(s): ${unknown.join(', ')}. Accepted: ${accepted}, or layers.<layerId>.<controlId> for an existing layer.`\n );\n }\n const changedLayers = Object.keys(applied).some((id) => id.startsWith('layers.'));\n controller.setAdjustments(changedLayers ? { ...base, layers } : base);\n const result: Record<string, unknown> = { applied, clamped };\n if (typeof label === 'string' && label) result.label = label;\n const notes: string[] = [];\n if ('tint' in applied && normalizeProcessVersion(controller.getState().adjustments.process) < 2) {\n notes.push('Tint has no effect on process 1 edits.');\n }\n if (notes.length) result.notes = notes;\n return result;\n },\n };\n\n const addLayer: AgentTool = {\n name: 'add_adjustment_layer',\n title: 'Add an adjustment layer',\n description:\n 'Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: ' +\n ADJUSTMENT_KINDS.map((k) => k.kind).join(', ') +\n '. Omitted params are neutral.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Adjustment kind.' },\n params: { type: 'object', description: 'Initial parameters (see list_controls); merged with neutral defaults.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' },\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (from list_layers). Default: above the other adjustment layers, below annotations.' },\n parentId: { type: 'string', description: 'Put the layer at the top of this group (from list_layers).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Add an adjustment layer',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {\n if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind \"${kind}\".`);\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n if (aboveId !== undefined && parentId !== undefined) throw new Error('Give aboveId or parentId, not both.');\n for (const ref of [aboveId, parentId]) {\n if (ref !== undefined && !controller.getLayer(ref as string)) throw new Error(`Unknown layer \"${ref}\". Call list_layers for ids.`);\n }\n if (parentId !== undefined && controller.getLayer(parentId as string)?.type !== 'group') throw new Error(`Layer \"${parentId}\" is not a group.`);\n const id = controller.addAdjustmentLayer({ kind: kind as AdjustmentKind, params, opacity, blend, name, aboveId, parentId });\n const layer = controller.getAdjustmentLayer(id)!;\n return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };\n },\n };\n\n const renderPreview: AgentTool = {\n name: 'render_preview',\n title: 'Render a preview',\n description:\n 'Renders the edited image (crop, adjustments, layers, annotations) as a PNG whose longest side is at most maxSide pixels. With layerId, renders only that layer (a group with its children) through the same crop and rotation.',\n inputSchema: {\n type: 'object',\n properties: {\n maxSide: { type: 'integer', minimum: 16, maximum: 2048, description: 'Longest side in pixels (default 512).' },\n layerId: { type: 'string', description: 'Render only this layer (from list_layers).' },\n },\n additionalProperties: false,\n },\n annotations: {\n title: 'Render a preview',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: async ({ maxSide, layerId }) => {\n if (layerId !== undefined && !controller.getLayer(layerId as string)) throw new Error(`Unknown layer \"${layerId}\". Call list_layers for ids.`);\n const side = Math.max(16, Math.min(2048, Math.round(typeof maxSide === 'number' ? maxSide : 512)));\n const state = controller.getState();\n const crop = state.crop ?? { x: 0, y: 0, ...state.imageDimensions };\n const quarter = Math.abs(Math.round((state.transform.rotate % 180) / 90)) % 2 === 1;\n const w = quarter ? crop.height : crop.width;\n const h = quarter ? crop.width : crop.height;\n const k = Math.min(1, side / Math.max(w, h, 1));\n const dataUrl = await controller.toDataURL({\n format: 'image/png',\n width: Math.max(1, Math.round(w * k)),\n height: Math.max(1, Math.round(h * k)),\n ...(layerId !== undefined ? { layerIds: [layerId as string] } : {}),\n });\n const comma = dataUrl.indexOf(',');\n if (!dataUrl.startsWith('data:image/png') || comma < 0 || comma === dataUrl.length - 1) {\n throw new Error('Rendering is not available in this environment.');\n }\n return {\n content: [\n { type: 'image', data: dataUrl.slice(comma + 1), mimeType: 'image/png' },\n { type: 'text', text: `Preview ${Math.round(w * k)}×${Math.round(h * k)}` },\n ],\n };\n },\n };\n\n return {\n id: options.id ?? 'image-editor',\n namespace: options.namespace,\n tools: [\n getAdjustments,\n listControls,\n setAdjustments,\n addLayer,\n renderPreview,\n createCaptureTool(createImageCaptureSurface(controller), { subject: 'the image (source orientation, before crop and rotation)', defaultPadding: 0 }),\n ...createImageSelectionTools(controller),\n ...createImageEditTools(controller),\n ...createImageLayerTools(controller),\n ...createImageTransformTools(controller),\n ...createImageSmartObjectTools(controller),\n ...createImageEffectTools(controller),\n ...createImagePaintTools(controller),\n ...createPigmentTools(),\n ...createImagePsdTools(controller),\n ],\n };\n}\n\n","import type { CurvePoint } from './types';\nimport { curveLutFromLevels } from './adjustment-layers';\n\n/**\n * Framework-neutral helpers for a Curves editor drawn in a `width` × `height` box (levels 0..255\n * on both axes, output up). The React and Svelte curve editors share them.\n */\n\nconst clampLevel = (v: number) => Math.min(255, Math.max(0, Math.round(v)));\n\n/** Box coordinates of a curve point. */\nexport function curvePointToBox(p: CurvePoint, width: number, height: number): { x: number; y: number } {\n return { x: (p.input / 255) * width, y: height - (p.output / 255) * height };\n}\n\n/** The curve point (whole levels, clamped) under box coordinates. */\nexport function boxToCurvePoint(x: number, y: number, width: number, height: number): CurvePoint {\n return { input: clampLevel((x / (width || 1)) * 255), output: clampLevel(((height - y) / (height || 1)) * 255) };\n}\n\n/** An SVG polyline path of the evaluated curve (spline, clamped, constant beyond the end points). */\nexport function curveSvgPath(points: readonly CurvePoint[], width: number, height: number, samples = 64): string {\n const lut = curveLutFromLevels(points);\n const n = lut.length - 1;\n let d = '';\n for (let i = 0; i <= samples; i++) {\n const t = i / samples;\n const y = lut[Math.round(t * n)];\n d += `${i === 0 ? 'M' : ' L'}${round2(t * width)} ${round2(height - y * height)}`;\n }\n return d;\n}\n\n/** Index of the point within `radius` box units of (x, y), nearest first; -1 for none. */\nexport function hitTestCurvePoint(\n points: readonly CurvePoint[],\n x: number,\n y: number,\n width: number,\n height: number,\n radius = 8\n): number {\n let best = -1;\n let bestD = radius * radius;\n points.forEach((p, i) => {\n const b = curvePointToBox(p, width, height);\n const d = (b.x - x) ** 2 + (b.y - y) ** 2;\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n });\n return best;\n}\n\n/** Inserts a point in input order (replacing one at the same input). Returns the new list and its index. */\nexport function insertCurvePoint(points: readonly CurvePoint[], p: CurvePoint): { points: CurvePoint[]; index: number } {\n const point = { input: clampLevel(p.input), output: clampLevel(p.output) };\n const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));\n let index = out.findIndex((q) => q.input > point.input);\n if (index === -1) index = out.length;\n out.splice(index, 0, point);\n return { points: out, index };\n}\n\n/** Moves point `index`, keeping its input strictly between its neighbours'. */\nexport function moveCurvePoint(points: readonly CurvePoint[], index: number, p: CurvePoint): CurvePoint[] {\n const out = points.map((q) => ({ ...q }));\n if (index < 0 || index >= out.length) return out;\n const lo = index > 0 ? out[index - 1].input + 1 : 0;\n const hi = index < out.length - 1 ? out[index + 1].input - 1 : 255;\n out[index] = { input: Math.min(hi, Math.max(lo, clampLevel(p.input))), output: clampLevel(p.output) };\n return out;\n}\n\n/** Removes point `index`; a curve keeps at least two points. */\nexport function removeCurvePoint(points: readonly CurvePoint[], index: number): CurvePoint[] {\n if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q) => ({ ...q }));\n return points.filter((_, i) => i !== index).map((q) => ({ ...q }));\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { ImagePoint } from '../types';\nimport type { ImageEditorController } from '../controller';\nimport { ellipsePoints, normalizeRect } from './shapes';\nimport { selectionModeFromModifiers, type SelectionToolOptions } from './tool-options';\n\nexport interface GestureModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n}\n\n/**\n * Pointer gestures of the selection tools, shared by the React and Svelte image editors: feed\n * it pointer positions in natural image pixels (`canvasToImagePoint(..., false)`).\n *\n * - Magic wand: selects on `begin` (async, as it may read pixels).\n * - Rectangle / ellipse: drag a box; a click without a drag deselects (replace mode).\n * - Lasso: drag a free-hand path, closed on `end`.\n *\n * Shift adds, Alt subtracts, both intersect (overriding the options bar mode). While dragging,\n * {@link draftPath} is the outline to show (`RenderOptions.draftSelectionPath`).\n */\nexport class SelectionGesture {\n private start: ImagePoint | null = null;\n private points: ImagePoint[] = [];\n private current: ImagePoint | null = null;\n private modifiers: GestureModifiers = {};\n /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */\n public error: string | null = null;\n\n constructor(\n readonly controller: ImageEditorController,\n private readonly getOptions: () => SelectionToolOptions\n ) {}\n\n get active(): boolean {\n return this.start !== null;\n }\n\n /** The outline of the shape being drawn, or `null`. */\n get draftPath(): ImagePoint[] | null {\n if (!this.start || !this.current) return null;\n const { tool } = this.getOptions();\n if (tool === 'lasso') return this.points.length > 1 ? this.points.slice() : null;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current.x - this.start.x,\n height: this.current.y - this.start.y,\n });\n if (rect.width < 1 && rect.height < 1) return null;\n if (tool === 'ellipse') return ellipsePoints(rect);\n return [\n { x: rect.x, y: rect.y },\n { x: rect.x + rect.width, y: rect.y },\n { x: rect.x + rect.width, y: rect.y + rect.height },\n { x: rect.x, y: rect.y + rect.height },\n ];\n }\n\n /** Pointer down. Resolves once a wand click has been applied. */\n async begin(point: ImagePoint, modifiers: GestureModifiers = {}): Promise<void> {\n const options = this.getOptions();\n this.error = null;\n this.modifiers = modifiers;\n if (options.tool === 'wand') {\n this.start = null;\n try {\n await this.controller.magicWand(point, {\n tolerance: options.tolerance,\n contiguous: options.contiguous,\n antiAlias: options.antiAlias,\n distance: options.distance,\n sampleMerged: options.sampleMerged,\n sample: options.sample,\n mode: selectionModeFromModifiers(modifiers, options.mode),\n });\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n return;\n }\n this.start = point;\n this.current = point;\n this.points = [point];\n }\n\n /** Pointer move; returns true when the draft changed (re-render). */\n move(point: ImagePoint): boolean {\n if (!this.start) return false;\n this.current = point;\n if (this.getOptions().tool === 'lasso') {\n const last = this.points[this.points.length - 1];\n if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;\n this.points.push(point);\n }\n return true;\n }\n\n /** Pointer up: commits the shape. Returns true when the selection changed. */\n end(point?: ImagePoint, modifiers?: GestureModifiers): boolean {\n if (!this.start) return false;\n if (point) this.move(point);\n const options = this.getOptions();\n const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);\n const path = this.draftPath;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current!.x - this.start.x,\n height: this.current!.y - this.start.y,\n });\n this.start = null;\n this.current = null;\n this.points = [];\n if (!path) return mode === 'replace' ? this.controller.deselect() : false;\n const opts = { mode, antiAlias: options.antiAlias };\n if (options.tool === 'lasso') return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;\n return options.tool === 'ellipse' ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);\n }\n\n /** Abandons a drag without changing the selection. */\n cancel(): void {\n this.start = null;\n this.current = null;\n this.points = [];\n }\n}\n","/**\n * Region-based mosaic (pixelation) and box-blur filters for redacting or anonymizing\n * rectangular bounding boxes on an ImageData-like buffer.\n */\n\nexport interface MosaicRegion {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\t/**\n\t * When true, coordinates are interpreted as normalized values:\n\t * - [0..1] when all values are <= 1\n\t * - [0..1000] when any value is > 1 (e.g. Gemini 0..1000 bounding boxes)\n\t */\n\tnormalized?: boolean;\n}\n\nexport interface MosaicImageDataLike {\n\tdata: Uint8ClampedArray;\n\twidth: number;\n\theight: number;\n}\n\n/**\n * Resolves a MosaicRegion to clamped integer pixel bounds [x0, y0, x1, y1] within the image dimensions.\n */\nexport function resolveMosaicRegionBounds(\n\tregion: MosaicRegion,\n\timgWidth: number,\n\timgHeight: number\n): { x0: number; y0: number; x1: number; y1: number } | null {\n\tif (imgWidth <= 0 || imgHeight <= 0 || region.width <= 0 || region.height <= 0) {\n\t\treturn null;\n\t}\n\n\tlet rx = region.x;\n\tlet ry = region.y;\n\tlet rw = region.width;\n\tlet rh = region.height;\n\n\tconst isZeroToOne =\n\t\trx >= 0 &&\n\t\try >= 0 &&\n\t\trw > 0 &&\n\t\trh > 0 &&\n\t\trx <= 1 &&\n\t\try <= 1 &&\n\t\trw <= 1 &&\n\t\trh <= 1 &&\n\t\t(region.normalized === true ||\n\t\t\t(region.normalized === undefined &&\n\t\t\t\t(imgWidth > 1 || imgHeight > 1) &&\n\t\t\t\t(!Number.isInteger(rx) || !Number.isInteger(ry) || !Number.isInteger(rw) || !Number.isInteger(rh))));\n\n\tif (isZeroToOne) {\n\t\trx = rx * imgWidth;\n\t\try = ry * imgHeight;\n\t\trw = rw * imgWidth;\n\t\trh = rh * imgHeight;\n\t} else if (region.normalized === true) {\n\t\t// 0..1000 normalized coordinates\n\t\trx = (rx / 1000) * imgWidth;\n\t\try = (ry / 1000) * imgHeight;\n\t\trw = (rw / 1000) * imgWidth;\n\t\trh = (rh / 1000) * imgHeight;\n\t}\n\n\tconst x0 = Math.max(0, Math.min(imgWidth, Math.floor(rx)));\n\tconst y0 = Math.max(0, Math.min(imgHeight, Math.floor(ry)));\n\tconst x1 = Math.max(0, Math.min(imgWidth, Math.ceil(rx + rw)));\n\tconst y1 = Math.max(0, Math.min(imgHeight, Math.ceil(ry + rh)));\n\n\tif (x1 <= x0 || y1 <= y0) return null;\n\treturn { x0, y0, x1, y1 };\n}\n\n/**\n * Pixelates each rectangular region by averaging `blockSize x blockSize` blocks.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionMosaic<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tblockSize: number = 12\n): T {\n\tconst { data, width, height } = imageData;\n\tconst step = Math.max(2, Math.round(blockSize));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\n\t\tfor (let by = y0; by < y1; by += step) {\n\t\t\tconst bh = Math.min(step, y1 - by);\n\t\t\tfor (let bx = x0; bx < x1; bx += step) {\n\t\t\t\tconst bw = Math.min(step, x1 - bx);\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tconst count = bw * bh;\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\trSum += data[idx];\n\t\t\t\t\t\tgSum += data[idx + 1];\n\t\t\t\t\t\tbSum += data[idx + 2];\n\t\t\t\t\t\taSum += data[idx + 3];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst rAvg = Math.round(rSum / count);\n\t\t\t\tconst gAvg = Math.round(gSum / count);\n\t\t\t\tconst bAvg = Math.round(bSum / count);\n\t\t\t\tconst aAvg = Math.round(aSum / count);\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\tdata[idx] = rAvg;\n\t\t\t\t\t\tdata[idx + 1] = gAvg;\n\t\t\t\t\t\tdata[idx + 2] = bAvg;\n\t\t\t\t\t\tdata[idx + 3] = aAvg;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n\n/**\n * Applies a separable 2-pass box blur to each rectangular region.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionBlur<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tradius: number = 10\n): T {\n\tconst { data, width, height } = imageData;\n\tconst rad = Math.max(1, Math.min(64, Math.round(radius)));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\t\tconst rw = x1 - x0;\n\t\tconst rh = y1 - y0;\n\t\tconst temp = new Uint8ClampedArray(rw * rh * 4);\n\n\t\t// Horizontal pass into temp\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst srcY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, x - rad);\n\t\t\t\tconst kEnd = Math.min(rw - 1, x + rad);\n\t\t\t\tfor (let kx = kStart; kx <= kEnd; kx++) {\n\t\t\t\t\tconst srcIdx = (srcY * width + (x0 + kx)) * 4;\n\t\t\t\t\trSum += data[srcIdx];\n\t\t\t\t\tgSum += data[srcIdx + 1];\n\t\t\t\t\tbSum += data[srcIdx + 2];\n\t\t\t\t\taSum += data[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (y * rw + x) * 4;\n\t\t\t\ttemp[dstIdx] = Math.round(rSum / count);\n\t\t\t\ttemp[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\ttemp[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\ttemp[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\n\t\t// Vertical pass from temp back into imageData.data\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst dstY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, y - rad);\n\t\t\t\tconst kEnd = Math.min(rh - 1, y + rad);\n\t\t\t\tfor (let ky = kStart; ky <= kEnd; ky++) {\n\t\t\t\t\tconst srcIdx = (ky * rw + x) * 4;\n\t\t\t\t\trSum += temp[srcIdx];\n\t\t\t\t\tgSum += temp[srcIdx + 1];\n\t\t\t\t\tbSum += temp[srcIdx + 2];\n\t\t\t\t\taSum += temp[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (dstY * width + (x0 + x)) * 4;\n\t\t\t\tdata[dstIdx] = Math.round(rSum / count);\n\t\t\t\tdata[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\tdata[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\tdata[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n","/**\n * The layers panel's view model, shared by the React and Svelte panels: the rows to show (top of\n * the stack first, groups indented, collapsed groups hiding their children), where a drop lands,\n * and the menu of layer operations with whether each one applies to the active layer.\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport type { ImageEditorController } from '../controller';\nimport { findLayer, type ImageLayer, type SmartFilter, type SmartObjectLayer } from './model';\nimport type { LayerPlacement } from './ops';\n\nexport interface LayerPanelRow {\n layer: ImageLayer;\n /** Nesting depth (0 = top level). */\n depth: number;\n parentId: string | null;\n /** Index among its siblings, 0 = bottom. */\n index: number;\n active: boolean;\n /** Display name (adjustment layers without a name show their kind). */\n label: string;\n /** Clipped to the layer below (drawn with an indent arrow). */\n clipped: boolean;\n}\n\n/** Rows top to bottom; children of collapsed groups are left out. */\nexport function layerPanelRows(layers: readonly ImageLayer[], activeId: string | null): LayerPanelRow[] {\n const rows: LayerPanelRow[] = [];\n const walk = (list: readonly ImageLayer[], depth: number, parentId: string | null) => {\n for (let i = list.length - 1; i >= 0; i--) {\n const layer = list[i];\n rows.push({\n layer,\n depth,\n parentId,\n index: i,\n active: layer.id === activeId,\n label: layerLabel(layer),\n clipped: Boolean(layer.clipped),\n });\n if (layer.type === 'group' && !layer.collapsed) walk(layer.children, depth + 1, layer.id);\n }\n };\n walk(layers, 0, null);\n return rows;\n}\n\nexport function layerLabel(layer: ImageLayer): string {\n if (layer.name) return layer.name;\n if (layer.type === 'adjustment') return adjustmentKindLabel(layer.kind);\n return layer.type === 'group' ? 'Group' : 'Layer';\n}\n\n/** A short type badge for rows without a thumbnail. */\nexport function layerBadge(layer: ImageLayer): string {\n switch (layer.type) {\n case 'pixel':\n return 'px';\n case 'adjustment':\n return 'adj';\n case 'group':\n return 'grp';\n case 'annotation':\n return layer.annotation.type === 'text' ? 'T' : 'ann';\n case 'smart':\n return 'so';\n }\n}\n\n/** Where a row was dropped on: the top or bottom part of the row, or the middle of a group row. */\nexport type DropZone = 'above' | 'below' | 'inside';\n\n/** The zone for a pointer at `fraction` (0 top .. 1 bottom) of a row's height. */\nexport function dropZone(fraction: number, isGroup: boolean): DropZone {\n if (isGroup && fraction > 0.3 && fraction < 0.7) return 'inside';\n return fraction < 0.5 ? 'above' : 'below';\n}\n\n/** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */\nexport function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null {\n if (dragId === targetId) return null;\n const target = findLayer(layers, targetId);\n if (!target) return null;\n if (zone === 'inside' && target.layer.type === 'group') return { parentId: targetId };\n // The panel lists the top first: dropping above a row means higher in the stack.\n return zone === 'above' ? { aboveId: targetId } : { belowId: targetId };\n}\n\nexport type LayerMenuAction =\n | 'new-layer'\n | 'new-group'\n | 'duplicate'\n | 'delete'\n | 'group-selected'\n | 'ungroup'\n | 'merge-down'\n | 'merge-visible'\n | 'flatten'\n | 'via-copy'\n | 'via-cut'\n | 'add-mask'\n | 'add-mask-selection'\n | 'apply-mask'\n | 'delete-mask'\n | 'toggle-mask'\n | 'toggle-mask-link'\n | 'toggle-clip'\n | 'rasterize'\n | 'convert-smart'\n | 'edit-contents'\n | 'smart-via-copy'\n | 'add-filter-mask';\n\nexport interface LayerMenuItem {\n action: LayerMenuAction;\n label: string;\n enabled: boolean;\n}\n\n/** The layer menu for the controller's current state (active layer, selection). */\nexport function layerMenu(controller: ImageEditorController): LayerMenuItem[] {\n const s = controller.getState();\n const active = controller.getActiveLayer();\n const loc = active ? findLayer(s.layers, active.id) : null;\n const siblings = loc ? (loc.parent ? loc.parent.children : s.layers) : [];\n const below = loc ? siblings[loc.index - 1] : undefined;\n const hasSel = controller.hasSelection;\n const isPixel = active?.type === 'pixel';\n return [\n { action: 'new-layer', label: 'New Layer', enabled: true },\n { action: 'new-group', label: 'New Group', enabled: true },\n { action: 'duplicate', label: 'Duplicate Layer', enabled: Boolean(active) },\n { action: 'delete', label: 'Delete Layer', enabled: Boolean(active) },\n { action: 'group-selected', label: 'Group Layer', enabled: Boolean(active) },\n { action: 'ungroup', label: 'Ungroup', enabled: active?.type === 'group' },\n { action: 'merge-down', label: 'Merge Down', enabled: Boolean(active?.visible && below?.type === 'pixel') },\n { action: 'merge-visible', label: 'Merge Visible', enabled: s.layers.some((l) => l.visible) },\n { action: 'flatten', label: 'Flatten Image', enabled: s.layers.length > 0 },\n { action: 'via-copy', label: 'Layer via Copy', enabled: isPixel && hasSel },\n { action: 'via-cut', label: 'Layer via Cut', enabled: isPixel && hasSel },\n { action: 'add-mask', label: 'Add Layer Mask', enabled: Boolean(active) },\n { action: 'add-mask-selection', label: 'Add Mask from Selection', enabled: Boolean(active) && hasSel },\n { action: 'apply-mask', label: 'Apply Layer Mask', enabled: isPixel && Boolean(active?.mask) },\n { action: 'delete-mask', label: 'Delete Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask', label: active?.mask?.enabled === false ? 'Enable Layer Mask' : 'Disable Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask-link', label: active?.mask?.linked === false ? 'Link Layer Mask' : 'Unlink Layer Mask', enabled: Boolean(active?.mask && !active.mask.source) },\n { action: 'rasterize', label: 'Rasterize Layer', enabled: active?.type === 'annotation' || active?.type === 'smart' || (isPixel && Boolean(active?.type === 'pixel' && active.filters?.length)) },\n { action: 'convert-smart', label: 'Convert to Smart Object', enabled: Boolean(active && active.type !== 'annotation') },\n { action: 'edit-contents', label: 'Edit Contents', enabled: active?.type === 'smart' },\n { action: 'smart-via-copy', label: 'New Smart Object via Copy', enabled: active?.type === 'smart' },\n { action: 'add-filter-mask', label: 'Add Smart Filter Mask', enabled: active?.type === 'smart' && !active.smartFilters?.mask },\n { action: 'toggle-clip', label: active?.clipped ? 'Release Clipping Mask' : 'Create Clipping Mask', enabled: Boolean(loc && loc.index > 0) },\n ];\n}\n\n/** Runs a menu action on the active layer. Resolves to the error message when it was refused. */\nexport async function runLayerMenuAction(controller: ImageEditorController, action: LayerMenuAction): Promise<string | null> {\n const active = controller.getActiveLayer();\n const id = active?.id;\n try {\n switch (action) {\n case 'new-layer':\n controller.addPixelLayer();\n break;\n case 'new-group':\n controller.addGroup();\n break;\n case 'duplicate':\n if (id) controller.duplicateLayer(id);\n break;\n case 'delete':\n if (id) controller.removeLayer(id);\n break;\n case 'group-selected':\n if (id) controller.groupLayers([id]);\n break;\n case 'ungroup':\n if (id) controller.ungroupLayer(id);\n break;\n case 'merge-down':\n if (id) await controller.mergeDown(id);\n break;\n case 'merge-visible':\n await controller.mergeVisible();\n break;\n case 'flatten':\n await controller.flattenImage();\n break;\n case 'via-copy':\n case 'via-cut':\n controller.layerViaCopy({ cut: action === 'via-cut' });\n break;\n case 'add-mask':\n if (id) controller.addLayerMask(id, 'reveal-all');\n break;\n case 'add-mask-selection':\n if (id) controller.addLayerMask(id, 'selection');\n break;\n case 'apply-mask':\n if (id) await controller.applyLayerMask(id);\n break;\n case 'delete-mask':\n if (id) controller.removeLayerMask(id);\n break;\n case 'toggle-mask':\n if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });\n break;\n case 'toggle-mask-link':\n if (id && active?.mask) controller.setLayerMaskProps(id, { linked: active.mask.linked === false });\n break;\n case 'rasterize':\n if (id) await controller.rasterizeLayer(id);\n break;\n case 'convert-smart':\n if (id) controller.convertToSmartObject([id]);\n break;\n case 'edit-contents':\n if (id) controller.editSmartObjectContents(id);\n break;\n case 'smart-via-copy':\n if (id) controller.copySmartObject(id, false);\n break;\n case 'add-filter-mask':\n if (id) controller.setSmartFilterMask(id, 'reveal-all');\n break;\n case 'toggle-clip':\n if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });\n break;\n }\n return null;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n}\n\n/** One row of a smart object's filter list (Photoshop shows them under the layer, top filter first). */\nexport interface SmartFilterRow {\n filter: SmartFilter;\n /** Display name: Develop, Blur (radius), or the Photoshop filter's name. */\n label: string;\n /** Index in the stack, 0 = bottom (applied first). */\n index: number;\n /** False for Photoshop filters we keep but do not draw. */\n drawn: boolean;\n}\n\n/** The filter rows of a smart object, top filter first (empty when it has none). */\nexport function smartFilterRows(layer: ImageLayer): SmartFilterRow[] {\n if (layer.type !== 'smart') return [];\n const list = (layer as SmartObjectLayer).smartFilters?.filters ?? [];\n const rows: SmartFilterRow[] = list.map((f, index) => ({\n filter: f,\n index,\n drawn: f.kind !== 'psd',\n label: f.kind === 'psd' ? f.name.replace(/&/g, '').replace(/\\.\\.\\.$/, '') : f.kind === 'blur' ? `Blur (${f.radius} px)` : 'Develop',\n }));\n return rows.reverse();\n}\n","/**\n * The layer effects section's view model, shared by the React and Svelte panels: whether a layer\n * can take effects, its effect rows, the menu of kinds to add, and per effect a flat list of\n * controls (sliders, colours, toggles, choices) with how a change maps to `updateLayerEffect`.\n */\n\nimport { BLEND_MODES } from '../blend';\nimport {\n defaultEffectGradient,\n hasActiveEffects,\n layerEffectLabel,\n LAYER_EFFECT_KINDS,\n type EffectFill,\n type LayerEffect,\n type LayerEffectKind,\n} from '../../raster/effects/model';\nimport { builtinPattern, builtinPatternIds, type EffectPattern } from '../../raster/effects/patterns';\nimport type { ImageLayer } from './model';\n\nexport interface LayerEffectRow {\n id: string;\n kind: LayerEffectKind;\n label: string;\n enabled: boolean;\n}\n\nexport interface LayerEffectsPanelModel {\n /** Whether the layer can carry effects (adjustment layers cannot; a fully locked layer refuses changes). */\n available: boolean;\n /** Why not, when unavailable or read-only. */\n reason?: string;\n readOnly: boolean;\n enabled: boolean;\n /** Scale Effects, 1 = 100 %. */\n scale: number;\n rows: LayerEffectRow[];\n}\n\n/** The effects section for `layer`. */\nexport function layerEffectsPanel(layer: ImageLayer | null | undefined): LayerEffectsPanelModel {\n const empty = { readOnly: true, enabled: true, scale: 1, rows: [] };\n if (!layer) return { ...empty, available: false, reason: 'Select a layer to add effects.' };\n if (layer.type === 'adjustment') return { ...empty, available: false, reason: 'Adjustment layers cannot have effects.' };\n const fx = layer.effects;\n const readOnly = Boolean(layer.locks?.all);\n return {\n available: true,\n readOnly,\n reason: readOnly ? 'The layer is locked; unlock it to change its effects.' : undefined,\n enabled: fx?.enabled ?? true,\n scale: fx?.scale ?? 1,\n rows: (fx?.items ?? []).map((e) => ({ id: e.id, kind: e.kind, label: layerEffectLabel(e.kind), enabled: e.enabled })),\n };\n}\n\n/** The fx badge of a layer row: `on` (effects render), `off` (effects but switched off), or `null`. */\nexport function layerEffectsBadge(layer: ImageLayer): 'on' | 'off' | null {\n if (!layer.effects?.items.length) return null;\n return hasActiveEffects(layer.effects) ? 'on' : 'off';\n}\n\n/** The kinds the \"Add effect\" menu offers, in the Layer Style dialog's order. */\nexport const LAYER_EFFECT_MENU: ReadonlyArray<{ kind: LayerEffectKind; label: string }> = LAYER_EFFECT_KINDS.map((kind) => ({ kind, label: layerEffectLabel(kind) }));\n\nexport type EffectControl =\n | { key: string; label: string; type: 'range'; min: number; max: number; step: number; value: number; /** Shown and edited as percent (value × 100). */ percent?: boolean; unit?: string }\n | { key: string; label: string; type: 'color'; value: string }\n | { key: string; label: string; type: 'checkbox'; value: boolean }\n | { key: string; label: string; type: 'select'; value: string; options: Array<{ value: string; label: string }> };\n\nconst BLENDS = BLEND_MODES.filter((m) => m.id !== 'pigment').map((m) => ({ value: m.id as string, label: m.label }));\nconst opt = (...pairs: Array<[string, string]>) => pairs.map(([value, label]) => ({ value, label }));\n\nfunction patternOptions(patterns: readonly EffectPattern[]): Array<{ value: string; label: string }> {\n return [...patterns.map((p) => ({ value: p.id, label: p.name || p.id })), ...builtinPatternIds().map((id) => ({ value: id, label: builtinPattern(id)!.name }))];\n}\n\ntype Rec = Record<string, unknown>;\n\n/** Reads a dotted path (`fill.color`, `gradient.stops.last.color`). */\nfunction getPath(o: unknown, path: string): unknown {\n let v: unknown = o;\n for (const p of path.split('.')) {\n if (v === null || v === undefined) return undefined;\n if (Array.isArray(v)) v = p === 'last' ? v[v.length - 1] : v[Number(p)];\n else v = (v as Rec)[p];\n }\n return v;\n}\n\n/** A copy of `o` with `path` set to `value` (arrays and objects copied along the path). */\nfunction setPath(o: unknown, parts: string[], value: unknown): unknown {\n if (!parts.length) return value;\n const [p, ...rest] = parts;\n if (Array.isArray(o)) {\n const i = p === 'last' ? o.length - 1 : Number(p);\n const out = [...o];\n out[i] = setPath(o[i], rest, value);\n return out;\n }\n const r = (o ?? {}) as Rec;\n return { ...r, [p]: setPath(r[p], rest, value) };\n}\n\nfunction fillControls(f: EffectFill, allowPattern: boolean, patterns: readonly EffectPattern[]): EffectControl[] {\n const types = allowPattern ? opt(['color', 'Color'], ['gradient', 'Gradient'], ['pattern', 'Pattern']) : opt(['color', 'Color'], ['gradient', 'Gradient']);\n const out: EffectControl[] = [{ key: 'fill.type', label: 'Fill', type: 'select', value: f.type, options: types }];\n if (f.type === 'color') out.push({ key: 'fill.color', label: 'Color', type: 'color', value: f.color });\n else if (f.type === 'gradient') out.push(...gradientControls('fill.gradient', f.gradient));\n else {\n out.push(\n { key: 'fill.patternId', label: 'Pattern', type: 'select', value: f.patternId, options: patternOptions(patterns) },\n { key: 'fill.scale', label: 'Scale', type: 'range', min: 0.01, max: 10, step: 0.01, value: f.scale, percent: true, unit: '%' }\n );\n }\n return out;\n}\n\nfunction gradientControls(prefix: string, g: { stops: Array<{ color: string }>; style: string; angle: number; scale: number; reverse: boolean; align: boolean }): EffectControl[] {\n return [\n { key: `${prefix}.stops.0.color`, label: 'Start colour', type: 'color', value: g.stops[0]?.color ?? '#000000' },\n { key: `${prefix}.stops.last.color`, label: 'End colour', type: 'color', value: g.stops[g.stops.length - 1]?.color ?? '#ffffff' },\n { key: `${prefix}.style`, label: 'Style', type: 'select', value: g.style, options: opt(['linear', 'Linear'], ['radial', 'Radial'], ['angle', 'Angle'], ['reflected', 'Reflected'], ['diamond', 'Diamond']) },\n { key: `${prefix}.angle`, label: 'Angle', type: 'range', min: -180, max: 180, step: 1, value: g.angle, unit: '°' },\n { key: `${prefix}.scale`, label: 'Scale', type: 'range', min: 0.1, max: 1.5, step: 0.01, value: g.scale, percent: true, unit: '%' },\n { key: `${prefix}.reverse`, label: 'Reverse', type: 'checkbox', value: g.reverse },\n { key: `${prefix}.align`, label: 'Align with layer', type: 'checkbox', value: g.align },\n ];\n}\n\nconst px = (key: string, label: string, value: number, max = 250): EffectControl => ({ key, label, type: 'range', min: 0, max, step: 1, value, unit: 'px' });\nconst pct = (key: string, label: string, value: number, min = 0, max = 1): EffectControl => ({ key, label, type: 'range', min, max, step: 0.01, value, percent: true, unit: '%' });\nconst deg = (key: string, label: string, value: number, min = -180, max = 180): EffectControl => ({ key, label, type: 'range', min, max, step: 1, value, unit: '°' });\n\n/**\n * The controls for one effect, in the Layer Style dialog's order. `patterns` are the document's\n * patterns (the built-in ones are always offered).\n */\nexport function layerEffectControls(e: LayerEffect, patterns: readonly EffectPattern[] = []): EffectControl[] {\n const blended: EffectControl[] =\n e.kind === 'bevel-emboss'\n ? []\n : [\n { key: 'blend', label: 'Blend mode', type: 'select', value: e.blend, options: BLENDS },\n pct('opacity', 'Opacity', e.opacity),\n ];\n switch (e.kind) {\n case 'drop-shadow':\n case 'inner-shadow':\n return [\n ...blended,\n { key: 'color', label: 'Color', type: 'color', value: e.color },\n { key: 'useGlobalLight', label: 'Use global light', type: 'checkbox', value: e.useGlobalLight },\n deg('angle', 'Angle', e.angle),\n px('distance', 'Distance', e.distance, 500),\n e.kind === 'drop-shadow' ? pct('spread', 'Spread', e.spread) : pct('choke', 'Choke', e.choke),\n px('size', 'Size', e.size),\n pct('noise', 'Noise', e.noise),\n ...(e.kind === 'drop-shadow' ? [{ key: 'knocksOut', label: 'Layer knocks out shadow', type: 'checkbox', value: e.knocksOut } as EffectControl] : []),\n ];\n case 'outer-glow':\n case 'inner-glow':\n return [\n ...blended,\n ...fillControls(e.fill, false, patterns),\n { key: 'technique', label: 'Technique', type: 'select', value: e.technique, options: opt(['softer', 'Softer'], ['precise', 'Precise']) },\n ...(e.kind === 'inner-glow' ? [{ key: 'source', label: 'Source', type: 'select', value: e.source, options: opt(['edge', 'Edge'], ['center', 'Center']) } as EffectControl] : []),\n e.kind === 'outer-glow' ? pct('spread', 'Spread', e.spread) : pct('choke', 'Choke', e.choke),\n px('size', 'Size', e.size),\n pct('range', 'Range', e.range, 0.01, 1),\n pct('noise', 'Noise', e.noise),\n ];\n case 'bevel-emboss':\n return [\n { key: 'style', label: 'Style', type: 'select', value: e.style, options: opt(['inner-bevel', 'Inner Bevel'], ['outer-bevel', 'Outer Bevel'], ['emboss', 'Emboss'], ['pillow-emboss', 'Pillow Emboss'], ['stroke-emboss', 'Stroke Emboss']) },\n { key: 'technique', label: 'Technique', type: 'select', value: e.technique, options: opt(['smooth', 'Smooth'], ['chisel-hard', 'Chisel Hard'], ['chisel-soft', 'Chisel Soft']) },\n pct('depth', 'Depth', e.depth, 0, 10),\n { key: 'direction', label: 'Direction', type: 'select', value: e.direction, options: opt(['up', 'Up'], ['down', 'Down']) },\n px('size', 'Size', e.size),\n px('soften', 'Soften', e.soften, 16),\n { key: 'useGlobalLight', label: 'Use global light', type: 'checkbox', value: e.useGlobalLight },\n deg('angle', 'Angle', e.angle),\n deg('altitude', 'Altitude', e.altitude, 0, 90),\n { key: 'highlightColor', label: 'Highlight', type: 'color', value: e.highlightColor },\n { key: 'highlightBlend', label: 'Highlight mode', type: 'select', value: e.highlightBlend, options: BLENDS },\n pct('highlightOpacity', 'Highlight opacity', e.highlightOpacity),\n { key: 'shadowColor', label: 'Shadow', type: 'color', value: e.shadowColor },\n { key: 'shadowBlend', label: 'Shadow mode', type: 'select', value: e.shadowBlend, options: BLENDS },\n pct('shadowOpacity', 'Shadow opacity', e.shadowOpacity),\n ];\n case 'satin':\n return [\n ...blended,\n { key: 'color', label: 'Color', type: 'color', value: e.color },\n deg('angle', 'Angle', e.angle),\n px('distance', 'Distance', e.distance),\n px('size', 'Size', e.size),\n { key: 'invert', label: 'Invert', type: 'checkbox', value: e.invert },\n ];\n case 'color-overlay':\n return [...blended, { key: 'color', label: 'Color', type: 'color', value: e.color }];\n case 'gradient-overlay':\n return [...blended, ...gradientControls('gradient', e.gradient)];\n case 'pattern-overlay':\n return [\n ...blended,\n { key: 'patternId', label: 'Pattern', type: 'select', value: e.patternId, options: patternOptions(patterns) },\n pct('scale', 'Scale', e.scale, 0.01, 10),\n deg('angle', 'Angle', e.angle ?? 0),\n { key: 'link', label: 'Link with layer', type: 'checkbox', value: e.link ?? true },\n ];\n case 'stroke':\n return [\n ...blended,\n px('size', 'Size', e.size),\n { key: 'position', label: 'Position', type: 'select', value: e.position, options: opt(['outside', 'Outside'], ['inside', 'Inside'], ['center', 'Center']) },\n ...fillControls(e.fill, true, patterns),\n ];\n }\n}\n\n/** A new fill of `type`, keeping the colour where it can. */\nfunction fillOfType(current: EffectFill, type: string, patterns: readonly EffectPattern[]): EffectFill {\n if (type === 'gradient') return { type: 'gradient', gradient: defaultEffectGradient() };\n if (type === 'pattern') return { type: 'pattern', patternId: patterns[0]?.id ?? 'checker', scale: 1 };\n return { type: 'color', color: current.type === 'color' ? current.color : '#000000' };\n}\n\n/**\n * The `updateLayerEffect` settings for setting control `key` of `effect` to `value` (a nested\n * object such as `fill` or `gradient` is replaced whole, with the one field changed). Values are\n * in the effect's own units (fractions for percent controls).\n */\nexport function effectControlChange(effect: LayerEffect, key: string, value: unknown, patterns: readonly EffectPattern[] = []): Record<string, unknown> {\n const [top, ...rest] = key.split('.');\n if (top === 'fill' && rest[0] === 'type') return { fill: fillOfType((effect as { fill: EffectFill }).fill, String(value), patterns) };\n if (top === 'patternId' && !rest.length) {\n const name = patternOptions(patterns).find((o) => o.value === value)?.label;\n return name ? { patternId: value, name } : { patternId: value };\n }\n if (!rest.length) return { [top]: value };\n // A pattern name follows its id.\n if (top === 'fill' && rest[0] === 'patternId') {\n const name = patternOptions(patterns).find((o) => o.value === value)?.label;\n return { fill: { ...(effect as { fill: Rec }).fill, patternId: value, ...(name ? { name } : {}) } };\n }\n return { [top]: setPath(getPath(effect, top), rest, value) };\n}\n","/** Framework-free helpers behind the PSD buttons of the React and Svelte layer panels. */\n\nimport type { ImageEditorController } from '../controller';\n\n/** Types the file pickers accept. */\nexport const PSD_FILE_ACCEPT = '.psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop';\n\nfunction blobBytes(file: Blob): Promise<Uint8Array> {\n if (typeof file.arrayBuffer === 'function') return file.arrayBuffer().then((b) => new Uint8Array(b));\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));\n reader.onerror = () => reject(reader.error ?? new Error('The file could not be read.'));\n reader.readAsArrayBuffer(file);\n });\n}\n\n/** Reads a picked file and opens it as the document (one undo step). Returns the import warnings. */\nexport async function openPsdFile(controller: ImageEditorController, file: Blob): Promise<string[]> {\n return controller.loadPsd(await blobBytes(file)).warnings;\n}\n\n/** A file name for the exported document: `name` with a `.psd` / `.psb` extension. */\nexport function psdFileName(name: string, psb: boolean): string {\n const base = name.replace(/\\.[a-z0-9]{2,4}$/i, '') || 'image';\n return `${base}.${psb ? 'psb' : 'psd'}`;\n}\n\n/**\n * Writes the document as a PSD and offers it as a download (in a browser). Returns the file\n * name and the export warnings.\n */\nexport async function downloadPsd(controller: ImageEditorController, name = 'image'): Promise<{ fileName: string; warnings: string[] }> {\n const res = await controller.toPsd();\n const fileName = psdFileName(name, res.psb);\n if (typeof document !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {\n const blob = new Blob([res.bytes as BlobPart], { type: 'image/vnd.adobe.photoshop' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = fileName;\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n }\n return { fileName, warnings: res.warnings };\n}\n","/**\n * Free Transform's view model, shared by the React and Svelte editors: the overlay (outline,\n * eight handles, the reference point), what a pointer is over, Photoshop's drags and modifiers,\n * the options bar's readout and fields, and the keyboard (Enter commits, Esc cancels, arrows\n * nudge). It drives a `FreeTransformSession`; the bindings forward pointer and key events and draw\n * `overlay()` through the renderer's `freeTransform` option.\n *\n * Drags (as Photoshop CC, where a corner scales proportionally by default):\n * - corner: scale from the opposite corner (Alt: from the reference point); Shift toggles\n * proportional; side: scale one axis;\n * - Mod (Ctrl / Cmd) + corner: distort that corner; Mod + side: skew along the side (Alt: about\n * the centre); Mod + Alt + Shift + corner: perspective;\n * - outside the box: rotate about the reference point (Shift: 15° steps); inside: move (Shift:\n * along one axis); the reference point: move it;\n * - the mode switch (rotate, skew, distort, perspective) makes every handle do that;\n * - Warp: drag a mesh anchor or handle (the grid stays smooth across its lines), or anywhere\n * inside to pull the surface; presets and the grid size come from the options bar. The warp is\n * applied in the frame's own space before the corner map, so both can be used together.\n */\n\nimport type { ImageEditorController } from '../controller';\nimport type { FreeTransformInit, FreeTransformSession, FreeTransformResult } from '../free-transform';\nimport {\n cornersFromMatrix,\n describeTransform,\n matrixFromParams,\n type FreeTransformMode,\n type Quad,\n type TransformAnchor,\n type TransformReadout,\n} from '../transform';\nimport { applyHomography, invertHomography, rectToQuad, type Homography } from '../../geom/homography';\nimport {\n WARP_STYLES,\n WARP_STYLE_LABELS,\n dragWarpMeshPoint,\n identityWarpMesh,\n pullWarpMesh,\n resizeWarpMesh,\n warpMeshEditPoints,\n warpMeshGridCurves,\n warpMeshParamAt,\n warpPresetMesh,\n type WarpPreset,\n} from '../../geom/warp';\nimport type { WarpMesh, WarpStyle } from '../../geom/types';\nimport type { Vec2 } from '../../geom/types';\nimport type { Interpolation } from '../../raster/transform';\nimport type { ImagePoint } from '../types';\nimport { flattenLayers } from './model';\nimport type { ShortcutEvent } from './paint-ui';\n\n/** What a pointer is over (`mesh:<index>`: a warp mesh control point). */\nexport type TransformHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'reference' | 'inside' | 'outside' | `mesh:${number}`;\n\nconst CORNERS = ['nw', 'ne', 'se', 'sw'] as const;\nconst SIDES = ['n', 'e', 's', 'w'] as const;\nconst CORNER_INDEX: Record<string, number> = { nw: 0, ne: 1, se: 2, sw: 3 };\nconst SIDE_CORNERS: Record<string, [number, number]> = { n: [0, 1], e: [1, 2], s: [2, 3], w: [3, 0] };\n/** Box fractions of each handle. */\nconst HANDLE_FRACTION: Record<string, [number, number]> = { nw: [0, 0], n: [0.5, 0], ne: [1, 0], e: [1, 0.5], se: [1, 1], s: [0.5, 1], sw: [0, 1], w: [0, 0.5] };\nconst ANCHOR_FRACTION: Record<TransformAnchor, [number, number]> = {\n 'top-left': [0, 0],\n top: [0.5, 0],\n 'top-right': [1, 0],\n left: [0, 0.5],\n center: [0.5, 0.5],\n right: [1, 0.5],\n 'bottom-left': [0, 1],\n bottom: [0.5, 1],\n 'bottom-right': [1, 1],\n};\n\nexport interface TransformModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n /** Ctrl or Cmd. */\n ctrlKey?: boolean;\n metaKey?: boolean;\n}\n\n/** The modes of the options bar's switch (Warp comes with its own mesh UI). */\nexport const FREE_TRANSFORM_MODE_OPTIONS: readonly { id: FreeTransformMode; label: string; title: string }[] = [\n { id: 'scale', label: 'Free', title: 'Free Transform: corners scale, outside rotates, Ctrl/Cmd distorts' },\n { id: 'rotate', label: 'Rotate', title: 'Every handle rotates about the reference point' },\n { id: 'skew', label: 'Skew', title: 'Sides slide along themselves' },\n { id: 'distort', label: 'Distort', title: 'Each corner moves freely' },\n { id: 'perspective', label: 'Perspective', title: 'A corner and its partner move symmetrically' },\n { id: 'warp', label: 'Warp', title: 'Bend through a mesh: drag its points or pull inside; presets in the bar' },\n];\n\n/** The Warp style menu: Custom (the mesh as dragged) and Photoshop's fifteen presets. */\nexport const WARP_STYLE_OPTIONS: readonly { id: WarpStyle | 'custom'; label: string }[] = [\n { id: 'custom', label: 'Custom' },\n ...WARP_STYLES.map((id) => ({ id, label: WARP_STYLE_LABELS[id] })),\n];\n\n/** The Warp grid menu: patches across and down. */\nexport const WARP_GRID_OPTIONS: readonly { id: string; label: string; columns: number; rows: number }[] = [\n { id: '1x1', label: 'Default', columns: 1, rows: 1 },\n { id: '3x3', label: '3 × 3', columns: 3, rows: 3 },\n { id: '5x5', label: '5 × 5', columns: 5, rows: 5 },\n];\n\n/** Warp preset settings as the options bar shows them (percent). */\nexport interface WarpBar {\n style: WarpStyle | 'custom';\n bend: number;\n horizontal: number;\n vertical: number;\n orientation: 'horizontal' | 'vertical';\n columns: number;\n rows: number;\n}\n\n/** The options bar's numeric fields (Photoshop's X, Y, W, H, angle and skews). */\nexport const FREE_TRANSFORM_FIELDS = [\n { id: 'x', label: 'X', unit: 'px', title: 'Reference point X (document pixels)' },\n { id: 'y', label: 'Y', unit: 'px', title: 'Reference point Y (document pixels)' },\n { id: 'scaleX', label: 'W', unit: '%', title: 'Width in percent of the original' },\n { id: 'scaleY', label: 'H', unit: '%', title: 'Height in percent of the original' },\n { id: 'rotate', label: '∠', unit: '°', title: 'Rotation in degrees, clockwise' },\n { id: 'skewX', label: 'H skew', unit: '°', title: 'Horizontal skew in degrees' },\n { id: 'skewY', label: 'V skew', unit: '°', title: 'Vertical skew in degrees' },\n] as const;\n\nexport type FreeTransformField = (typeof FREE_TRANSFORM_FIELDS)[number]['id'];\n\n/** What the options bar shows. */\nexport interface FreeTransformBar extends Record<FreeTransformField, number> {\n /** False after Distort or Perspective: the numbers no longer describe the shape. */\n affine: boolean;\n}\n\nexport interface TransformOverlay {\n /** The transformed frame, corner to corner (document pixels). */\n outline: ImagePoint[];\n /** The eight handles (none in Warp mode). */\n handles: Array<{ id: 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'; x: number; y: number }>;\n reference: ImagePoint;\n /** In Warp mode: the mesh's grid lines as cubics, its anchors, and its handles with the anchor each belongs to. */\n warp?: { curves: ImagePoint[][]; anchors: ImagePoint[]; handles: Array<{ point: ImagePoint; anchor: ImagePoint }> };\n}\n\nconst CURSOR: Record<TransformHandle, string> = {\n nw: 'nwse-resize',\n se: 'nwse-resize',\n ne: 'nesw-resize',\n sw: 'nesw-resize',\n n: 'ns-resize',\n s: 'ns-resize',\n e: 'ew-resize',\n w: 'ew-resize',\n reference: 'move',\n inside: 'move',\n outside: 'alias',\n};\nconst meshIndex = (h: TransformHandle | null) => (h && h.startsWith('mesh:') ? Number(h.slice(5)) : -1);\n\nconst mod = (m: TransformModifiers) => Boolean(m.ctrlKey || m.metaKey);\nconst add = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y });\nconst sub = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y });\n\nfunction rotateAbout(p: Vec2, c: Vec2, rad: number): Vec2 {\n const cos = Math.cos(rad);\n const sin = Math.sin(rad);\n const d = sub(p, c);\n return { x: c.x + d.x * cos - d.y * sin, y: c.y + d.x * sin + d.y * cos };\n}\n\nfunction insideQuad(q: readonly Vec2[], p: Vec2): boolean {\n let sign = 0;\n for (let i = 0; i < 4; i++) {\n const a = q[i];\n const b = q[(i + 1) % 4];\n const c = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);\n if (Math.abs(c) < 1e-12) continue;\n const s = Math.sign(c);\n if (sign && s !== sign) return false;\n sign = s;\n }\n return true;\n}\n\n/** Even-odd point-in-polygon (the warped mesh's border). */\nfunction insidePolygon(poly: readonly Vec2[], p: Vec2): boolean {\n let inside = false;\n for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {\n const a = poly[i];\n const b = poly[j];\n if (a.y > p.y !== b.y > p.y && p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x) inside = !inside;\n }\n return inside;\n}\n\nexport interface FreeTransformToolOptions {\n /** Defers preview resampling to the next frame (pass `requestAnimationFrame`; default: at once). */\n schedule?: (run: () => void) => void;\n /** Called whenever the overlay, readout or error changes. */\n onChange?: () => void;\n}\n\n/**\n * The interactive Free Transform (see the module comment). `begin` opens a session on the\n * controller; pointer and key methods drive it; `commit` / `cancel` close it.\n */\nexport class FreeTransformTool {\n private sessionRef: FreeTransformSession | null = null;\n private modeValue: FreeTransformMode = 'scale';\n /** The reference point in the untransformed frame's space (follows the transform). */\n private refBox: Vec2 = { x: 0, y: 0 };\n private corners: Quad | null = null;\n private drag: { handle: TransformHandle; start: Vec2; corners: Quad; refBox: Vec2; ref: Vec2; mesh: WarpMesh | null; st: { s: number; t: number } | null } | null = null;\n /** The warp mesh in the frame's own space (null: no warp). */\n private mesh: WarpMesh | null = null;\n private warpSettings: WarpBar = { style: 'custom', bend: 50, horizontal: 0, vertical: 0, orientation: 'horizontal', columns: 1, rows: 1 };\n private pending = false;\n private viewScale = 1;\n private viewClip: { x: number; y: number; width: number; height: number } | undefined;\n /** Values typed into the options bar since the last drag (V skew is not recovered from the shape). */\n private typed: Partial<Record<FreeTransformField, number>> = {};\n /** Width and height scale together when typed (the chain icon). */\n proportional = true;\n error: string | null = null;\n lastResult: FreeTransformResult | null = null;\n\n constructor(\n private readonly controller: ImageEditorController,\n private readonly options: FreeTransformToolOptions = {}\n ) {}\n\n get active(): boolean {\n return Boolean(this.sessionRef?.isOpen);\n }\n\n get session(): FreeTransformSession | null {\n return this.active ? this.sessionRef : null;\n }\n\n get mode(): FreeTransformMode {\n return this.modeValue;\n }\n\n get interpolation(): Interpolation {\n return this.sessionRef?.interpolation ?? 'bicubic';\n }\n\n private readonly listeners = new Set<() => void>();\n\n /** Calls `listener` whenever the overlay, readout, mode or error changes. Returns the unsubscribe. */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private changed(): void {\n this.options.onChange?.();\n for (const l of this.listeners) l();\n }\n\n /** Opens a Free Transform (default: the selected pixels of the active layer, or the whole layer). Returns an error message or `null`. */\n begin(init: FreeTransformInit = {}): string | null {\n if (this.active) return null;\n try {\n if (!init.layerId && init.content !== 'selection-outline' && !this.controller.getState().activeLayerId) {\n // Photoshop always has a target layer: use the topmost visible pixel layer.\n const top = [...flattenLayers(this.controller.getState().layers)].reverse().find((l) => l.type === 'pixel' && l.visible);\n if (top) this.controller.setActiveLayer(top.id);\n }\n const s = this.controller.beginFreeTransform(init);\n this.sessionRef = s;\n this.corners = s.geometry.corners;\n this.refBox = { x: s.box.x + s.box.width / 2, y: s.box.y + s.box.height / 2 };\n this.modeValue = 'scale';\n // A smart object's warp is part of its placement: keep editing it.\n this.mesh = s.geometry.warp ?? null;\n this.warpSettings = { style: 'custom', bend: 50, horizontal: 0, vertical: 0, orientation: 'horizontal', columns: 1, rows: 1 };\n this.typed = {};\n this.error = null;\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n }\n this.changed();\n return this.error;\n }\n\n setMode(mode: FreeTransformMode): void {\n this.modeValue = mode;\n const s = this.session;\n if (mode === 'warp' && s && !this.mesh) this.mesh = identityWarpMesh(s.box, this.warpSettings.columns, this.warpSettings.rows);\n this.changed();\n }\n\n /** The Warp options bar's settings. */\n warpBar(): WarpBar {\n return { ...this.warpSettings };\n }\n\n /** The warp mesh (frame space), or null. */\n get warp(): WarpMesh | null {\n return this.mesh;\n }\n\n /**\n * Applies Warp options: a preset `style` with `bend` / `horizontal` / `vertical` (percent) and\n * `orientation` replaces the mesh; `custom` keeps it; the grid size re-splits it (exactly when it\n * can). Switches to Warp mode.\n */\n setWarp(changes: Partial<WarpBar>): void {\n const s = this.session;\n if (!s) return;\n this.modeValue = 'warp';\n const next = { ...this.warpSettings, ...changes };\n this.warpSettings = next;\n let mesh = this.mesh ?? identityWarpMesh(s.box, next.columns, next.rows);\n if (next.style !== 'custom' && ('style' in changes || 'bend' in changes || 'horizontal' in changes || 'vertical' in changes || 'orientation' in changes)) {\n const preset: WarpPreset = { style: next.style, bend: next.bend, horizontal: next.horizontal, vertical: next.vertical, orientation: next.orientation };\n mesh = warpPresetMesh(preset, s.box) ?? identityWarpMesh(s.box);\n mesh = resizeWarpMesh(mesh, next.columns, next.rows);\n }\n if ('columns' in changes || 'rows' in changes) mesh = resizeWarpMesh(mesh, next.columns, next.rows);\n this.mesh = mesh;\n this.changed();\n this.preview();\n }\n\n /** Sets the reference point to an anchor of the frame (the options bar's locator). */\n setReference(anchor: TransformAnchor): void {\n const s = this.session;\n if (!s) return;\n const [fx, fy] = ANCHOR_FRACTION[anchor];\n this.refBox = { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height };\n this.changed();\n }\n\n setInterpolation(interpolation: Interpolation): void {\n const s = this.session;\n if (!s) return;\n this.run(() => s.update({ interpolation }, false));\n }\n\n /** Screen pixels per document pixel and the visible document area, for previews. */\n setView(scale: number, clip?: { x: number; y: number; width: number; height: number }): void {\n this.viewScale = scale > 0 ? scale : 1;\n this.viewClip = clip;\n }\n\n private homography() {\n const s = this.session;\n return s && this.corners ? rectToQuad(s.box, this.corners) : null;\n }\n\n /** The reference point in document pixels. */\n referencePoint(): Vec2 | null {\n const h = this.homography();\n return h ? applyHomography(h, this.refBox) : null;\n }\n\n /** The outline, handles and reference point to draw, or `null` when no transform is open. */\n overlay(): TransformOverlay | null {\n const s = this.session;\n const h = this.homography();\n if (!s || !h || !this.corners) return null;\n const handles = [...CORNERS, ...SIDES].map((id) => {\n const [fx, fy] = HANDLE_FRACTION[id];\n const p = applyHomography(h, { x: s.box.x + fx * s.box.width, y: s.box.y + fy * s.box.height }) ?? { x: 0, y: 0 };\n return { id, x: p.x, y: p.y };\n });\n const out: TransformOverlay = { outline: this.corners.map((p) => ({ x: p.x, y: p.y })), handles, reference: this.referencePoint() ?? { x: 0, y: 0 } };\n if (this.modeValue === 'warp' && this.mesh) {\n // Control points through the corner map: exact for affine corners, close for perspective.\n const m = this.mesh;\n const at = (p: Vec2) => applyHomography(h, p) ?? p;\n const pts = m.points.map(at);\n const edit = warpMeshEditPoints(m);\n out.handles = [];\n out.outline = warpMeshGridCurves({ ...m, points: pts }).length ? this.meshOutline(pts, m) : out.outline;\n out.warp = {\n curves: warpMeshGridCurves({ ...m, points: pts }),\n anchors: edit.anchors.map((i) => pts[i]),\n handles: edit.handles.map((e) => ({ point: pts[e.index], anchor: pts[e.anchor] })),\n };\n }\n return out;\n }\n\n /** The mesh's border anchors, clockwise (for the inside test). */\n private meshOutline(pts: Vec2[], m: WarpMesh): Vec2[] {\n const nx = 3 * (m.us.length - 1) + 1;\n const ny = 3 * (m.vs.length - 1) + 1;\n const ring: Vec2[] = [];\n for (let i = 0; i < nx; i++) ring.push(pts[i]);\n for (let j = 1; j < ny; j++) ring.push(pts[j * nx + nx - 1]);\n for (let i = nx - 2; i >= 0; i--) ring.push(pts[(ny - 1) * nx + i]);\n for (let j = ny - 2; j > 0; j--) ring.push(pts[j * nx]);\n return ring;\n }\n\n /** What a document point is over (`tolerance` in document pixels: a few screen pixels / scale). */\n hit(p: ImagePoint, tolerance: number): TransformHandle | null {\n const o = this.overlay();\n if (!o) return null;\n if (this.modeValue === 'warp' && this.mesh) {\n const h = this.homography()!;\n const edit = warpMeshEditPoints(this.mesh);\n let best = -1;\n let bestD = tolerance;\n for (const i of [...edit.anchors, ...edit.handles.map((e) => e.index)]) {\n const q = applyHomography(h, this.mesh.points[i]);\n if (!q) continue;\n const d = Math.hypot(q.x - p.x, q.y - p.y);\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n }\n if (best >= 0) return `mesh:${best}`;\n return insidePolygon(o.outline, p) ? 'inside' : 'outside';\n }\n // The nearest of the handles and the reference point within the tolerance (a small frame on\n // screen puts several within reach); handles win ties.\n let best: TransformHandle | null = null;\n let bestD = tolerance;\n for (const h of o.handles) {\n const d = Math.hypot(h.x - p.x, h.y - p.y);\n if (d <= bestD) {\n best = h.id;\n bestD = d;\n }\n }\n if (Math.hypot(o.reference.x - p.x, o.reference.y - p.y) < bestD) best = 'reference';\n if (best) return best;\n return insideQuad(o.outline, p) ? 'inside' : 'outside';\n }\n\n /** The CSS cursor for a handle. */\n cursor(handle: TransformHandle | null): string {\n if (!handle) return 'default';\n if (meshIndex(handle) >= 0) return 'pointer';\n if (this.modeValue === 'warp') return handle === 'inside' ? 'grab' : 'default';\n if (this.modeValue === 'rotate' && handle !== 'inside' && handle !== 'reference') return CURSOR.outside;\n return CURSOR[handle];\n }\n\n /** Starts a drag on what `p` is over. */\n down(p: ImagePoint, tolerance: number): TransformHandle | null {\n if (!this.session || !this.corners) return null;\n const handle = this.hit(p, tolerance);\n if (!handle) return null;\n let st: { s: number; t: number } | null = null;\n if (this.modeValue === 'warp' && this.mesh && handle === 'inside') {\n const q = this.toFrame(p, this.corners);\n if (q) {\n const r = warpMeshParamAt(this.mesh, q);\n st = { s: r.s, t: r.t };\n }\n }\n this.drag = { handle, start: { x: p.x, y: p.y }, corners: this.corners, refBox: { ...this.refBox }, ref: this.referencePoint() ?? p, mesh: this.mesh, st };\n return handle;\n }\n\n /** A document point in the frame's own space under `corners`. */\n private toFrame(p: Vec2, corners: Quad): Vec2 | null {\n const s = this.session;\n const h: Homography | null = s ? rectToQuad(s.box, corners) : null;\n const inv = h && invertHomography(h);\n return inv ? applyHomography(inv, p) : null;\n }\n\n /** Continues the drag to `p` with the modifiers held now. */\n move(p: ImagePoint, m: TransformModifiers = {}): void {\n const d = this.drag;\n const s = this.session;\n if (!d || !s) return;\n if (d.handle === 'reference') {\n const h = rectToQuad(s.box, d.corners);\n const inv = h && invertHomography(h);\n const q = inv && applyHomography(inv, p);\n if (q) this.refBox = q;\n this.changed();\n return;\n }\n if (this.modeValue === 'warp' && d.mesh) {\n // Warp: mesh points and pulls in the frame's own space; outside the mesh does nothing.\n const a = this.toFrame(d.start, d.corners);\n const b = this.toFrame(p, d.corners);\n if (!a || !b) return;\n const delta = { x: b.x - a.x, y: b.y - a.y };\n const i = meshIndex(d.handle);\n if (i >= 0) this.mesh = dragWarpMeshPoint(d.mesh, i, delta);\n else if (d.handle === 'inside' && d.st) this.mesh = pullWarpMesh(d.mesh, d.st.s, d.st.t, delta);\n else return;\n this.warpSettings = { ...this.warpSettings, style: 'custom' };\n this.changed();\n this.preview();\n return;\n }\n const corners = this.dragCorners(d, p, m);\n if (corners) this.setCorners(corners);\n }\n\n /** Ends the drag (the transform stays open). */\n up(): void {\n this.drag = null;\n }\n\n private dragCorners(d: NonNullable<FreeTransformTool['drag']>, p: Vec2, m: TransformModifiers): Quad | null {\n const s = this.session!;\n const c0 = d.corners;\n let delta = sub(p, d.start);\n const map = (f: (q: Vec2, i: number) => Vec2) => c0.map(f) as unknown as Quad;\n const handle = d.handle;\n const isCorner = handle in CORNER_INDEX;\n const isSide = handle in SIDE_CORNERS;\n const rotating = handle === 'outside' || (this.modeValue === 'rotate' && (isCorner || isSide));\n if (handle === 'inside') {\n if (m.shiftKey) delta = Math.abs(delta.x) >= Math.abs(delta.y) ? { x: delta.x, y: 0 } : { x: 0, y: delta.y };\n return map((q) => add(q, delta));\n }\n if (rotating) {\n const r = d.ref;\n let a = Math.atan2(p.y - r.y, p.x - r.x) - Math.atan2(d.start.y - r.y, d.start.x - r.x);\n if (m.shiftKey) a = Math.round(a / (Math.PI / 12)) * (Math.PI / 12);\n return map((q) => rotateAbout(q, r, a));\n }\n const perspective = this.modeValue === 'perspective' || (mod(m) && m.altKey && m.shiftKey);\n const distort = this.modeValue === 'distort' || (mod(m) && !perspective);\n const skew = this.modeValue === 'skew' || (mod(m) && isSide);\n if (isSide && (skew || perspective)) {\n // Slide the side along its own direction (Alt: the opposite side the other way).\n const [i, j] = SIDE_CORNERS[handle];\n const dir = sub(c0[j], c0[i]);\n const len = Math.hypot(dir.x, dir.y) || 1;\n const t = (delta.x * dir.x + delta.y * dir.y) / len;\n const v = { x: (dir.x / len) * t, y: (dir.y / len) * t };\n const [oi, oj] = SIDE_CORNERS[{ n: 's', s: 'n', e: 'w', w: 'e' }[handle as 'n']];\n return map((q, k) => (k === i || k === j ? add(q, v) : m.altKey && (k === oi || k === oj) ? sub(q, v) : q));\n }\n if (isCorner && perspective) {\n const k = CORNER_INDEX[handle];\n if (Math.abs(delta.x) >= Math.abs(delta.y)) {\n const partner = [1, 0, 3, 2][k];\n return map((q, i) => (i === k ? { x: q.x + delta.x, y: q.y } : i === partner ? { x: q.x - delta.x, y: q.y } : q));\n }\n const partner = [3, 2, 1, 0][k];\n return map((q, i) => (i === k ? { x: q.x, y: q.y + delta.y } : i === partner ? { x: q.x, y: q.y - delta.y } : q));\n }\n if (distort || (isCorner && this.modeValue === 'skew')) {\n if (isCorner) return map((q, i) => (i === CORNER_INDEX[handle] ? add(q, delta) : q));\n const [i, j] = SIDE_CORNERS[handle];\n return map((q, k) => (k === i || k === j ? add(q, delta) : q));\n }\n // Scale in the frame's own space, so a rotated or distorted frame scales along its sides.\n const h = rectToQuad(s.box, c0);\n const inv = h && invertHomography(h);\n if (!h || !inv) return null;\n const q = applyHomography(inv, p);\n const q0 = applyHomography(inv, d.start);\n if (!q || !q0) return null;\n const b = s.box;\n const [fx, fy] = HANDLE_FRACTION[handle];\n const grab = { x: b.x + fx * b.width, y: b.y + fy * b.height };\n const anchor = m.altKey ? d.refBox : { x: b.x + (1 - fx) * b.width, y: b.y + (1 - fy) * b.height };\n const target = add(grab, sub(q, q0));\n const ax = grab.x - anchor.x;\n const ay = grab.y - anchor.y;\n let sx = isCorner || handle === 'e' || handle === 'w' ? (Math.abs(ax) > 1e-9 ? (target.x - anchor.x) / ax : 1) : 1;\n let sy = isCorner || handle === 'n' || handle === 's' ? (Math.abs(ay) > 1e-9 ? (target.y - anchor.y) / ay : 1) : 1;\n // Corners scale proportionally by default; Shift frees them.\n if (isCorner && !m.shiftKey) {\n const k = Math.abs(sx) >= Math.abs(sy) ? sx : sy;\n sx = k;\n sy = k;\n }\n const scaled = [\n { x: b.x, y: b.y },\n { x: b.x + b.width, y: b.y },\n { x: b.x + b.width, y: b.y + b.height },\n { x: b.x, y: b.y + b.height },\n ].map((c) => applyHomography(h, { x: anchor.x + (c.x - anchor.x) * sx, y: anchor.y + (c.y - anchor.y) * sy }));\n return scaled.every(Boolean) ? (scaled as unknown as Quad) : null;\n }\n\n /** Sets new corners and refreshes the preview (scheduled). Invalid shapes keep the last good one. */\n setCorners(corners: Quad): void {\n const s = this.session;\n if (!s) return;\n const h = rectToQuad(s.box, corners);\n if (!h) return;\n this.corners = corners;\n this.typed = {};\n this.changed();\n this.preview();\n }\n\n private preview(): void {\n if (this.pending) return;\n this.pending = true;\n const run = () => {\n this.pending = false;\n const s = this.session;\n if (!s || !this.corners) return;\n this.run(() => s.update({ corners: this.corners!, warp: this.mesh }, { scale: this.viewScale, clip: this.viewClip }));\n };\n if (this.options.schedule) this.options.schedule(run);\n else run();\n }\n\n private run(fn: () => void): void {\n try {\n fn();\n this.error = null;\n } catch (e) {\n this.error = e instanceof Error ? e.message : String(e);\n if (this.sessionRef && !this.sessionRef.isOpen) this.sessionRef = null;\n }\n this.changed();\n }\n\n /** The options bar's numbers for the current shape. */\n bar(): FreeTransformBar | null {\n const s = this.session;\n if (!s || !this.corners) return null;\n const read: TransformReadout = describeTransform({ box: s.box, corners: this.corners }, this.refBox);\n return {\n affine: read.affine,\n x: this.typed.x ?? read.x,\n y: this.typed.y ?? read.y,\n scaleX: this.typed.scaleX ?? read.scaleX,\n scaleY: this.typed.scaleY ?? read.scaleY,\n rotate: this.typed.rotate ?? read.rotate,\n skewX: this.typed.skewX ?? read.skewX,\n skewY: this.typed.skewY ?? 0,\n };\n }\n\n /** Applies a typed options-bar value (W and H together while `proportional`). */\n setField(field: FreeTransformField, value: number): void {\n const s = this.session;\n const cur = this.bar();\n if (!s || !cur || !Number.isFinite(value)) return;\n const next = { ...cur, [field]: value };\n if (this.proportional && field === 'scaleX') next.scaleY = cur.scaleY === 0 ? value : (value / cur.scaleX) * cur.scaleY;\n if (this.proportional && field === 'scaleY') next.scaleX = cur.scaleX === 0 ? value : (value / cur.scaleY) * cur.scaleX;\n if (!next.scaleX || !next.scaleY) {\n this.error = 'Width and height must not be 0 %.';\n this.changed();\n return;\n }\n const m = matrixFromParams(s.box, {\n scaleX: next.scaleX,\n scaleY: next.scaleY,\n rotate: next.rotate,\n skewX: next.skewX,\n skewY: next.skewY,\n reference: this.refBox,\n dx: next.x - this.refBox.x,\n dy: next.y - this.refBox.y,\n });\n this.setCorners(cornersFromMatrix(s.box, m));\n this.typed = { scaleX: next.scaleX, scaleY: next.scaleY, rotate: next.rotate, skewX: next.skewX, skewY: next.skewY, x: next.x, y: next.y };\n this.changed();\n }\n\n /** Finishes with full-quality resampling (one undo step). */\n commit(): FreeTransformResult | null {\n const s = this.session;\n if (!s) return null;\n let res: FreeTransformResult | null = null;\n this.pending = false;\n this.run(() => {\n if (this.corners) s.update({ corners: this.corners, warp: this.mesh }, false);\n res = s.commit();\n });\n this.lastResult = res;\n this.sessionRef = null;\n this.corners = null;\n this.mesh = null;\n this.drag = null;\n this.changed();\n return res;\n }\n\n /** Abandons the transform and restores everything. */\n cancel(): void {\n this.sessionRef?.cancel();\n this.sessionRef = null;\n this.corners = null;\n this.mesh = null;\n this.drag = null;\n this.pending = false;\n this.error = null;\n this.changed();\n }\n\n /**\n * Keyboard: Enter commits, Escape cancels, arrows nudge by 1 px (Shift: 10). Ctrl/Cmd+T opens a\n * Free Transform when none is open. Returns true when the key was used (prevent its default).\n */\n key(e: ShortcutEvent): boolean {\n const t = e.target as { tagName?: string; isContentEditable?: boolean } | null;\n const typing = Boolean(t && (t.isContentEditable || t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT'));\n if (!this.active) {\n if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === 't' && !typing) {\n this.begin();\n return true;\n }\n return false;\n }\n if (e.key === 'Enter') {\n this.commit();\n return true;\n }\n if (e.key === 'Escape') {\n this.cancel();\n return true;\n }\n if (typing) return false;\n const step = e.shiftKey ? 10 : 1;\n const nudge: Record<string, Vec2> = { ArrowLeft: { x: -step, y: 0 }, ArrowRight: { x: step, y: 0 }, ArrowUp: { x: 0, y: -step }, ArrowDown: { x: 0, y: step } };\n const v = nudge[e.key];\n if (v && this.corners) {\n this.setCorners(this.corners.map((q) => add(q, v)) as unknown as Quad);\n return true;\n }\n return false;\n }\n}\n"]}